From b6dd22bb6d68c229b89d130a0f494df61dfea971 Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 3 Sep 2021 09:54:41 +0800 Subject: [PATCH 0001/1041] create gh-pages brnch --- .gitattributes | 1 + .github/workflows/deploy_docs.yml | 26 + .github/workflows/tests.yml | 20 + .gitignore | 25 + .mvn/maven.config | 1 + BUILD.md | 25 + CHANGELOG.md | 13 + Dockerfile | 11 + LICENSE.md | 132 + README.md | 82 + convex | 8 + convex-benchmarks/.gitignore | 4 + convex-benchmarks/README.md | 41 + convex-benchmarks/pom.xml | 84 + .../java/convex/benchmarks/Benchmarks.java | 55 + .../convex/benchmarks/BigBlockBenchmark.java | 65 + .../java/convex/benchmarks/CVMBenchmark.java | 84 + .../java/convex/benchmarks/EtchBenchmark.java | 53 + .../java/convex/benchmarks/EvalBenchmark.java | 49 + .../java/convex/benchmarks/HashBenchmark.java | 32 + .../convex/benchmarks/LatencyBenchmark.java | 113 + .../convex/benchmarks/ListDataBenchmark.java | 25 + .../java/convex/benchmarks/MapBenchmark.java | 26 + .../java/convex/benchmarks/OpBenchmark.java | 55 + .../convex/benchmarks/SignatureBenchmark.java | 61 + .../ThreadCoordinationBenchmark.java | 48 + convex-cli/.gitignore | 5 + convex-cli/pom.xml | 213 + .../src/docs/asciidoc/images/convex_logo.svg | 1 + convex-cli/src/docs/asciidoc/index.adoc | 660 +++ .../src/main/java/convex/cli/Account.java | 38 + .../main/java/convex/cli/AccountBalance.java | 75 + .../main/java/convex/cli/AccountCreate.java | 117 + .../src/main/java/convex/cli/AccountFund.java | 96 + .../java/convex/cli/AccountInformation.java | 77 + .../src/main/java/convex/cli/Constants.java | 27 + .../src/main/java/convex/cli/Helpers.java | 114 + convex-cli/src/main/java/convex/cli/Key.java | 36 + .../src/main/java/convex/cli/KeyExport.java | 98 + .../src/main/java/convex/cli/KeyGenerate.java | 62 + .../src/main/java/convex/cli/KeyImport.java | 83 + .../src/main/java/convex/cli/KeyList.java | 63 + .../src/main/java/convex/cli/Local.java | 35 + .../src/main/java/convex/cli/LocalGUI.java | 41 + .../src/main/java/convex/cli/LocalStart.java | 129 + convex-cli/src/main/java/convex/cli/Main.java | 389 ++ convex-cli/src/main/java/convex/cli/Peer.java | 37 + .../src/main/java/convex/cli/PeerCreate.java | 146 + .../src/main/java/convex/cli/PeerStart.java | 142 + .../src/main/java/convex/cli/Query.java | 82 + .../src/main/java/convex/cli/Status.java | 108 + .../src/main/java/convex/cli/Transaction.java | 107 + .../main/java/convex/cli/output/Output.java | 70 + .../java/convex/cli/output/OutputField.java | 25 + .../java/convex/cli/peer/PeerManager.java | 349 ++ .../main/java/convex/cli/peer/Session.java | 147 + .../java/convex/cli/peer/SessionItem.java | 106 + .../convex/cli/CLICommandKeyExportTest.java | 68 + .../convex/cli/CLICommandKeyImportTest.java | 34 + .../java/convex/cli/CLICommandKeyTest.java | 28 + .../src/test/java/convex/cli/CLIHelpTest.java | 33 + .../src/test/java/convex/cli/CLIMainTest.java | 51 + .../java/convex/cli/CommandLineTester.java | 69 + .../src/test/java/convex/cli/Helper.java | 19 + .../java/convex/cli/output/OutputTest.java | 68 + .../java/convex/cli/peer/SessionTest.java | 107 + convex-core/.gitignore | 5 + convex-core/pom.xml | 125 + .../convex/core/lang/reader/antlr/Convex.g4 | 189 + convex-core/src/main/assembly/full.xml | 34 + convex-core/src/main/assembly/testing.xml | 30 + convex-core/src/main/cvx/asset/box.cvx | 100 + convex-core/src/main/cvx/asset/box/actor.cvx | 301 ++ convex-core/src/main/cvx/asset/nft/simple.cvx | 208 + convex-core/src/main/cvx/asset/nft/tokens.cvx | 747 ++++ convex-core/src/main/cvx/convex/asset.cvx | 392 ++ convex-core/src/main/cvx/convex/core.cvx | 692 +++ .../src/main/cvx/convex/core/metadata.cvx | 1158 +++++ convex-core/src/main/cvx/convex/fungible.cvx | 380 ++ convex-core/src/main/cvx/convex/play.cvx | 80 + convex-core/src/main/cvx/convex/registry.cvx | 140 + convex-core/src/main/cvx/convex/trust.cvx | 260 ++ .../src/main/cvx/convex/trusted-oracle.cvx | 141 + .../main/cvx/convex/trusted-oracle/actor.cvx | 107 + convex-core/src/main/cvx/lab/convex/xform.cvx | 175 + convex-core/src/main/cvx/lab/messenger.cvx | 9 + .../src/main/cvx/lab/prediction-market.cvx | 102 + convex-core/src/main/cvx/lab/secured-loan.con | 18 + convex-core/src/main/cvx/torus/currencies.cvx | 12 + convex-core/src/main/cvx/torus/exchange.cvx | 632 +++ .../src/main/java/convex/core/Belief.java | 723 ++++ .../src/main/java/convex/core/Block.java | 243 ++ .../main/java/convex/core/BlockResult.java | 211 + .../src/main/java/convex/core/Coin.java | 43 + .../src/main/java/convex/core/Constants.java | 195 + .../src/main/java/convex/core/ErrorCodes.java | 190 + .../main/java/convex/core/MergeContext.java | 87 + .../src/main/java/convex/core/Order.java | 309 ++ .../src/main/java/convex/core/Peer.java | 564 +++ .../src/main/java/convex/core/Result.java | 199 + .../src/main/java/convex/core/State.java | 808 ++++ .../java/convex/core/crypto/AKeyPair.java | 117 + .../java/convex/core/crypto/ASignature.java | 90 + .../convex/core/crypto/Ed25519KeyPair.java | 336 ++ .../convex/core/crypto/Ed25519Signature.java | 141 + .../java/convex/core/crypto/Encoding.java | 12 + .../main/java/convex/core/crypto/Hashing.java | 143 + .../convex/core/crypto/InsecureRandom.java | 83 + .../java/convex/core/crypto/Mnemonic.java | 257 ++ .../src/main/java/convex/core/crypto/PBE.java | 34 + .../java/convex/core/crypto/PEMTools.java | 163 + .../java/convex/core/crypto/PFXTools.java | 170 + .../java/convex/core/crypto/Providers.java | 27 + .../java/convex/core/crypto/Symmetric.java | 160 + .../main/java/convex/core/crypto/Wallet.java | 69 + .../java/convex/core/crypto/WalletEntry.java | 86 + .../java/convex/core/data/AArrayBlob.java | 299 ++ .../src/main/java/convex/core/data/ABlob.java | 393 ++ .../main/java/convex/core/data/ABlobMap.java | 115 + .../src/main/java/convex/core/data/ACell.java | 495 +++ .../java/convex/core/data/ACollection.java | 211 + .../java/convex/core/data/ACountable.java | 56 + .../java/convex/core/data/ADataStructure.java | 121 + .../main/java/convex/core/data/AHashMap.java | 171 + .../main/java/convex/core/data/AHashSet.java | 160 + .../src/main/java/convex/core/data/AList.java | 74 + .../main/java/convex/core/data/ALongBlob.java | 150 + .../src/main/java/convex/core/data/AMap.java | 323 ++ .../main/java/convex/core/data/AMapEntry.java | 156 + .../java/convex/core/data/ANumericBlob.java | 65 + .../main/java/convex/core/data/AObject.java | 56 + .../main/java/convex/core/data/ARecord.java | 340 ++ .../java/convex/core/data/ARecordGeneric.java | 102 + .../main/java/convex/core/data/ASequence.java | 262 ++ .../src/main/java/convex/core/data/ASet.java | 216 + .../main/java/convex/core/data/AString.java | 84 + .../main/java/convex/core/data/ASymbolic.java | 70 + .../main/java/convex/core/data/AVector.java | 268 ++ .../java/convex/core/data/AccountKey.java | 281 ++ .../java/convex/core/data/AccountStatus.java | 500 +++ .../main/java/convex/core/data/Address.java | 227 + .../src/main/java/convex/core/data/Blob.java | 269 ++ .../main/java/convex/core/data/BlobMap.java | 744 ++++ .../main/java/convex/core/data/BlobMaps.java | 38 + .../main/java/convex/core/data/BlobTree.java | 503 +++ .../src/main/java/convex/core/data/Blobs.java | 96 + .../main/java/convex/core/data/Format.java | 981 +++++ .../src/main/java/convex/core/data/Hash.java | 223 + .../java/convex/core/data/IAssociative.java | 12 + .../main/java/convex/core/data/INumeric.java | 33 + .../java/convex/core/data/IRefFunction.java | 16 + .../java/convex/core/data/IValidated.java | 26 + .../java/convex/core/data/IWriteable.java | 31 + .../main/java/convex/core/data/Keyword.java | 150 + .../main/java/convex/core/data/Keywords.java | 89 + .../src/main/java/convex/core/data/List.java | 425 ++ .../src/main/java/convex/core/data/Lists.java | 19 + .../main/java/convex/core/data/LongBlob.java | 146 + .../main/java/convex/core/data/MapEntry.java | 345 ++ .../main/java/convex/core/data/MapLeaf.java | 757 ++++ .../main/java/convex/core/data/MapTree.java | 880 ++++ .../src/main/java/convex/core/data/Maps.java | 156 + .../java/convex/core/data/PeerStatus.java | 283 ++ .../src/main/java/convex/core/data/Ref.java | 687 +++ .../main/java/convex/core/data/RefDirect.java | 130 + .../main/java/convex/core/data/RefSoft.java | 151 + .../main/java/convex/core/data/SetLeaf.java | 535 +++ .../main/java/convex/core/data/SetTree.java | 655 +++ .../src/main/java/convex/core/data/Sets.java | 106 + .../java/convex/core/data/SignedData.java | 306 ++ .../java/convex/core/data/StringShort.java | 163 + .../java/convex/core/data/StringSlice.java | 99 + .../java/convex/core/data/StringTree.java | 190 + .../main/java/convex/core/data/Strings.java | 42 + .../main/java/convex/core/data/Symbol.java | 147 + .../main/java/convex/core/data/Syntax.java | 336 ++ .../src/main/java/convex/core/data/Tag.java | 87 + .../java/convex/core/data/VectorArray.java | 216 + .../java/convex/core/data/VectorLeaf.java | 763 ++++ .../java/convex/core/data/VectorTree.java | 719 ++++ .../main/java/convex/core/data/Vectors.java | 134 + .../java/convex/core/data/package-info.java | 5 + .../convex/core/data/prim/APrimitive.java | 66 + .../java/convex/core/data/prim/CVMBool.java | 101 + .../java/convex/core/data/prim/CVMByte.java | 117 + .../java/convex/core/data/prim/CVMChar.java | 132 + .../java/convex/core/data/prim/CVMDouble.java | 130 + .../java/convex/core/data/prim/CVMLong.java | 129 + .../convex/core/data/type/ANumericType.java | 11 + .../convex/core/data/type/AStandardType.java | 42 + .../java/convex/core/data/type/AType.java | 46 + .../convex/core/data/type/AddressType.java | 39 + .../main/java/convex/core/data/type/Any.java | 46 + .../main/java/convex/core/data/type/Blob.java | 39 + .../convex/core/data/type/BlobMapType.java | 41 + .../java/convex/core/data/type/Boolean.java | 40 + .../main/java/convex/core/data/type/Byte.java | 40 + .../convex/core/data/type/CharacterType.java | 40 + .../convex/core/data/type/Collection.java | 39 + .../convex/core/data/type/DataStructure.java | 40 + .../java/convex/core/data/type/Double.java | 40 + .../java/convex/core/data/type/Function.java | 40 + .../convex/core/data/type/KeywordType.java | 41 + .../main/java/convex/core/data/type/List.java | 39 + .../main/java/convex/core/data/type/Long.java | 40 + .../main/java/convex/core/data/type/Map.java | 40 + .../main/java/convex/core/data/type/Nil.java | 47 + .../java/convex/core/data/type/Number.java | 48 + .../java/convex/core/data/type/OpCode.java | 42 + .../java/convex/core/data/type/Record.java | 38 + .../java/convex/core/data/type/Sequence.java | 40 + .../main/java/convex/core/data/type/Set.java | 39 + .../convex/core/data/type/StringType.java | 41 + .../convex/core/data/type/SymbolType.java | 41 + .../convex/core/data/type/SyntaxType.java | 40 + .../convex/core/data/type/Transaction.java | 29 + .../java/convex/core/data/type/Types.java | 87 + .../java/convex/core/data/type/Vector.java | 40 + .../core/exceptions/BadFormatException.java | 21 + .../exceptions/BadSignatureException.java | 21 + .../convex/core/exceptions/BaseException.java | 27 + .../core/exceptions/InvalidDataException.java | 21 + .../core/exceptions/MissingDataException.java | 37 + .../core/exceptions/ParseException.java | 17 + .../convex/core/exceptions/TODOException.java | 18 + .../core/exceptions/ValidationException.java | 16 + .../java/convex/core/init/AInitConfig.java | 79 + .../src/main/java/convex/core/init/Init.java | 350 ++ .../src/main/java/convex/core/lang/AFn.java | 60 + .../src/main/java/convex/core/lang/AOp.java | 91 + .../main/java/convex/core/lang/Compiler.java | 868 ++++ .../main/java/convex/core/lang/Context.java | 2262 ++++++++++ .../src/main/java/convex/core/lang/Core.java | 2469 +++++++++++ .../src/main/java/convex/core/lang/IFn.java | 25 + .../src/main/java/convex/core/lang/Juice.java | 332 ++ .../src/main/java/convex/core/lang/Ops.java | 94 + .../src/main/java/convex/core/lang/RT.java | 1381 ++++++ .../main/java/convex/core/lang/Reader.java | 78 + .../main/java/convex/core/lang/Symbols.java | 297 ++ .../java/convex/core/lang/impl/AClosure.java | 35 + .../java/convex/core/lang/impl/ADataFn.java | 77 + .../convex/core/lang/impl/AExceptional.java | 32 + .../java/convex/core/lang/impl/AReturn.java | 8 + .../convex/core/lang/impl/ATrampoline.java | 28 + .../java/convex/core/lang/impl/CoreFn.java | 128 + .../java/convex/core/lang/impl/CorePred.java | 29 + .../convex/core/lang/impl/ErrorValue.java | 137 + .../main/java/convex/core/lang/impl/Fn.java | 216 + .../java/convex/core/lang/impl/HaltValue.java | 43 + .../java/convex/core/lang/impl/ICoreDef.java | 20 + .../java/convex/core/lang/impl/KeywordFn.java | 52 + .../java/convex/core/lang/impl/MapFn.java | 41 + .../java/convex/core/lang/impl/MultiFn.java | 149 + .../convex/core/lang/impl/RecordFormat.java | 59 + .../convex/core/lang/impl/RecurValue.java | 45 + .../java/convex/core/lang/impl/Reduced.java | 32 + .../convex/core/lang/impl/ReturnValue.java | 43 + .../convex/core/lang/impl/RollbackValue.java | 43 + .../java/convex/core/lang/impl/SeqFn.java | 56 + .../java/convex/core/lang/impl/SetFn.java | 38 + .../convex/core/lang/impl/TailcallValue.java | 49 + .../java/convex/core/lang/ops/AMultiOp.java | 67 + .../main/java/convex/core/lang/ops/Cond.java | 112 + .../java/convex/core/lang/ops/Constant.java | 131 + .../main/java/convex/core/lang/ops/Def.java | 155 + .../main/java/convex/core/lang/ops/Do.java | 88 + .../java/convex/core/lang/ops/Invoke.java | 108 + .../java/convex/core/lang/ops/Lambda.java | 111 + .../main/java/convex/core/lang/ops/Let.java | 179 + .../main/java/convex/core/lang/ops/Local.java | 104 + .../java/convex/core/lang/ops/Lookup.java | 136 + .../main/java/convex/core/lang/ops/Query.java | 94 + .../main/java/convex/core/lang/ops/Set.java | 128 + .../java/convex/core/lang/ops/Special.java | 154 + .../convex/core/lang/reader/AntlrReader.java | 477 +++ .../convex/core/lang/reader/ReaderUtils.java | 79 + .../main/java/convex/core/store/AStore.java | 115 + .../java/convex/core/store/BlobCache.java | 66 + .../java/convex/core/store/MemoryStore.java | 108 + .../main/java/convex/core/store/Stores.java | 70 + .../core/transactions/ATransaction.java | 118 + .../java/convex/core/transactions/Call.java | 144 + .../java/convex/core/transactions/Invoke.java | 173 + .../convex/core/transactions/Transfer.java | 147 + .../src/main/java/convex/core/util/Bits.java | 106 + .../main/java/convex/core/util/Counters.java | 25 + .../main/java/convex/core/util/Economics.java | 53 + .../main/java/convex/core/util/Errors.java | 54 + .../src/main/java/convex/core/util/Huge.java | 137 + .../java/convex/core/util/MergeFunction.java | 16 + .../main/java/convex/core/util/Shutdown.java | 77 + .../src/main/java/convex/core/util/Text.java | 71 + .../src/main/java/convex/core/util/UMath.java | 35 + .../src/main/java/convex/core/util/Utils.java | 1339 ++++++ convex-core/src/main/java/etch/Etch.java | 909 ++++ convex-core/src/main/java/etch/EtchStore.java | 218 + convex-core/src/test/cvx/test/asset/box.cvx | 301 ++ .../src/test/cvx/test/asset/nft/simple.cvx | 278 ++ .../src/test/cvx/test/asset/nft/tokens.cvx | 1042 +++++ .../src/test/cvx/test/convex/asset.cvx | 167 + .../test/convex/asset/quantity/set-long.cvx | 59 + .../src/test/cvx/test/convex/fungible.cvx | 509 +++ .../test/cvx/test/convex/fungible/generic.cvx | 46 + convex-core/src/test/cvx/test/convex/play.cvx | 97 + .../src/test/cvx/test/convex/registry.cvx | 153 + .../src/test/cvx/test/convex/trust.cvx | 390 ++ .../test/cvx/test/convex/trusted-oracle.cvx | 109 + .../src/test/cvx/test/torus/exchange.cvx | 499 +++ .../test/java/convex/actors/ActorsTest.java | 256 ++ .../convex/actors/PredictionMarketTest.java | 168 + .../test/java/convex/actors/RegistryTest.java | 111 + .../test/java/convex/actors/TorusTest.java | 251 ++ .../test/java/convex/comms/GenTestFormat.java | 59 + .../java/convex/comms/VLCEncodingTest.java | 142 + .../test/java/convex/comms/VLCParamTest.java | 56 + .../java/convex/core/BeliefMergeTest.java | 497 +++ .../java/convex/core/BeliefVotingTest.java | 16 + .../java/convex/core/MessageSizeTest.java | 32 + .../src/test/java/convex/core/PeerTest.java | 118 + .../src/test/java/convex/core/ResultTest.java | 31 + .../test/java/convex/core/StakingTest.java | 71 + .../src/test/java/convex/core/StateTest.java | 72 + .../convex/core/StateTransitionsTest.java | 317 ++ .../java/convex/core/TransactionTest.java | 49 + .../convex/core/crypto/AccountKeyTest.java | 20 + .../java/convex/core/crypto/Ed25519Test.java | 201 + .../java/convex/core/crypto/HashTest.java | 102 + .../java/convex/core/crypto/MnemonicTest.java | 43 + .../test/java/convex/core/crypto/PBETest.java | 16 + .../java/convex/core/crypto/PEMToolsTest.java | 56 + .../test/java/convex/core/crypto/PFXTest.java | 41 + .../convex/core/crypto/ParamTestHash.java | 47 + .../convex/core/crypto/SignKeyPairTest.java | 20 + .../convex/core/crypto/SymmetricTest.java | 41 + .../java/convex/core/crypto/WalletTest.java | 17 + .../java/convex/core/data/AccountKeyTest.java | 46 + .../java/convex/core/data/AddressTest.java | 33 + .../java/convex/core/data/BlobMapsTest.java | 297 ++ .../test/java/convex/core/data/BlobsTest.java | 239 ++ .../java/convex/core/data/BlocksTest.java | 49 + .../convex/core/data/CollectionsTest.java | 138 + .../java/convex/core/data/EncodingTest.java | 223 + .../java/convex/core/data/FuzzTestFormat.java | 97 + .../convex/core/data/GenTestAnyValue.java | 143 + .../java/convex/core/data/GenTestBlobs.java | 25 + .../core/data/GenTestDataStructures.java | 72 + .../java/convex/core/data/GenTestMap.java | 72 + .../convex/core/data/GenTestMessages.java | 25 + .../java/convex/core/data/GenTestStrings.java | 19 + .../java/convex/core/data/GenTestVectors.java | 44 + .../java/convex/core/data/KeywordTest.java | 67 + .../test/java/convex/core/data/ListsTest.java | 90 + .../test/java/convex/core/data/MapsTest.java | 361 ++ .../java/convex/core/data/ObjectsTest.java | 183 + .../java/convex/core/data/ParamTestBlobs.java | 72 + .../java/convex/core/data/ParamTestOps.java | 79 + .../java/convex/core/data/ParamTestRefs.java | 81 + .../convex/core/data/ParamTestValues.java | 69 + .../convex/core/data/ParamTestVector.java | 77 + .../java/convex/core/data/RecordTest.java | 90 + .../test/java/convex/core/data/RefTest.java | 182 + .../test/java/convex/core/data/SetsTest.java | 209 + .../java/convex/core/data/SignedDataTest.java | 110 + .../java/convex/core/data/StreamsTest.java | 33 + .../java/convex/core/data/StringsTest.java | 31 + .../java/convex/core/data/SymbolTest.java | 55 + .../java/convex/core/data/SyntaxTest.java | 59 + .../java/convex/core/data/TreeVectorTest.java | 81 + .../java/convex/core/data/VectorsTest.java | 337 ++ .../java/convex/core/data/prim/ByteTest.java | 39 + .../java/convex/core/data/prim/LongTest.java | 14 + .../java/convex/core/data/type/TypesTest.java | 183 + .../test/java/convex/core/init/InitTest.java | 124 + .../test/java/convex/core/lang/ACVMTest.java | 240 ++ .../test/java/convex/core/lang/AliasTest.java | 120 + .../java/convex/core/lang/CompilerTest.java | 595 +++ .../java/convex/core/lang/ContextTest.java | 201 + .../test/java/convex/core/lang/CoreTest.java | 3793 +++++++++++++++++ .../convex/core/lang/DataStructuresTest.java | 63 + .../test/java/convex/core/lang/DocsTest.java | 58 + .../java/convex/core/lang/GenTestCode.java | 63 + .../java/convex/core/lang/GenTestCore.java | 220 + .../test/java/convex/core/lang/GenTestRT.java | 44 + .../test/java/convex/core/lang/JuiceTest.java | 173 + .../java/convex/core/lang/NumericsTest.java | 307 ++ .../test/java/convex/core/lang/OpsTest.java | 302 ++ .../java/convex/core/lang/ParamTestCasts.java | 64 + .../java/convex/core/lang/ParamTestEvals.java | 122 + .../java/convex/core/lang/ParamTestJuice.java | 126 + .../core/lang/ParamTestRTSequences.java | 82 + .../test/java/convex/core/lang/RTTest.java | 88 + .../java/convex/core/lang/ReaderTest.java | 273 ++ .../java/convex/core/lang/SyntaxTest.java | 19 + .../test/java/convex/core/lang/TestState.java | 252 ++ .../convex/core/lang/reader/ANTLRTest.java | 144 + .../java/convex/core/util/EconomicsTest.java | 47 + .../convex/core/util/GenTestEconomics.java | 31 + .../src/test/java/convex/lib/AssetTest.java | 106 + .../test/java/convex/lib/FungibleTest.java | 244 ++ .../test/java/convex/lib/SimpleNFTTest.java | 69 + .../src/test/java/convex/lib/TrustTest.java | 237 + .../test/java/convex/store/EtchInitTest.java | 35 + .../test/java/convex/store/EtchStoreTest.java | 240 ++ .../java/convex/store/MemoryStoreTest.java | 114 + .../java/convex/store/ParamTestStores.java | 5 + .../src/test/java/convex/test/Assertions.java | 115 + .../src/test/java/convex/test/Samples.java | 282 ++ .../src/test/java/convex/test/Testing.java | 39 + .../convex/test/generators/AddressGen.java | 19 + .../convex/test/generators/AnyMapGen.java | 60 + .../java/convex/test/generators/BlobGen.java | 48 + .../convex/test/generators/CollectionGen.java | 38 + .../test/generators/DataStructureGen.java | 46 + .../java/convex/test/generators/FormGen.java | 72 + .../convex/test/generators/KeywordGen.java | 33 + .../java/convex/test/generators/ListGen.java | 26 + .../java/convex/test/generators/MapGen.java | 48 + .../convex/test/generators/NumericGen.java | 39 + .../convex/test/generators/PrimitiveGen.java | 46 + .../convex/test/generators/RecordGen.java | 36 + .../java/convex/test/generators/SetGen.java | 46 + .../convex/test/generators/StringGen.java | 32 + .../test/generators/TransactionGen.java | 39 + .../java/convex/test/generators/ValueGen.java | 59 + .../convex/test/generators/VectorGen.java | 70 + .../java/convex/util/BigIntegerParamTest.java | 46 + .../src/test/java/convex/util/BitsTest.java | 18 + .../test/java/convex/util/GenTestHuge.java | 64 + .../test/java/convex/util/GenTestUMath.java | 24 + .../src/test/java/convex/util/HugeTest.java | 46 + .../src/test/java/convex/util/TextTest.java | 28 + .../src/test/java/convex/util/UMathTest.java | 15 + .../src/test/java/convex/util/UtilsTest.java | 375 ++ .../src/test/java/etch/api/TestEtch.java | 73 + .../test/resources/contracts/box/test1.con | 33 + .../test/resources/contracts/deposit-box.con | 19 + .../test/resources/contracts/exceptional.con | 30 + .../src/test/resources/contracts/funding.con | 54 + .../src/test/resources/contracts/hello.con | 16 + .../contracts/nft/simple-nft-test.con | 21 + .../src/test/resources/contracts/token.con | 42 + .../src/test/resources/examples/adventure.cvx | 21 + .../test/resources/junit-platform.properties | 4 + .../src/test/resources/testsource/min.con | 8 + convex-gui/.gitignore | 5 + convex-gui/pom.xml | 103 + convex-gui/src/main/assembly/full.xml | 34 + convex-gui/src/main/assembly/testing.xml | 30 + .../java/convex/gui/client/ConvexClient.java | 116 + .../convex/gui/client/panels/HomePanel.java | 41 + .../gui/components/AccountChooserPanel.java | 102 + .../convex/gui/components/ActionPanel.java | 23 + .../gui/components/BaseListComponent.java | 16 + .../gui/components/BlockViewComponent.java | 82 + .../java/convex/gui/components/CodeLabel.java | 16 + .../gui/components/DefaultReceiveAction.java | 51 + .../convex/gui/components/DropdownMenu.java | 30 + .../java/convex/gui/components/Identicon.java | 42 + .../convex/gui/components/PeerComponent.java | 139 + .../java/convex/gui/components/PeerView.java | 92 + .../convex/gui/components/ScrollyList.java | 96 + .../java/convex/gui/components/Toast.java | 93 + .../gui/components/UnlockWalletDialog.java | 93 + .../gui/components/WalletComponent.java | 100 + .../convex/gui/components/WorldPanel.java | 54 + .../components/models/AccountsTableModel.java | 90 + .../components/models/OracleTableModel.java | 109 + .../gui/components/models/StateModel.java | 68 + .../java/convex/gui/etch/EtchExplorer.java | 112 + .../convex/gui/etch/panels/DatabasePanel.java | 98 + .../main/java/convex/gui/manager/PeerGUI.java | 317 ++ .../gui/manager/mainpanels/AboutPanel.java | 81 + .../gui/manager/mainpanels/AccountsPanel.java | 150 + .../gui/manager/mainpanels/ActorsPanel.java | 38 + .../gui/manager/mainpanels/DeployPanel.java | 44 + .../gui/manager/mainpanels/HomePanel.java | 36 + .../gui/manager/mainpanels/KeyGenPanel.java | 221 + .../mainpanels/MessageFormatPanel.java | 134 + .../manager/mainpanels/PeersListPanel.java | 157 + .../gui/manager/mainpanels/WalletPanel.java | 60 + .../mainpanels/actors/DeployPanel.java | 43 + .../mainpanels/actors/MarketComponent.java | 214 + .../mainpanels/actors/MarketsPanel.java | 53 + .../mainpanels/actors/OraclePanel.java | 193 + .../gui/manager/windows/BaseWindow.java | 32 + .../manager/windows/actor/ActorInfoPanel.java | 51 + .../windows/actor/ActorInvokePanel.java | 44 + .../manager/windows/actor/ActorWindow.java | 44 + .../gui/manager/windows/actor/ArgBox.java | 13 + .../gui/manager/windows/actor/ParamLabel.java | 20 + .../windows/actor/SmartOpComponent.java | 177 + .../gui/manager/windows/etch/EtchWindow.java | 47 + .../gui/manager/windows/peer/PeerWindow.java | 44 + .../gui/manager/windows/peer/REPLPanel.java | 296 ++ .../gui/manager/windows/peer/StressPanel.java | 249 ++ .../manager/windows/state/StateTreeNode.java | 79 + .../manager/windows/state/StateTreePanel.java | 129 + .../manager/windows/state/StateWindow.java | 30 + .../convex/gui/utils/RobinsonProjection.java | 112 + .../main/java/convex/gui/utils/Toolkit.java | 139 + .../src/main/java/convex/wallet/Wallet.java | 73 + .../src/main/resources/images/Convex.png | Bin 0 -> 10210 bytes convex-gui/src/main/resources/images/cog.png | Bin 0 -> 5007 bytes .../resources/images/ic_cake_black_36dp.png | Bin 0 -> 657 bytes .../images/ic_lock_open_black_36dp.png | Bin 0 -> 502 bytes .../images/ic_lock_outline_black_36dp.png | Bin 0 -> 504 bytes .../images/ic_priority_high_black_36dp.png | Bin 0 -> 185 bytes .../resources/images/ic_stars_black_36dp.png | Bin 0 -> 951 bytes .../main/resources/images/padlock-open.png | Bin 0 -> 1946 bytes .../src/main/resources/images/padlock.png | Bin 0 -> 1948 bytes .../src/main/resources/images/world.png | Bin 0 -> 468500 bytes .../src/test/java/convex/gui/GUITest.java | 25 + convex-peer/.gitignore | 5 + convex-peer/pom.xml | 59 + .../main/java/convex/api/Applications.java | 37 + .../src/main/java/convex/api/Convex.java | 898 ++++ .../src/main/java/convex/net/Connection.java | 770 ++++ .../java/convex/net/MemoryByteChannel.java | 70 + .../src/main/java/convex/net/Message.java | 111 + .../main/java/convex/net/MessageReceiver.java | 174 + .../main/java/convex/net/MessageSender.java | 81 + .../src/main/java/convex/net/MessageType.java | 143 + .../src/main/java/convex/net/NIOServer.java | 269 ++ .../main/java/convex/net/ResultConsumer.java | 191 + .../src/main/java/convex/peer/API.java | 205 + .../java/convex/peer/ChallengeRequest.java | 89 + .../java/convex/peer/ConnectionManager.java | 639 +++ .../main/java/convex/peer/IServerEvent.java | 12 + .../src/main/java/convex/peer/Server.java | 1207 ++++++ .../main/java/convex/peer/ServerEvent.java | 30 + .../java/convex/peer/ServerInformation.java | 88 + .../src/test/java/convex/api/ConvexTest.java | 103 + .../java/convex/api/TestApplications.java | 30 + .../java/convex/examples/AcquireState.java | 27 + .../test/java/convex/examples/ClientApp.java | 27 + .../java/convex/examples/Ed25519Sign.java | 54 + .../java/convex/examples/JoinTestNetwork.java | 51 + .../java/convex/examples/PeerCluster.java | 110 + .../test/java/convex/examples/SigSamples.java | 27 + .../test/java/convex/net/ConnectionTest.java | 88 + .../convex/net/MemoryByteChannelTest.java | 23 + .../java/convex/peer/MessageReceiverTest.java | 56 + .../test/java/convex/peer/MessageTest.java | 28 + .../test/java/convex/peer/RestoreTest.java | 96 + .../src/test/java/convex/peer/ServerTest.java | 289 ++ .../test/java/convex/peer/TestNetwork.java | 85 + convex.bat | 3 + docs/coding-principles.md | 74 + docs/wip/ethos.md | 23 + docs/wip/governance.md | 19 + docs/wip/misc-faq.md | 16 + pom.xml | 207 + 552 files changed, 84566 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/deploy_docs.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 .mvn/maven.config create mode 100644 BUILD.md create mode 100644 CHANGELOG.md create mode 100644 Dockerfile create mode 100644 LICENSE.md create mode 100644 README.md create mode 100755 convex create mode 100644 convex-benchmarks/.gitignore create mode 100644 convex-benchmarks/README.md create mode 100644 convex-benchmarks/pom.xml create mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/Benchmarks.java create mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/BigBlockBenchmark.java create mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/CVMBenchmark.java create mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/EtchBenchmark.java create mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/EvalBenchmark.java create mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/HashBenchmark.java create mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/LatencyBenchmark.java create mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/ListDataBenchmark.java create mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/MapBenchmark.java create mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/OpBenchmark.java create mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/SignatureBenchmark.java create mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/ThreadCoordinationBenchmark.java create mode 100644 convex-cli/.gitignore create mode 100644 convex-cli/pom.xml create mode 100644 convex-cli/src/docs/asciidoc/images/convex_logo.svg create mode 100644 convex-cli/src/docs/asciidoc/index.adoc create mode 100644 convex-cli/src/main/java/convex/cli/Account.java create mode 100644 convex-cli/src/main/java/convex/cli/AccountBalance.java create mode 100644 convex-cli/src/main/java/convex/cli/AccountCreate.java create mode 100644 convex-cli/src/main/java/convex/cli/AccountFund.java create mode 100644 convex-cli/src/main/java/convex/cli/AccountInformation.java create mode 100644 convex-cli/src/main/java/convex/cli/Constants.java create mode 100644 convex-cli/src/main/java/convex/cli/Helpers.java create mode 100644 convex-cli/src/main/java/convex/cli/Key.java create mode 100644 convex-cli/src/main/java/convex/cli/KeyExport.java create mode 100644 convex-cli/src/main/java/convex/cli/KeyGenerate.java create mode 100644 convex-cli/src/main/java/convex/cli/KeyImport.java create mode 100644 convex-cli/src/main/java/convex/cli/KeyList.java create mode 100644 convex-cli/src/main/java/convex/cli/Local.java create mode 100644 convex-cli/src/main/java/convex/cli/LocalGUI.java create mode 100644 convex-cli/src/main/java/convex/cli/LocalStart.java create mode 100644 convex-cli/src/main/java/convex/cli/Main.java create mode 100644 convex-cli/src/main/java/convex/cli/Peer.java create mode 100644 convex-cli/src/main/java/convex/cli/PeerCreate.java create mode 100644 convex-cli/src/main/java/convex/cli/PeerStart.java create mode 100644 convex-cli/src/main/java/convex/cli/Query.java create mode 100644 convex-cli/src/main/java/convex/cli/Status.java create mode 100644 convex-cli/src/main/java/convex/cli/Transaction.java create mode 100644 convex-cli/src/main/java/convex/cli/output/Output.java create mode 100644 convex-cli/src/main/java/convex/cli/output/OutputField.java create mode 100644 convex-cli/src/main/java/convex/cli/peer/PeerManager.java create mode 100644 convex-cli/src/main/java/convex/cli/peer/Session.java create mode 100644 convex-cli/src/main/java/convex/cli/peer/SessionItem.java create mode 100644 convex-cli/src/test/java/convex/cli/CLICommandKeyExportTest.java create mode 100644 convex-cli/src/test/java/convex/cli/CLICommandKeyImportTest.java create mode 100644 convex-cli/src/test/java/convex/cli/CLICommandKeyTest.java create mode 100644 convex-cli/src/test/java/convex/cli/CLIHelpTest.java create mode 100644 convex-cli/src/test/java/convex/cli/CLIMainTest.java create mode 100644 convex-cli/src/test/java/convex/cli/CommandLineTester.java create mode 100644 convex-cli/src/test/java/convex/cli/Helper.java create mode 100644 convex-cli/src/test/java/convex/cli/output/OutputTest.java create mode 100644 convex-cli/src/test/java/convex/cli/peer/SessionTest.java create mode 100644 convex-core/.gitignore create mode 100644 convex-core/pom.xml create mode 100644 convex-core/src/main/antlr4/convex/core/lang/reader/antlr/Convex.g4 create mode 100644 convex-core/src/main/assembly/full.xml create mode 100644 convex-core/src/main/assembly/testing.xml create mode 100644 convex-core/src/main/cvx/asset/box.cvx create mode 100644 convex-core/src/main/cvx/asset/box/actor.cvx create mode 100644 convex-core/src/main/cvx/asset/nft/simple.cvx create mode 100644 convex-core/src/main/cvx/asset/nft/tokens.cvx create mode 100644 convex-core/src/main/cvx/convex/asset.cvx create mode 100644 convex-core/src/main/cvx/convex/core.cvx create mode 100644 convex-core/src/main/cvx/convex/core/metadata.cvx create mode 100644 convex-core/src/main/cvx/convex/fungible.cvx create mode 100644 convex-core/src/main/cvx/convex/play.cvx create mode 100644 convex-core/src/main/cvx/convex/registry.cvx create mode 100644 convex-core/src/main/cvx/convex/trust.cvx create mode 100644 convex-core/src/main/cvx/convex/trusted-oracle.cvx create mode 100644 convex-core/src/main/cvx/convex/trusted-oracle/actor.cvx create mode 100644 convex-core/src/main/cvx/lab/convex/xform.cvx create mode 100644 convex-core/src/main/cvx/lab/messenger.cvx create mode 100644 convex-core/src/main/cvx/lab/prediction-market.cvx create mode 100644 convex-core/src/main/cvx/lab/secured-loan.con create mode 100644 convex-core/src/main/cvx/torus/currencies.cvx create mode 100644 convex-core/src/main/cvx/torus/exchange.cvx create mode 100644 convex-core/src/main/java/convex/core/Belief.java create mode 100644 convex-core/src/main/java/convex/core/Block.java create mode 100644 convex-core/src/main/java/convex/core/BlockResult.java create mode 100644 convex-core/src/main/java/convex/core/Coin.java create mode 100644 convex-core/src/main/java/convex/core/Constants.java create mode 100644 convex-core/src/main/java/convex/core/ErrorCodes.java create mode 100644 convex-core/src/main/java/convex/core/MergeContext.java create mode 100644 convex-core/src/main/java/convex/core/Order.java create mode 100644 convex-core/src/main/java/convex/core/Peer.java create mode 100644 convex-core/src/main/java/convex/core/Result.java create mode 100644 convex-core/src/main/java/convex/core/State.java create mode 100644 convex-core/src/main/java/convex/core/crypto/AKeyPair.java create mode 100644 convex-core/src/main/java/convex/core/crypto/ASignature.java create mode 100644 convex-core/src/main/java/convex/core/crypto/Ed25519KeyPair.java create mode 100644 convex-core/src/main/java/convex/core/crypto/Ed25519Signature.java create mode 100644 convex-core/src/main/java/convex/core/crypto/Encoding.java create mode 100644 convex-core/src/main/java/convex/core/crypto/Hashing.java create mode 100644 convex-core/src/main/java/convex/core/crypto/InsecureRandom.java create mode 100644 convex-core/src/main/java/convex/core/crypto/Mnemonic.java create mode 100644 convex-core/src/main/java/convex/core/crypto/PBE.java create mode 100644 convex-core/src/main/java/convex/core/crypto/PEMTools.java create mode 100644 convex-core/src/main/java/convex/core/crypto/PFXTools.java create mode 100644 convex-core/src/main/java/convex/core/crypto/Providers.java create mode 100644 convex-core/src/main/java/convex/core/crypto/Symmetric.java create mode 100644 convex-core/src/main/java/convex/core/crypto/Wallet.java create mode 100644 convex-core/src/main/java/convex/core/crypto/WalletEntry.java create mode 100644 convex-core/src/main/java/convex/core/data/AArrayBlob.java create mode 100644 convex-core/src/main/java/convex/core/data/ABlob.java create mode 100644 convex-core/src/main/java/convex/core/data/ABlobMap.java create mode 100644 convex-core/src/main/java/convex/core/data/ACell.java create mode 100644 convex-core/src/main/java/convex/core/data/ACollection.java create mode 100644 convex-core/src/main/java/convex/core/data/ACountable.java create mode 100644 convex-core/src/main/java/convex/core/data/ADataStructure.java create mode 100644 convex-core/src/main/java/convex/core/data/AHashMap.java create mode 100644 convex-core/src/main/java/convex/core/data/AHashSet.java create mode 100644 convex-core/src/main/java/convex/core/data/AList.java create mode 100644 convex-core/src/main/java/convex/core/data/ALongBlob.java create mode 100644 convex-core/src/main/java/convex/core/data/AMap.java create mode 100644 convex-core/src/main/java/convex/core/data/AMapEntry.java create mode 100644 convex-core/src/main/java/convex/core/data/ANumericBlob.java create mode 100644 convex-core/src/main/java/convex/core/data/AObject.java create mode 100644 convex-core/src/main/java/convex/core/data/ARecord.java create mode 100644 convex-core/src/main/java/convex/core/data/ARecordGeneric.java create mode 100644 convex-core/src/main/java/convex/core/data/ASequence.java create mode 100644 convex-core/src/main/java/convex/core/data/ASet.java create mode 100644 convex-core/src/main/java/convex/core/data/AString.java create mode 100644 convex-core/src/main/java/convex/core/data/ASymbolic.java create mode 100644 convex-core/src/main/java/convex/core/data/AVector.java create mode 100644 convex-core/src/main/java/convex/core/data/AccountKey.java create mode 100644 convex-core/src/main/java/convex/core/data/AccountStatus.java create mode 100644 convex-core/src/main/java/convex/core/data/Address.java create mode 100644 convex-core/src/main/java/convex/core/data/Blob.java create mode 100644 convex-core/src/main/java/convex/core/data/BlobMap.java create mode 100644 convex-core/src/main/java/convex/core/data/BlobMaps.java create mode 100644 convex-core/src/main/java/convex/core/data/BlobTree.java create mode 100644 convex-core/src/main/java/convex/core/data/Blobs.java create mode 100644 convex-core/src/main/java/convex/core/data/Format.java create mode 100644 convex-core/src/main/java/convex/core/data/Hash.java create mode 100644 convex-core/src/main/java/convex/core/data/IAssociative.java create mode 100644 convex-core/src/main/java/convex/core/data/INumeric.java create mode 100644 convex-core/src/main/java/convex/core/data/IRefFunction.java create mode 100644 convex-core/src/main/java/convex/core/data/IValidated.java create mode 100644 convex-core/src/main/java/convex/core/data/IWriteable.java create mode 100644 convex-core/src/main/java/convex/core/data/Keyword.java create mode 100644 convex-core/src/main/java/convex/core/data/Keywords.java create mode 100644 convex-core/src/main/java/convex/core/data/List.java create mode 100644 convex-core/src/main/java/convex/core/data/Lists.java create mode 100644 convex-core/src/main/java/convex/core/data/LongBlob.java create mode 100644 convex-core/src/main/java/convex/core/data/MapEntry.java create mode 100644 convex-core/src/main/java/convex/core/data/MapLeaf.java create mode 100644 convex-core/src/main/java/convex/core/data/MapTree.java create mode 100644 convex-core/src/main/java/convex/core/data/Maps.java create mode 100644 convex-core/src/main/java/convex/core/data/PeerStatus.java create mode 100644 convex-core/src/main/java/convex/core/data/Ref.java create mode 100644 convex-core/src/main/java/convex/core/data/RefDirect.java create mode 100644 convex-core/src/main/java/convex/core/data/RefSoft.java create mode 100644 convex-core/src/main/java/convex/core/data/SetLeaf.java create mode 100644 convex-core/src/main/java/convex/core/data/SetTree.java create mode 100644 convex-core/src/main/java/convex/core/data/Sets.java create mode 100644 convex-core/src/main/java/convex/core/data/SignedData.java create mode 100644 convex-core/src/main/java/convex/core/data/StringShort.java create mode 100644 convex-core/src/main/java/convex/core/data/StringSlice.java create mode 100644 convex-core/src/main/java/convex/core/data/StringTree.java create mode 100644 convex-core/src/main/java/convex/core/data/Strings.java create mode 100644 convex-core/src/main/java/convex/core/data/Symbol.java create mode 100644 convex-core/src/main/java/convex/core/data/Syntax.java create mode 100644 convex-core/src/main/java/convex/core/data/Tag.java create mode 100644 convex-core/src/main/java/convex/core/data/VectorArray.java create mode 100644 convex-core/src/main/java/convex/core/data/VectorLeaf.java create mode 100644 convex-core/src/main/java/convex/core/data/VectorTree.java create mode 100644 convex-core/src/main/java/convex/core/data/Vectors.java create mode 100644 convex-core/src/main/java/convex/core/data/package-info.java create mode 100644 convex-core/src/main/java/convex/core/data/prim/APrimitive.java create mode 100644 convex-core/src/main/java/convex/core/data/prim/CVMBool.java create mode 100644 convex-core/src/main/java/convex/core/data/prim/CVMByte.java create mode 100644 convex-core/src/main/java/convex/core/data/prim/CVMChar.java create mode 100644 convex-core/src/main/java/convex/core/data/prim/CVMDouble.java create mode 100644 convex-core/src/main/java/convex/core/data/prim/CVMLong.java create mode 100644 convex-core/src/main/java/convex/core/data/type/ANumericType.java create mode 100644 convex-core/src/main/java/convex/core/data/type/AStandardType.java create mode 100644 convex-core/src/main/java/convex/core/data/type/AType.java create mode 100644 convex-core/src/main/java/convex/core/data/type/AddressType.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Any.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Blob.java create mode 100644 convex-core/src/main/java/convex/core/data/type/BlobMapType.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Boolean.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Byte.java create mode 100644 convex-core/src/main/java/convex/core/data/type/CharacterType.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Collection.java create mode 100644 convex-core/src/main/java/convex/core/data/type/DataStructure.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Double.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Function.java create mode 100644 convex-core/src/main/java/convex/core/data/type/KeywordType.java create mode 100644 convex-core/src/main/java/convex/core/data/type/List.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Long.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Map.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Nil.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Number.java create mode 100644 convex-core/src/main/java/convex/core/data/type/OpCode.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Record.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Sequence.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Set.java create mode 100644 convex-core/src/main/java/convex/core/data/type/StringType.java create mode 100644 convex-core/src/main/java/convex/core/data/type/SymbolType.java create mode 100644 convex-core/src/main/java/convex/core/data/type/SyntaxType.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Transaction.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Types.java create mode 100644 convex-core/src/main/java/convex/core/data/type/Vector.java create mode 100644 convex-core/src/main/java/convex/core/exceptions/BadFormatException.java create mode 100644 convex-core/src/main/java/convex/core/exceptions/BadSignatureException.java create mode 100644 convex-core/src/main/java/convex/core/exceptions/BaseException.java create mode 100644 convex-core/src/main/java/convex/core/exceptions/InvalidDataException.java create mode 100644 convex-core/src/main/java/convex/core/exceptions/MissingDataException.java create mode 100644 convex-core/src/main/java/convex/core/exceptions/ParseException.java create mode 100644 convex-core/src/main/java/convex/core/exceptions/TODOException.java create mode 100644 convex-core/src/main/java/convex/core/exceptions/ValidationException.java create mode 100644 convex-core/src/main/java/convex/core/init/AInitConfig.java create mode 100644 convex-core/src/main/java/convex/core/init/Init.java create mode 100644 convex-core/src/main/java/convex/core/lang/AFn.java create mode 100644 convex-core/src/main/java/convex/core/lang/AOp.java create mode 100644 convex-core/src/main/java/convex/core/lang/Compiler.java create mode 100644 convex-core/src/main/java/convex/core/lang/Context.java create mode 100644 convex-core/src/main/java/convex/core/lang/Core.java create mode 100644 convex-core/src/main/java/convex/core/lang/IFn.java create mode 100644 convex-core/src/main/java/convex/core/lang/Juice.java create mode 100644 convex-core/src/main/java/convex/core/lang/Ops.java create mode 100644 convex-core/src/main/java/convex/core/lang/RT.java create mode 100644 convex-core/src/main/java/convex/core/lang/Reader.java create mode 100644 convex-core/src/main/java/convex/core/lang/Symbols.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/AClosure.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/ADataFn.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/AExceptional.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/AReturn.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/ATrampoline.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/CoreFn.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/CorePred.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/ErrorValue.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/Fn.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/HaltValue.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/ICoreDef.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/KeywordFn.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/MapFn.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/MultiFn.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/RecordFormat.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/RecurValue.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/Reduced.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/ReturnValue.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/RollbackValue.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/SeqFn.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/SetFn.java create mode 100644 convex-core/src/main/java/convex/core/lang/impl/TailcallValue.java create mode 100644 convex-core/src/main/java/convex/core/lang/ops/AMultiOp.java create mode 100644 convex-core/src/main/java/convex/core/lang/ops/Cond.java create mode 100644 convex-core/src/main/java/convex/core/lang/ops/Constant.java create mode 100644 convex-core/src/main/java/convex/core/lang/ops/Def.java create mode 100644 convex-core/src/main/java/convex/core/lang/ops/Do.java create mode 100644 convex-core/src/main/java/convex/core/lang/ops/Invoke.java create mode 100644 convex-core/src/main/java/convex/core/lang/ops/Lambda.java create mode 100644 convex-core/src/main/java/convex/core/lang/ops/Let.java create mode 100644 convex-core/src/main/java/convex/core/lang/ops/Local.java create mode 100644 convex-core/src/main/java/convex/core/lang/ops/Lookup.java create mode 100644 convex-core/src/main/java/convex/core/lang/ops/Query.java create mode 100644 convex-core/src/main/java/convex/core/lang/ops/Set.java create mode 100644 convex-core/src/main/java/convex/core/lang/ops/Special.java create mode 100644 convex-core/src/main/java/convex/core/lang/reader/AntlrReader.java create mode 100644 convex-core/src/main/java/convex/core/lang/reader/ReaderUtils.java create mode 100644 convex-core/src/main/java/convex/core/store/AStore.java create mode 100644 convex-core/src/main/java/convex/core/store/BlobCache.java create mode 100644 convex-core/src/main/java/convex/core/store/MemoryStore.java create mode 100644 convex-core/src/main/java/convex/core/store/Stores.java create mode 100644 convex-core/src/main/java/convex/core/transactions/ATransaction.java create mode 100644 convex-core/src/main/java/convex/core/transactions/Call.java create mode 100644 convex-core/src/main/java/convex/core/transactions/Invoke.java create mode 100644 convex-core/src/main/java/convex/core/transactions/Transfer.java create mode 100644 convex-core/src/main/java/convex/core/util/Bits.java create mode 100644 convex-core/src/main/java/convex/core/util/Counters.java create mode 100644 convex-core/src/main/java/convex/core/util/Economics.java create mode 100644 convex-core/src/main/java/convex/core/util/Errors.java create mode 100644 convex-core/src/main/java/convex/core/util/Huge.java create mode 100644 convex-core/src/main/java/convex/core/util/MergeFunction.java create mode 100644 convex-core/src/main/java/convex/core/util/Shutdown.java create mode 100644 convex-core/src/main/java/convex/core/util/Text.java create mode 100644 convex-core/src/main/java/convex/core/util/UMath.java create mode 100644 convex-core/src/main/java/convex/core/util/Utils.java create mode 100644 convex-core/src/main/java/etch/Etch.java create mode 100644 convex-core/src/main/java/etch/EtchStore.java create mode 100644 convex-core/src/test/cvx/test/asset/box.cvx create mode 100644 convex-core/src/test/cvx/test/asset/nft/simple.cvx create mode 100644 convex-core/src/test/cvx/test/asset/nft/tokens.cvx create mode 100644 convex-core/src/test/cvx/test/convex/asset.cvx create mode 100644 convex-core/src/test/cvx/test/convex/asset/quantity/set-long.cvx create mode 100644 convex-core/src/test/cvx/test/convex/fungible.cvx create mode 100644 convex-core/src/test/cvx/test/convex/fungible/generic.cvx create mode 100644 convex-core/src/test/cvx/test/convex/play.cvx create mode 100644 convex-core/src/test/cvx/test/convex/registry.cvx create mode 100644 convex-core/src/test/cvx/test/convex/trust.cvx create mode 100644 convex-core/src/test/cvx/test/convex/trusted-oracle.cvx create mode 100644 convex-core/src/test/cvx/test/torus/exchange.cvx create mode 100644 convex-core/src/test/java/convex/actors/ActorsTest.java create mode 100644 convex-core/src/test/java/convex/actors/PredictionMarketTest.java create mode 100644 convex-core/src/test/java/convex/actors/RegistryTest.java create mode 100644 convex-core/src/test/java/convex/actors/TorusTest.java create mode 100644 convex-core/src/test/java/convex/comms/GenTestFormat.java create mode 100644 convex-core/src/test/java/convex/comms/VLCEncodingTest.java create mode 100644 convex-core/src/test/java/convex/comms/VLCParamTest.java create mode 100644 convex-core/src/test/java/convex/core/BeliefMergeTest.java create mode 100644 convex-core/src/test/java/convex/core/BeliefVotingTest.java create mode 100644 convex-core/src/test/java/convex/core/MessageSizeTest.java create mode 100644 convex-core/src/test/java/convex/core/PeerTest.java create mode 100644 convex-core/src/test/java/convex/core/ResultTest.java create mode 100644 convex-core/src/test/java/convex/core/StakingTest.java create mode 100644 convex-core/src/test/java/convex/core/StateTest.java create mode 100644 convex-core/src/test/java/convex/core/StateTransitionsTest.java create mode 100644 convex-core/src/test/java/convex/core/TransactionTest.java create mode 100644 convex-core/src/test/java/convex/core/crypto/AccountKeyTest.java create mode 100644 convex-core/src/test/java/convex/core/crypto/Ed25519Test.java create mode 100644 convex-core/src/test/java/convex/core/crypto/HashTest.java create mode 100644 convex-core/src/test/java/convex/core/crypto/MnemonicTest.java create mode 100644 convex-core/src/test/java/convex/core/crypto/PBETest.java create mode 100644 convex-core/src/test/java/convex/core/crypto/PEMToolsTest.java create mode 100644 convex-core/src/test/java/convex/core/crypto/PFXTest.java create mode 100644 convex-core/src/test/java/convex/core/crypto/ParamTestHash.java create mode 100644 convex-core/src/test/java/convex/core/crypto/SignKeyPairTest.java create mode 100644 convex-core/src/test/java/convex/core/crypto/SymmetricTest.java create mode 100644 convex-core/src/test/java/convex/core/crypto/WalletTest.java create mode 100644 convex-core/src/test/java/convex/core/data/AccountKeyTest.java create mode 100644 convex-core/src/test/java/convex/core/data/AddressTest.java create mode 100644 convex-core/src/test/java/convex/core/data/BlobMapsTest.java create mode 100644 convex-core/src/test/java/convex/core/data/BlobsTest.java create mode 100644 convex-core/src/test/java/convex/core/data/BlocksTest.java create mode 100644 convex-core/src/test/java/convex/core/data/CollectionsTest.java create mode 100644 convex-core/src/test/java/convex/core/data/EncodingTest.java create mode 100644 convex-core/src/test/java/convex/core/data/FuzzTestFormat.java create mode 100644 convex-core/src/test/java/convex/core/data/GenTestAnyValue.java create mode 100644 convex-core/src/test/java/convex/core/data/GenTestBlobs.java create mode 100644 convex-core/src/test/java/convex/core/data/GenTestDataStructures.java create mode 100644 convex-core/src/test/java/convex/core/data/GenTestMap.java create mode 100644 convex-core/src/test/java/convex/core/data/GenTestMessages.java create mode 100644 convex-core/src/test/java/convex/core/data/GenTestStrings.java create mode 100644 convex-core/src/test/java/convex/core/data/GenTestVectors.java create mode 100644 convex-core/src/test/java/convex/core/data/KeywordTest.java create mode 100644 convex-core/src/test/java/convex/core/data/ListsTest.java create mode 100644 convex-core/src/test/java/convex/core/data/MapsTest.java create mode 100644 convex-core/src/test/java/convex/core/data/ObjectsTest.java create mode 100644 convex-core/src/test/java/convex/core/data/ParamTestBlobs.java create mode 100644 convex-core/src/test/java/convex/core/data/ParamTestOps.java create mode 100644 convex-core/src/test/java/convex/core/data/ParamTestRefs.java create mode 100644 convex-core/src/test/java/convex/core/data/ParamTestValues.java create mode 100644 convex-core/src/test/java/convex/core/data/ParamTestVector.java create mode 100644 convex-core/src/test/java/convex/core/data/RecordTest.java create mode 100644 convex-core/src/test/java/convex/core/data/RefTest.java create mode 100644 convex-core/src/test/java/convex/core/data/SetsTest.java create mode 100644 convex-core/src/test/java/convex/core/data/SignedDataTest.java create mode 100644 convex-core/src/test/java/convex/core/data/StreamsTest.java create mode 100644 convex-core/src/test/java/convex/core/data/StringsTest.java create mode 100644 convex-core/src/test/java/convex/core/data/SymbolTest.java create mode 100644 convex-core/src/test/java/convex/core/data/SyntaxTest.java create mode 100644 convex-core/src/test/java/convex/core/data/TreeVectorTest.java create mode 100644 convex-core/src/test/java/convex/core/data/VectorsTest.java create mode 100644 convex-core/src/test/java/convex/core/data/prim/ByteTest.java create mode 100644 convex-core/src/test/java/convex/core/data/prim/LongTest.java create mode 100644 convex-core/src/test/java/convex/core/data/type/TypesTest.java create mode 100644 convex-core/src/test/java/convex/core/init/InitTest.java create mode 100644 convex-core/src/test/java/convex/core/lang/ACVMTest.java create mode 100644 convex-core/src/test/java/convex/core/lang/AliasTest.java create mode 100644 convex-core/src/test/java/convex/core/lang/CompilerTest.java create mode 100644 convex-core/src/test/java/convex/core/lang/ContextTest.java create mode 100644 convex-core/src/test/java/convex/core/lang/CoreTest.java create mode 100644 convex-core/src/test/java/convex/core/lang/DataStructuresTest.java create mode 100644 convex-core/src/test/java/convex/core/lang/DocsTest.java create mode 100644 convex-core/src/test/java/convex/core/lang/GenTestCode.java create mode 100644 convex-core/src/test/java/convex/core/lang/GenTestCore.java create mode 100644 convex-core/src/test/java/convex/core/lang/GenTestRT.java create mode 100644 convex-core/src/test/java/convex/core/lang/JuiceTest.java create mode 100644 convex-core/src/test/java/convex/core/lang/NumericsTest.java create mode 100644 convex-core/src/test/java/convex/core/lang/OpsTest.java create mode 100644 convex-core/src/test/java/convex/core/lang/ParamTestCasts.java create mode 100644 convex-core/src/test/java/convex/core/lang/ParamTestEvals.java create mode 100644 convex-core/src/test/java/convex/core/lang/ParamTestJuice.java create mode 100644 convex-core/src/test/java/convex/core/lang/ParamTestRTSequences.java create mode 100644 convex-core/src/test/java/convex/core/lang/RTTest.java create mode 100644 convex-core/src/test/java/convex/core/lang/ReaderTest.java create mode 100644 convex-core/src/test/java/convex/core/lang/SyntaxTest.java create mode 100644 convex-core/src/test/java/convex/core/lang/TestState.java create mode 100644 convex-core/src/test/java/convex/core/lang/reader/ANTLRTest.java create mode 100644 convex-core/src/test/java/convex/core/util/EconomicsTest.java create mode 100644 convex-core/src/test/java/convex/core/util/GenTestEconomics.java create mode 100644 convex-core/src/test/java/convex/lib/AssetTest.java create mode 100644 convex-core/src/test/java/convex/lib/FungibleTest.java create mode 100644 convex-core/src/test/java/convex/lib/SimpleNFTTest.java create mode 100644 convex-core/src/test/java/convex/lib/TrustTest.java create mode 100644 convex-core/src/test/java/convex/store/EtchInitTest.java create mode 100644 convex-core/src/test/java/convex/store/EtchStoreTest.java create mode 100644 convex-core/src/test/java/convex/store/MemoryStoreTest.java create mode 100644 convex-core/src/test/java/convex/store/ParamTestStores.java create mode 100644 convex-core/src/test/java/convex/test/Assertions.java create mode 100644 convex-core/src/test/java/convex/test/Samples.java create mode 100644 convex-core/src/test/java/convex/test/Testing.java create mode 100644 convex-core/src/test/java/convex/test/generators/AddressGen.java create mode 100644 convex-core/src/test/java/convex/test/generators/AnyMapGen.java create mode 100644 convex-core/src/test/java/convex/test/generators/BlobGen.java create mode 100644 convex-core/src/test/java/convex/test/generators/CollectionGen.java create mode 100644 convex-core/src/test/java/convex/test/generators/DataStructureGen.java create mode 100644 convex-core/src/test/java/convex/test/generators/FormGen.java create mode 100644 convex-core/src/test/java/convex/test/generators/KeywordGen.java create mode 100644 convex-core/src/test/java/convex/test/generators/ListGen.java create mode 100644 convex-core/src/test/java/convex/test/generators/MapGen.java create mode 100644 convex-core/src/test/java/convex/test/generators/NumericGen.java create mode 100644 convex-core/src/test/java/convex/test/generators/PrimitiveGen.java create mode 100644 convex-core/src/test/java/convex/test/generators/RecordGen.java create mode 100644 convex-core/src/test/java/convex/test/generators/SetGen.java create mode 100644 convex-core/src/test/java/convex/test/generators/StringGen.java create mode 100644 convex-core/src/test/java/convex/test/generators/TransactionGen.java create mode 100644 convex-core/src/test/java/convex/test/generators/ValueGen.java create mode 100644 convex-core/src/test/java/convex/test/generators/VectorGen.java create mode 100644 convex-core/src/test/java/convex/util/BigIntegerParamTest.java create mode 100644 convex-core/src/test/java/convex/util/BitsTest.java create mode 100644 convex-core/src/test/java/convex/util/GenTestHuge.java create mode 100644 convex-core/src/test/java/convex/util/GenTestUMath.java create mode 100644 convex-core/src/test/java/convex/util/HugeTest.java create mode 100644 convex-core/src/test/java/convex/util/TextTest.java create mode 100644 convex-core/src/test/java/convex/util/UMathTest.java create mode 100644 convex-core/src/test/java/convex/util/UtilsTest.java create mode 100644 convex-core/src/test/java/etch/api/TestEtch.java create mode 100644 convex-core/src/test/resources/contracts/box/test1.con create mode 100644 convex-core/src/test/resources/contracts/deposit-box.con create mode 100644 convex-core/src/test/resources/contracts/exceptional.con create mode 100644 convex-core/src/test/resources/contracts/funding.con create mode 100644 convex-core/src/test/resources/contracts/hello.con create mode 100644 convex-core/src/test/resources/contracts/nft/simple-nft-test.con create mode 100644 convex-core/src/test/resources/contracts/token.con create mode 100644 convex-core/src/test/resources/examples/adventure.cvx create mode 100644 convex-core/src/test/resources/junit-platform.properties create mode 100644 convex-core/src/test/resources/testsource/min.con create mode 100644 convex-gui/.gitignore create mode 100644 convex-gui/pom.xml create mode 100644 convex-gui/src/main/assembly/full.xml create mode 100644 convex-gui/src/main/assembly/testing.xml create mode 100644 convex-gui/src/main/java/convex/gui/client/ConvexClient.java create mode 100644 convex-gui/src/main/java/convex/gui/client/panels/HomePanel.java create mode 100644 convex-gui/src/main/java/convex/gui/components/AccountChooserPanel.java create mode 100644 convex-gui/src/main/java/convex/gui/components/ActionPanel.java create mode 100644 convex-gui/src/main/java/convex/gui/components/BaseListComponent.java create mode 100644 convex-gui/src/main/java/convex/gui/components/BlockViewComponent.java create mode 100644 convex-gui/src/main/java/convex/gui/components/CodeLabel.java create mode 100644 convex-gui/src/main/java/convex/gui/components/DefaultReceiveAction.java create mode 100644 convex-gui/src/main/java/convex/gui/components/DropdownMenu.java create mode 100644 convex-gui/src/main/java/convex/gui/components/Identicon.java create mode 100644 convex-gui/src/main/java/convex/gui/components/PeerComponent.java create mode 100644 convex-gui/src/main/java/convex/gui/components/PeerView.java create mode 100644 convex-gui/src/main/java/convex/gui/components/ScrollyList.java create mode 100644 convex-gui/src/main/java/convex/gui/components/Toast.java create mode 100644 convex-gui/src/main/java/convex/gui/components/UnlockWalletDialog.java create mode 100644 convex-gui/src/main/java/convex/gui/components/WalletComponent.java create mode 100644 convex-gui/src/main/java/convex/gui/components/WorldPanel.java create mode 100644 convex-gui/src/main/java/convex/gui/components/models/AccountsTableModel.java create mode 100644 convex-gui/src/main/java/convex/gui/components/models/OracleTableModel.java create mode 100644 convex-gui/src/main/java/convex/gui/components/models/StateModel.java create mode 100644 convex-gui/src/main/java/convex/gui/etch/EtchExplorer.java create mode 100644 convex-gui/src/main/java/convex/gui/etch/panels/DatabasePanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/PeerGUI.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/AboutPanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/AccountsPanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/ActorsPanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/DeployPanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/HomePanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/KeyGenPanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/MessageFormatPanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/PeersListPanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/WalletPanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/DeployPanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/MarketComponent.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/MarketsPanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/OraclePanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/BaseWindow.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorInfoPanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorInvokePanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorWindow.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/actor/ArgBox.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/actor/ParamLabel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/actor/SmartOpComponent.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/etch/EtchWindow.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/peer/PeerWindow.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/peer/REPLPanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/peer/StressPanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/state/StateTreeNode.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/state/StateTreePanel.java create mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/state/StateWindow.java create mode 100644 convex-gui/src/main/java/convex/gui/utils/RobinsonProjection.java create mode 100644 convex-gui/src/main/java/convex/gui/utils/Toolkit.java create mode 100644 convex-gui/src/main/java/convex/wallet/Wallet.java create mode 100644 convex-gui/src/main/resources/images/Convex.png create mode 100644 convex-gui/src/main/resources/images/cog.png create mode 100644 convex-gui/src/main/resources/images/ic_cake_black_36dp.png create mode 100644 convex-gui/src/main/resources/images/ic_lock_open_black_36dp.png create mode 100644 convex-gui/src/main/resources/images/ic_lock_outline_black_36dp.png create mode 100644 convex-gui/src/main/resources/images/ic_priority_high_black_36dp.png create mode 100644 convex-gui/src/main/resources/images/ic_stars_black_36dp.png create mode 100644 convex-gui/src/main/resources/images/padlock-open.png create mode 100644 convex-gui/src/main/resources/images/padlock.png create mode 100644 convex-gui/src/main/resources/images/world.png create mode 100644 convex-gui/src/test/java/convex/gui/GUITest.java create mode 100644 convex-peer/.gitignore create mode 100644 convex-peer/pom.xml create mode 100644 convex-peer/src/main/java/convex/api/Applications.java create mode 100644 convex-peer/src/main/java/convex/api/Convex.java create mode 100644 convex-peer/src/main/java/convex/net/Connection.java create mode 100644 convex-peer/src/main/java/convex/net/MemoryByteChannel.java create mode 100644 convex-peer/src/main/java/convex/net/Message.java create mode 100644 convex-peer/src/main/java/convex/net/MessageReceiver.java create mode 100644 convex-peer/src/main/java/convex/net/MessageSender.java create mode 100644 convex-peer/src/main/java/convex/net/MessageType.java create mode 100644 convex-peer/src/main/java/convex/net/NIOServer.java create mode 100644 convex-peer/src/main/java/convex/net/ResultConsumer.java create mode 100644 convex-peer/src/main/java/convex/peer/API.java create mode 100644 convex-peer/src/main/java/convex/peer/ChallengeRequest.java create mode 100644 convex-peer/src/main/java/convex/peer/ConnectionManager.java create mode 100644 convex-peer/src/main/java/convex/peer/IServerEvent.java create mode 100644 convex-peer/src/main/java/convex/peer/Server.java create mode 100644 convex-peer/src/main/java/convex/peer/ServerEvent.java create mode 100644 convex-peer/src/main/java/convex/peer/ServerInformation.java create mode 100644 convex-peer/src/test/java/convex/api/ConvexTest.java create mode 100644 convex-peer/src/test/java/convex/api/TestApplications.java create mode 100644 convex-peer/src/test/java/convex/examples/AcquireState.java create mode 100644 convex-peer/src/test/java/convex/examples/ClientApp.java create mode 100644 convex-peer/src/test/java/convex/examples/Ed25519Sign.java create mode 100644 convex-peer/src/test/java/convex/examples/JoinTestNetwork.java create mode 100644 convex-peer/src/test/java/convex/examples/PeerCluster.java create mode 100644 convex-peer/src/test/java/convex/examples/SigSamples.java create mode 100644 convex-peer/src/test/java/convex/net/ConnectionTest.java create mode 100644 convex-peer/src/test/java/convex/net/MemoryByteChannelTest.java create mode 100644 convex-peer/src/test/java/convex/peer/MessageReceiverTest.java create mode 100644 convex-peer/src/test/java/convex/peer/MessageTest.java create mode 100644 convex-peer/src/test/java/convex/peer/RestoreTest.java create mode 100644 convex-peer/src/test/java/convex/peer/ServerTest.java create mode 100644 convex-peer/src/test/java/convex/peer/TestNetwork.java create mode 100644 convex.bat create mode 100644 docs/coding-principles.md create mode 100644 docs/wip/ethos.md create mode 100644 docs/wip/governance.md create mode 100644 docs/wip/misc-faq.md create mode 100644 pom.xml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..27d765118 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.cvx linguist-language=clojure diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml new file mode 100644 index 000000000..399638f14 --- /dev/null +++ b/.github/workflows/deploy_docs.yml @@ -0,0 +1,26 @@ +name: Deploy Docs +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: '11.0.5' + - name: Cache Maven packages + uses: actions/cache@v2 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Run tests + run: mvn clean generate-resources + - name: Deploy 🚀 + uses: JamesIves/github-pages-deploy-action@4.1.4 + with: + branch: gh-pages + folder: convex-cli/target/html + target-folder: convex-cli/ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..77cbad02e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,20 @@ +name: tests +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: '15.0.2' + - name: Cache Maven packages + uses: actions/cache@v2 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Run tests + run: mvn clean test diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..267e6647a --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +*.jar +*.class +*.jfr +*.iml +/lib/ +/classes/ +target/ +/checkouts/ +.nrepl-port +.cpcache/ +/.project +/.classpath +/.settings/ +/.apt_generated/ +/myrecording.jfr +/.apt_generated_tests/ +**/.factorypath +/etch-db +/bin/ +.idea/ +.vscode/ +.DS_Store +/peers-shared-db +.metadata +/pom.xml.versionsBackup diff --git a/.mvn/maven.config b/.mvn/maven.config new file mode 100644 index 000000000..714163dc7 --- /dev/null +++ b/.mvn/maven.config @@ -0,0 +1 @@ +-Drevision=0.7.0-SNAPSHOT \ No newline at end of file diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 000000000..188dc8010 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,25 @@ +# Build instructions + +## Overview + +Convex min repository is structured as a multi-module Maven project. + +## Build and test + +``` +mvn clean install +``` + +## Release preparation + +Set version information + +``` +mvn versions:set -DnewVersion=0.7.0-rc3 +``` + +Build and deploy + +``` +mvn clean deploy -DperformRelease +``` \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..b24b2ff04 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.7.0] - 2020-06-30 +### Added +- Initial Public Alpha release + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..c380358e7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +# Docker for Convex CLI + +FROM maven:3-openjdk-15 + +ENV HOME=/home/convex + +WORKDIR $HOME + +ADD . $HOME + +RUN mvn clean package diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..e3e50106d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,132 @@ +LICENSE DRAFT + +Convex Public Licence v0.1 (WIP DRAFT) + +# Preamble + +This license is intended to support the open development of software within the Convex ecosystem, with two primary objectives: + +A) Enable Open Source development and usage of systems based on Convex technology +B) Ensure effective governance of economic systems and networks based on Convex technology + +As such the license has been written based on the following principles: + +- It is a "weak Copyleft" license, similar in spirit to the Eclipse Public License. Derivative Works must be released under the same license. However, you may freely link and utilise the Work from software using a different license. +- When used to manage assets in a decentralised system, the Convex license places a requirement to operate the system according to the Convex Network Governance Rules. This is primarily done to prevent unauthorised forking, which would be contrary to the objective of Convex to support open, shared global public networks for the Internet of Value + + +==== MODIFIED FROM APACHE 2.0 LICENSE + +# Definitions. + +"Licence" shall mean the terms and conditions for use, reproduction, and distribution as defined by this document. + +"Licensor" shall mean the copyright owner or entity authorised by the copyright owner that is granting the Licence. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorised to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + + + +"Legal Entity" shall mean any of: + 1. An individual + 2. A legally established company or organisation in any jurisdiction + 3. The union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + 4. A decentralised entity operating either autonomously or under the control of one or more Legal Entities. + + + +"Distribute" means the acts of a) distributing or b) making available in any manner that enables the transfer of a copy. + +==== Extra definitions - TBC + +"Economic Assets" shall mean entities with economic or commercial value, or from which economic or commercial value may be derived, through the exercise of partial or exclusive control. An Economic Asset may be entirely digital, a legal right, an asset that exists in the physical world, or any combination of these. + +"Decentralised Economic System" shall mean any system where any of the following are true: +- Consensus over some information of economic interest is determined between two or more Legal Entities +- Economic Assets are under the control of two or more Legal Entities +- Transactions are executed that represent exchange of value between two or more Legal Entities + +"Convex Network" shall mean the union of all systems using the Work as part of a public utility network or operating a Decentralised Economic System. + +"Originator" shall mean the original author of the Convex software. + +"Governance Body" shall mean the the Legal Entity legally authorised by the Originator to administer governance decisions for the Convex Network, which as of June 2021 is the Convex Foundation. + +"Convex Network Governance Rules" shall mean the set of rules established and updated by the governance body for the secure, equitable and efficient operation of the Convex Network + +# License Terms + + + +## 1. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, and distribute the Work and such Derivative Works in Source or Object form. + + + +## 2. Grant of Patent Licence. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + + + +## 3. Distribution + +You may distribute the Work (or Derivative Work) under this Licence, provided that: + + 1. The Work (or Derivative Work) must also be made available as Source Code, and You must accompany the Program with a statement that the Source Code for the Program is available under this Agreement, and informs Recipients how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. + 2. The Work (or Derivative Work) must be distributed either under this license, or a later version of this license formally approved by the Governance Body. + + + + +## 4. Governance + +If you utilise the Work in a Decentralised Economic System, you must ensure that this system is operated in accordance with the Convex Network Governance Rules. This term applies whether or not you have made any modifications to the Work. + + + + +## 5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this Licence, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +## 6. Trademarks. + +This Licence does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +## 7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this Licence. + +## 8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +## 9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this Licence. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +<-- Extra clause for license updates --> + +## 10. Relicensing. + +Contributor grants to the Governing Body a perpetual, worldwide, no-charge, royalty-free, irrevocable right to re-distribute contributions under any future revision of this license approved by the Governing Body. The Convex Foundation will ensure that the main Convex distribution is always free and open source for anyone to use with the Convex Network. + +<--- Licence copyright ---> + +# Copyright + +Copyright The Originator 2018-2021. Includes terms adapted from the Apache 2.0 License and Eclipse Public License v2.0 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..8710ee2a2 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# Convex + +[![Maven Central](https://img.shields.io/maven-central/v/world.convex/convex.svg?label=Maven%20Central)](https://search.maven.org/artifact/world.convex) + +Convex is a decentralised network and execution engine for the Internet of Value. + +It is designed as a full stack solution for decentralised application and economic systems that manage digital assets, where asset ownership is cryptographically secured and can be managed (optionally) with Smart Contracts. It can be considered functionally similar to a decentralised public blockchain, but offers some significant advantages: + +- High transaction throughput (tens of thousands of write transactions per second, potentially scaling to millions) +- Low latency for transaction confirmation (milliseconds for global consensus, depending on network speed) +- 100% Green - energy efficiency using the the Convergent Proof of Stake consensus algorithm +- Global State model with immutable data structures and atomic transactions +- Lambda Calculus based VM supporting Turing complete Smart Contracts +- Integrated on-chain compiler (Convex Lisp) + +## About this repository + +This repository contains the core Convex distribution including: + +- The Convex Virtual Machine (CVM) including data structures and execution environment +- The standard Convex Peer server implementation (NIO based) implementing Convergent Proof of Stake (CPoS) for consensus +- CLI Tools for operating Peers, scripting transactions and more +- The Etch database for persistent data storage +- A Swing GUI for managing local peers / exploring the network +- JMH Benchmarking suite +- Java Client API + +The repository also contains core "on-chain" libraries providing key full-stack functionality and tools for decentralised applications, including: + +- Fungible Tokens +- Non-fungible tokens +- `convex.asset` - library for managing arbitrary digital assets using a common abstraction +- `convex.trust` - library for access control and trusted operations +- `torus.exchange` - decentralised exchange for trading fungible tokens and currencies +- Example code and templates for various forms of smart contracts + +## Key features + +* *Virtual Machine* - The Convex Virtual Machine provides a secure execution environment based on the Lambda Calculus and capable of acting as the execution layer for smart contracts and autonomous agents. +* *Decentralised Consensus* - Similar to Blockchain technology, Convex incorporates a consensus mechanism that ensures all nodes ultimately agree on true values in the system without the control of any single entity. This property means that it is inherently tamper-proof and censorship-resistant. +* *Performance and Scalability* - Convex is capable of executing large volumes of transactions (tens of thousands of transactions per second) with low latency (sub-second global consensus) +* *100% Green* - No wasteful consumption of energy or computing resources + +## Running Convex + +### Command Line Interface (CLI) + +For more information about running a Convex Peer and the Command Line Interface see the [documentation](https://billbsing.github.io/convex/convex-cli/ +) + +### Local GUI Peers + +The convex Peer Manager (GUI application) can be used to run a local test network. + +This can be invoked by running the jar archive directly e.g. with the following command: + +`java -jar convex-gui/target/convex-gui-0.7.0-SNAPSHOT-jar-with-dependencies.jar` + +or you can run this from the command line by using the `local gui` command: + +``` +./convex local gui +``` + + +## Contributing + +Open Source contributions are welcome under the terms of the Convex Public License. Contributors retain copyright to their work, but must accept the terms of the license. + +We are planning to institute a Contributors Agreement for all contributions to the core Convex repository. + +The Convex Foundation may, at its sole discretion, award contributors with Convex Coins as recognition of value contributed to the Convex ecosystem. Convex coins are the native coin of the Convex network, and function as a utility token that provides the right to make use of the services of the network. Convex coins may be exchangeable for other digital assets and currencies. + +## Community + +We use Discord as the primary means for discussing Convex - you can join the public server at [https://discord.gg/5j2mPsk](https://discord.gg/5j2mPsk) + +Alternatively, email: info(at)convex.world + +## Copyright + +Copyright 2017-2021 The Convex Foundation diff --git a/convex b/convex new file mode 100755 index 000000000..4e193e861 --- /dev/null +++ b/convex @@ -0,0 +1,8 @@ +#!/bin/bash + +BASE_PATH=$(dirname $0) +JAR_NAME=`ls -1 $BASE_PATH/convex-cli/target/convex-cli.jar` + +# echo "using: $JAR_NAME" + +java -jar $JAR_NAME $@ diff --git a/convex-benchmarks/.gitignore b/convex-benchmarks/.gitignore new file mode 100644 index 000000000..acd9eef61 --- /dev/null +++ b/convex-benchmarks/.gitignore @@ -0,0 +1,4 @@ +/.settings/ +/.classpath +/.project +/pom.xml.versionsBackup diff --git a/convex-benchmarks/README.md b/convex-benchmarks/README.md new file mode 100644 index 000000000..f4e185ca6 --- /dev/null +++ b/convex-benchmarks/README.md @@ -0,0 +1,41 @@ +# Convex Benchmarks + +## Benchmarking + +Convex includes a wide set of benchmarks, which are used to evaluate performance enhancements. These are mostly implemented with the JMH framework, and reside in the `convex.benchmarks` package. + +## Preparing to run benchmarks + +To run benchmarks, it is best to build the full `convex-benchmarks` jar with dependencies which includes all benchmarks, tests and dependencies. This can be done with the following commend: + +`mvn clean install` + +## Directly running benchmarks + +After building the testing `.jar`, you can launch benchmarks as main classes in the `convex.benchmarks` package, e.g. + +``` +java -cp target/convex-benchmarks-jar-with-dependencies.jar convex.benchmarks.EtchBenchmark +``` + +## Running with Java Flight Recorder + +If you want to analyse profiling results for the benchmarks, you can run using JFR to produce a profiling output file `flight.jfr` + +``` +java -cp target/convex-benchmarks-jar-with-dependencies.jar -XX:+FlightRecorder -XX:StartFlightRecording=duration=200s,filename=flight.jfr convex.benchmarks.CVMBenchmark +``` + +The resulting `flight.jfr` can the be opened in tools such as JDK Mission Control which enables detailed analysis and visualisation of profiling results. This is a useful approach that the Convex team use to identify performance bottlenecks. + +## Benchmark results + +After running benchmarks, you should see results similar to this: + +``` +Benchmark Mode Cnt Score Error Units +EtchBenchmark.readDataRandom thrpt 5 4848620.857 ± 110622.054 ops/s +EtchBenchmark.writeData thrpt 5 728486.145 ± 168739.491 ops/s +``` + +For example, this can be interpreted as an indication that the Etch database layer is handling approximately 4.8 million reads and 729k million atomic writes per second in the testing environment. Usual benchmarking caveats apply and results may vary considerably based on your system setup (available RAM, disk performance etc.) - it is advisable to examine the benchmark source to determine precisely which operations are being performed. diff --git a/convex-benchmarks/pom.xml b/convex-benchmarks/pom.xml new file mode 100644 index 000000000..c33afcc52 --- /dev/null +++ b/convex-benchmarks/pom.xml @@ -0,0 +1,84 @@ + + + world.convex + convex + 0.7.0-rc3 + + + 4.0.0 + + convex-benchmarks + + Convex Benchmarks + Convex Benchmark Suite + https://convex.world + + + convex-benchmarks + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 11 + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + + + + + + maven-assembly-plugin + 3.3.0 + + ${project.directory} + + + convex.cli.Main + + + + jar-with-dependencies + + + + + + create-archive + package + + single + + + + + + + + + + world.convex + convex-peer + ${convex.version} + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + true + + + + diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/Benchmarks.java b/convex-benchmarks/src/main/java/convex/benchmarks/Benchmarks.java new file mode 100644 index 000000000..5f3494de0 --- /dev/null +++ b/convex-benchmarks/src/main/java/convex/benchmarks/Benchmarks.java @@ -0,0 +1,55 @@ +package convex.benchmarks; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.stream.Collectors; + +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.openjdk.jmh.runner.options.TimeValue; + +import convex.core.State; +import convex.core.crypto.AKeyPair; +import convex.core.data.AccountKey; +import convex.core.data.Address; +import convex.core.init.Init; +import convex.core.lang.Context; + +public class Benchmarks { + + public static final AKeyPair[] KEYPAIRS = new AKeyPair[] { + AKeyPair.createSeeded(2), + AKeyPair.createSeeded(3), + AKeyPair.createSeeded(5), + AKeyPair.createSeeded(7), + AKeyPair.createSeeded(11), + AKeyPair.createSeeded(13), + AKeyPair.createSeeded(17), + AKeyPair.createSeeded(19), + }; + + public static ArrayList PEER_KEYPAIRS=(ArrayList) Arrays.asList(KEYPAIRS).stream().collect(Collectors.toList()); + public static ArrayList PEER_KEYS=(ArrayList) Arrays.asList(KEYPAIRS).stream().map(kp->kp.getAccountKey()).collect(Collectors.toList()); + + public static final AKeyPair FIRST_PEER_KEYPAIR = KEYPAIRS[0]; + public static final AccountKey FIRST_PEER_KEY = FIRST_PEER_KEYPAIR.getAccountKey(); + + public static final AKeyPair HERO_KEYPAIR = KEYPAIRS[0]; + public static final AKeyPair VILLAIN_KEYPAIR = KEYPAIRS[1]; + + public static Address HERO=Init.getGenesisAddress(); + public static Address VILLAIN=HERO.offset(2); + + public static final AccountKey HERO_KEY = HERO_KEYPAIR.getAccountKey(); + + public static final State STATE = Init.createState(PEER_KEYS); + + static Options createOptions(Class c) { + return new OptionsBuilder().include(c.getSimpleName()).warmupIterations(1).measurementIterations(5) + .warmupTime(TimeValue.seconds(1)).measurementTime(TimeValue.seconds(1)).forks(0).build(); + } + + public static Context context() { + return Context.createFake(STATE,HERO); + } +} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/BigBlockBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/BigBlockBenchmark.java new file mode 100644 index 000000000..2dc14120c --- /dev/null +++ b/convex-benchmarks/src/main/java/convex/benchmarks/BigBlockBenchmark.java @@ -0,0 +1,65 @@ +package convex.benchmarks; + +import java.util.ArrayList; +import java.util.Random; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; + +import convex.core.Block; +import convex.core.BlockResult; +import convex.core.State; +import convex.core.crypto.AKeyPair; +import convex.core.crypto.Ed25519KeyPair; +import convex.core.data.ACell; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.core.data.SignedData; +import convex.core.exceptions.BadSignatureException; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Transfer; + +public class BigBlockBenchmark { + + static final int NUM_ACCOUNTS = 1000; + static final int NUM_TRANSACTIONS = 1000; + private static final long INITIAL_FUNDS = 1000000000; + static ArrayList keyPairs = new ArrayList(); + static ArrayList
addresses = new ArrayList
(); + public static State state = Benchmarks.STATE; + public static Block block; + static ArrayList> transactions = new ArrayList>(); + + static { + for (int i = 0; i < NUM_ACCOUNTS; i++) { + AKeyPair kp = Ed25519KeyPair.generate(); + keyPairs.add(kp); + + // Create synthetic accounts + Address a=state.nextAddress(); + state = state.putAccount(a, (AccountStatus.create(INITIAL_FUNDS,kp.getAccountKey()))); + addresses.add(a); + } + for (int i = 0; i < NUM_TRANSACTIONS; i++) { + int src=new Random().nextInt(NUM_ACCOUNTS); + AKeyPair kp = keyPairs.get(src); + Address source=addresses.get(src); + Address target = addresses.get(new Random().nextInt(NUM_ACCOUNTS)); + Transfer t = Transfer.create(source,1, target, 1); + transactions.add(kp.signData(t)); + } + block = Block.create(System.currentTimeMillis(),transactions,Benchmarks.FIRST_PEER_KEY); + } + + @Benchmark + public void benchmark() throws BadSignatureException { + BlockResult br=state.applyBlock(block); + ACell.createPersisted(br.getState()); + } + + public static void main(String[] args) throws Exception { + Options opt = Benchmarks.createOptions(BigBlockBenchmark.class); + new Runner(opt).run(); + } +} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/CVMBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/CVMBenchmark.java new file mode 100644 index 000000000..2d57cfa71 --- /dev/null +++ b/convex-benchmarks/src/main/java/convex/benchmarks/CVMBenchmark.java @@ -0,0 +1,84 @@ +package convex.benchmarks; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; + +import convex.core.State; +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.data.Keywords; +import convex.core.data.Maps; +import convex.core.data.Strings; +import convex.core.data.Vectors; +import convex.core.init.Init; +import convex.core.lang.Context; +import convex.core.lang.Core; +import convex.core.lang.Symbols; +import convex.core.lang.ops.Constant; +import convex.core.lang.ops.Lookup; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Call; +import convex.core.transactions.Invoke; +import convex.core.transactions.Transfer; + +/** + * Benchmark for applying transactions to CVM state. This is measuring the end-to-end time for processing + * transactions themselves on the CVM. + * + * Skips stuff around transactions, block overhead, signatures etc. + */ +public class CVMBenchmark { + static State STATE=Benchmarks.STATE; + static Address HERO=Benchmarks.HERO; + + @Benchmark + public void smallTransfer() { + State s=STATE; + Address addr=HERO; + ATransaction trans=Transfer.create(addr,1, Benchmarks.VILLAIN, 1000); + Context ctx=s.applyTransaction(trans); + ctx.getValue(); + } + + @Benchmark + public void simpleCalculationStatic() { + State s=STATE; + Address addr=HERO; + ATransaction trans=Invoke.create(addr,1, convex.core.lang.ops.Invoke.create(Constant.create(Core.PLUS),Constant.of(1L),Constant.of(2L))); + Context ctx=s.applyTransaction(trans); + ctx.getValue(); + } + + @Benchmark + public void simpleCalculationDynamic() { + State s=STATE; + Address addr=HERO; + ATransaction trans=Invoke.create(addr,1, convex.core.lang.ops.Invoke.create(Lookup.create("+"),Constant.of(1L),Constant.of(2L))); + Context ctx=s.applyTransaction(trans); + ctx.getValue(); + } + + @Benchmark + public void defInEnvironment() { + State s=STATE; + Address addr=HERO; + ATransaction trans=Invoke.create(addr,1, convex.core.lang.ops.Def.create("a", Constant.of(13L))); + Context ctx=s.applyTransaction(trans); + ctx.getValue(); + } + + @Benchmark + public void contractCall() { + State s=STATE; + Address addr=HERO; + ATransaction trans=Call.create(addr,1L, Init.REGISTRY_ADDRESS, Symbols.REGISTER, Vectors.of(Maps.of(Keywords.NAME,Strings.create("Bob")))); + Context ctx=s.applyTransaction(trans); + ctx.getValue(); + } + + public static void main(String[] args) throws Exception { + Options opt = Benchmarks.createOptions(CVMBenchmark.class); + new Runner(opt).run(); + } +} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/EtchBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/EtchBenchmark.java new file mode 100644 index 000000000..2ed6a77e8 --- /dev/null +++ b/convex-benchmarks/src/main/java/convex/benchmarks/EtchBenchmark.java @@ -0,0 +1,53 @@ +package convex.benchmarks; + +import java.util.Random; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; + +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.Ref; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMLong; +import etch.EtchStore; + +public class EtchBenchmark { + + static final EtchStore store=EtchStore.createTemp(); + + static long nonce=0; + + @SuppressWarnings("unchecked") + static Ref[] refs=new Ref[1000]; + + static final Random rand=new Random(); + + static { + for (int i=0; i<1000; i++) { + AVector v=Vectors.of(0L,(long)i); + Ref r=v.getRef(); + refs[i]=r; + r.getHash(); + store.storeTopRef(r, Ref.STORED, null); + } + } + + @Benchmark + public void writeData() { + AVector v=Vectors.of(1L,nonce++); + store.storeTopRef(v.getRef(), Ref.STORED, null); + } + + @Benchmark + public void readDataRandom() { + int ix=rand.nextInt(1000); + store.refForHash(refs[ix].getHash()); + } + + public static void main(String[] args) throws Exception { + Options opt = Benchmarks.createOptions(EtchBenchmark.class); + new Runner(opt).run(); + } +} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/EvalBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/EvalBenchmark.java new file mode 100644 index 000000000..ba5e24b0d --- /dev/null +++ b/convex-benchmarks/src/main/java/convex/benchmarks/EvalBenchmark.java @@ -0,0 +1,49 @@ +package convex.benchmarks; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; + +import convex.core.data.ACell; +import convex.core.lang.Context; +import convex.core.lang.Reader; + +public class EvalBenchmark { + + static final Context CTX=Benchmarks.context(); + + private static final ACell eval(ACell form) { + return CTX.fork().eval(form).getResult(); + } + + static final ACell loopOp=Reader.read("(dotimes [i 1000])"); + @Benchmark + public void emptyLoop() { + eval(loopOp); + } + + static final ACell constantOp=Reader.read("1"); + @Benchmark + public void constant() { + eval(constantOp); + } + + // sum with dynamic core lookup + static final ACell simpleSum=Reader.read("(+ 1 2)"); + @Benchmark + public void simpleSum() { + eval(simpleSum); + } + + // sum with eval + static final ACell simpleSum2=Reader.read("(eval '(+ 1 2))"); + @Benchmark + public void evalSum() { + eval(simpleSum2); + } + + public static void main(String[] args) throws Exception { + Options opt = Benchmarks.createOptions(EvalBenchmark.class); + new Runner(opt).run(); + } +} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/HashBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/HashBenchmark.java new file mode 100644 index 000000000..1a50783d0 --- /dev/null +++ b/convex-benchmarks/src/main/java/convex/benchmarks/HashBenchmark.java @@ -0,0 +1,32 @@ +package convex.benchmarks; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; + +import convex.core.crypto.Hashing; +import convex.core.data.AArrayBlob; +import convex.core.data.Blob; +import convex.core.data.Format; +import convex.core.data.prim.CVMLong; + +public class HashBenchmark { + + @Benchmark + public void longHash_SHA_256() { + CVMLong l = CVMLong.create(17L); + AArrayBlob d = Format.encodedBlob(l); + Hashing.sha256(d.getInternalArray()); + } + + @Benchmark + public void kilobyteHash() { + Blob b = Blob.wrap(new byte[1024]); + b.getHash(); + } + + public static void main(String[] args) throws Exception { + Options opt = Benchmarks.createOptions(HashBenchmark.class); + new Runner(opt).run(); + } +} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/LatencyBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/LatencyBenchmark.java new file mode 100644 index 000000000..38f2b0bef --- /dev/null +++ b/convex-benchmarks/src/main/java/convex/benchmarks/LatencyBenchmark.java @@ -0,0 +1,113 @@ +package convex.benchmarks; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; + +import convex.api.Convex; +import convex.core.Coin; +import convex.core.Result; +import convex.core.crypto.AKeyPair; +import convex.core.data.Address; +import convex.core.lang.ops.Constant; +import convex.core.transactions.Invoke; +import convex.peer.API; +import convex.peer.Server; + +/** + * Benchmark for full round-trip latencies + */ +public class LatencyBenchmark { + + static Address HERO=null; + static Address VILLAIN=null; + static final AKeyPair[] KPS=new AKeyPair[] {AKeyPair.generate(),AKeyPair.generate()}; + + static Server server; + static Convex client; + static Convex client2; + static Convex peer; + static { + List servers=API.launchLocalPeers(Benchmarks.PEER_KEYPAIRS, Benchmarks.STATE, null, null); + server=servers.get(0); + try { + Thread.sleep(1000); + peer=Convex.connect(server); + HERO=peer.createAccountSync(KPS[0].getAccountKey()); + VILLAIN=peer.createAccountSync(KPS[1].getAccountKey()); + peer.transfer(HERO, Coin.EMERALD); + peer.transfer(VILLAIN, Coin.EMERALD); + + client=Convex.connect(server.getHostAddress(), HERO,KPS[0]); + client2=Convex.connect(server.getHostAddress(), VILLAIN,KPS[1]); + } catch (IOException | TimeoutException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + + @Benchmark + public void roundTripTransaction() throws TimeoutException, IOException { + client.transactSync(Invoke.create(Benchmarks.HERO,-1, Constant.of(1L))); + // System.out.println(server.getBroadcastCount()); + } + + @Benchmark + public void roundTripTwoTransactions() throws TimeoutException, IOException, InterruptedException, ExecutionException { + Future r1=client.transact(Invoke.create(HERO,-1, Constant.of(1L))); + Future r2=client2.transact(Invoke.create(VILLAIN,-1, Constant.of(1L))); + r1.get(1000,TimeUnit.MILLISECONDS); + r2.get(1000,TimeUnit.MILLISECONDS); + } + + @Benchmark + public void roundTrip10Transactions() throws TimeoutException, IOException, InterruptedException, ExecutionException { + doTransactions(10); + } + + @Benchmark + public void roundTrip50Transactions() throws TimeoutException, IOException, InterruptedException, ExecutionException { + doTransactions(50); + } + + @Benchmark + public void roundTrip1000Transactions() throws TimeoutException, IOException, InterruptedException, ExecutionException { + doTransactions(1000); + } + + @SuppressWarnings("unchecked") + private void doTransactions(int n) throws IOException, InterruptedException, ExecutionException, TimeoutException { + CompletableFuture[] rs=new CompletableFuture[n]; + for (int i=0; i f=client.transact(Invoke.create(HERO,-1, Constant.of(i))); + rs[i]=f; + } + CompletableFuture.allOf(rs).get(1000,TimeUnit.MILLISECONDS); + Result r0=rs[0].get(); + if (r0.isError()) { + throw new Error("Transaction failed: "+r0); + } + } + + @Benchmark + public void roundTripQuery() throws TimeoutException, IOException, InterruptedException, ExecutionException { + client.querySync(Constant.of(1L)); + } + + + public static void main(String[] args) throws Exception { + Options opt = Benchmarks.createOptions(LatencyBenchmark.class); + new Runner(opt).run(); + } +} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/ListDataBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/ListDataBenchmark.java new file mode 100644 index 000000000..6f0ae326d --- /dev/null +++ b/convex-benchmarks/src/main/java/convex/benchmarks/ListDataBenchmark.java @@ -0,0 +1,25 @@ +package convex.benchmarks; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; + +import convex.core.data.AVector; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMLong; + +public class ListDataBenchmark { + + @Benchmark + public void append1000() { + AVector list = Vectors.empty(); + for (long i = 0; i < 1000; i++) { + list = list.append(CVMLong.create(i)); + } + } + + public static void main(String[] args) throws Exception { + Options opt = Benchmarks.createOptions(ListDataBenchmark.class); + new Runner(opt).run(); + } +} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/MapBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/MapBenchmark.java new file mode 100644 index 000000000..8d2ece0b3 --- /dev/null +++ b/convex-benchmarks/src/main/java/convex/benchmarks/MapBenchmark.java @@ -0,0 +1,26 @@ +package convex.benchmarks; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; + +import convex.core.data.AMap; +import convex.core.data.Maps; +import convex.core.data.prim.CVMLong; + +public class MapBenchmark { + + @Benchmark + public void assocMap1000() { + AMap m = Maps.empty(); + for (long i = 0; i < 1000; i++) { + CVMLong ci=CVMLong.create(i); + m = m.assoc(ci, ci); + } + } + + public static void main(String[] args) throws Exception { + Options opt = Benchmarks.createOptions(MapBenchmark.class); + new Runner(opt).run(); + } +} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/OpBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/OpBenchmark.java new file mode 100644 index 000000000..3dd0d4a7b --- /dev/null +++ b/convex-benchmarks/src/main/java/convex/benchmarks/OpBenchmark.java @@ -0,0 +1,55 @@ +package convex.benchmarks; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; + +import convex.core.data.ACell; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.Core; +import convex.core.lang.Reader; +import convex.core.lang.ops.Constant; +import convex.core.lang.ops.Invoke; + +public class OpBenchmark { + + static final Context CTX=Benchmarks.context(); + + private static final ACell runOp(AOp op) { + return CTX.fork().execute(op).getResult(); + } + + static final AOp loopOp=CTX.expandCompile(Reader.read("(dotimes [i 1000])")).getResult(); + @Benchmark + public void emptyLoop() { + runOp(loopOp); + } + + static final AOp constantOp=CTX.expandCompile(Reader.read("1")).getResult(); + @Benchmark + public void constant() { + runOp(constantOp); + } + + // sum with dynamic core lookup + static final AOp simpleSum=CTX.expandCompile(Reader.read("(+ 1 2)")).getResult(); + @Benchmark + public void simpleSum() { + runOp(simpleSum); + } + + // sum without dynamic core lookup (much faster!!) + static final AOp simpleSum2=Invoke.create(Constant.create(Core.PLUS),Constant.of(1L),Constant.of(2)); + @Benchmark + public void simpleSumPrecompiled() { + runOp(simpleSum2); + } + + + + public static void main(String[] args) throws Exception { + Options opt = Benchmarks.createOptions(OpBenchmark.class); + new Runner(opt).run(); + } +} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/SignatureBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/SignatureBenchmark.java new file mode 100644 index 000000000..5a7cb49b0 --- /dev/null +++ b/convex-benchmarks/src/main/java/convex/benchmarks/SignatureBenchmark.java @@ -0,0 +1,61 @@ +package convex.benchmarks; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; + +import convex.core.crypto.AKeyPair; +import convex.core.crypto.ASignature; +import convex.core.data.ABlob; +import convex.core.data.ACell; +import convex.core.data.Blobs; +import convex.core.data.Ref; +import convex.core.data.SignedData; + +public class SignatureBenchmark { + + private static final AKeyPair KEYPAIR=AKeyPair.generate(); + private static final SignedData SIGNED=makeSigned(); + + private static SignedData makeSigned() { + SignedData signed= KEYPAIR.signData(Blobs.fromHex("cafebabe")); + ACell.createPersisted(signed); + return signed; + } + + private static final ASignature SIGNATURE=SIGNED.getSignature(); + + @Benchmark + public void signData() { + ABlob b=Blobs.createRandom(16); + KEYPAIR.signData(b); + } + + @Benchmark + public void signVerify() { + ABlob b=Blobs.createRandom(16); + SignedData sd=KEYPAIR.signData(b); + ASignature sig=sd.getSignature(); + + sig.verify(b.getHash(), KEYPAIR.getAccountKey()); + } + + @Benchmark + public void verify() { + ASignature sig=SIGNATURE; + sig.verify(SIGNED.getValue().getHash(), KEYPAIR.getAccountKey()); + } + + @SuppressWarnings("unchecked") + @Benchmark + public void verifyFromStore() { + SignedData signed=(SignedData) Ref.forHash(SIGNED.getHash()).getValue(); + signed.checkSignature(); + } + + + public static void main(String[] args) throws Exception { + Options opt = Benchmarks.createOptions(SignatureBenchmark.class); + new Runner(opt).run(); + } +} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/ThreadCoordinationBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/ThreadCoordinationBenchmark.java new file mode 100644 index 000000000..24f56e26f --- /dev/null +++ b/convex-benchmarks/src/main/java/convex/benchmarks/ThreadCoordinationBenchmark.java @@ -0,0 +1,48 @@ +package convex.benchmarks; + +import java.util.concurrent.ArrayBlockingQueue; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; + +import convex.core.util.Utils; + +public class ThreadCoordinationBenchmark { + + public static final ArrayBlockingQueue INPUT =new ArrayBlockingQueue<>(100); + public static final ArrayBlockingQueue OUTPUT =new ArrayBlockingQueue<>(100); + + static { + Thread processor = new Thread(()->{ + while (true) { + try { + Object o; + o = INPUT.take(); + OUTPUT.put(o); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + }); + processor.setDaemon(true); + processor.start(); + } + + @Benchmark + public void pushThroughQueue() { + try { + INPUT.put(1L); + OUTPUT.take(); + } catch (InterruptedException e) { + throw Utils.sneakyThrow(e); + } + } + + public static void main(String[] args) throws Exception { + Options opt = Benchmarks.createOptions(ThreadCoordinationBenchmark.class); + new Runner(opt).run(); + } + +} diff --git a/convex-cli/.gitignore b/convex-cli/.gitignore new file mode 100644 index 000000000..e661051a1 --- /dev/null +++ b/convex-cli/.gitignore @@ -0,0 +1,5 @@ +/target/ +/.settings/ +/.classpath +/.project +/pom.xml.versionsBackup diff --git a/convex-cli/pom.xml b/convex-cli/pom.xml new file mode 100644 index 000000000..4e3884d0f --- /dev/null +++ b/convex-cli/pom.xml @@ -0,0 +1,213 @@ + + + world.convex + convex + 0.7.0-rc3 + + 4.0.0 + + convex-cli + + Convex CLI + Convex command line integration and tools + https://convex.world + + + + 1.2.3 + 1.6.0 + 0.6.0 + + + + + spring-repo + https://repo.spring.io/release + + + + + + maven-assembly-plugin + 3.3.0 + + ${project.directory} + + + convex.cli.Main + + + + jar-with-dependencies + + convex-cli + + false + + + + create-archive + package + + single + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + io.spring.asciidoctor + spring-asciidoctor-extensions-block-switch + ${asciidoctor.spring.version} + + + org.asciidoctor + asciidoctorj-pdf + ${asciidoctorj.pdf.version} + + + + + convert-to-html + generate-resources + + process-asciidoc + + + ${project.build.directory}/html + + coderay + ./images + left + font + + + + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + + org.asciidoctor + + + asciidoctor-maven-plugin + + + [2.2.1,) + + + + process-asciidoc + + + + + + + + + + + + + + + + + + world.convex + convex-peer + ${convex.version} + + + world.convex + convex-core + ${convex.version} + + + world.convex + convex-gui + ${convex.version} + + + org.slf4j + slf4j-simple + + + + + info.picocli + picocli + 4.6.1 + + + com.pholser + junit-quickcheck-core + 1.0 + test + + + com.pholser + junit-quickcheck-generators + 1.0 + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.junit.vintage + junit-vintage-engine + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + + + org.slf4j + slf4j-api + ${slf4j.version} + + + ch.qos.logback + logback-core + ${logback.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + diff --git a/convex-cli/src/docs/asciidoc/images/convex_logo.svg b/convex-cli/src/docs/asciidoc/images/convex_logo.svg new file mode 100644 index 000000000..84fcb7786 --- /dev/null +++ b/convex-cli/src/docs/asciidoc/images/convex_logo.svg @@ -0,0 +1 @@ +Convex logoCreated with Sketch. diff --git a/convex-cli/src/docs/asciidoc/index.adoc b/convex-cli/src/docs/asciidoc/index.adoc new file mode 100644 index 000000000..52ea07d8e --- /dev/null +++ b/convex-cli/src/docs/asciidoc/index.adoc @@ -0,0 +1,660 @@ += Convex Command Line Interface +:toc: +:toc-title: Convex CLI + +image::convex_logo.svg[Convex Command Line Interface,100,float=right,opts=inline] + +== Introduction +Convex Command Line Interface or CLI, allows you to control and setup a local Convex network, or add a peer to an already existing network. +The current test Convex network can be found at https://convex.world[convex.world]. + + + +=== Overview +This CLI is part of the Convex code base. The CLI has been built to do the following things: + +. Run a Local Convex network. + +. Run a Local Peer(s) connected to a local network. + +. Send and Query transactions within a local network. + +. Setup accounts within a local network. + +. Step key pairs to use for accessing local and remote networks. + +. Run a Local Peer connected to a remote Convex network. + + + +== Getting Started +As yet we have not packaged this in any package manager for downloading so currently the CLI can only be downloaded via github. + +=== Get the source +You will need to visit https://github.com/Convex-Dev/convex[convex-dev @ github] and clone the repository onto your local computer. + +The develop branch is currently the latest version. + +.terminal commands + git clone https://github.com/Convex-Dev/convex.git + cd convex + git checkout develop + git pull + +=== Convex Projects +The Convex code repository is made up of the following sub projects: + +convex-benchmaks:: Run benchmarks on the convex network. +convex-cli:: This CLI project. +convex-core:: The main convex core library. +convex-gui:: A local convex network running as a GUI application. +convex-peer:: Peer library used to run convex peers. + + +=== Compile and Setup +Once you have downloaded the latest source of Convex, you can now compile the suite of projects. + +To do this you need to execute the `Maven` command: + +.terminal command + mvn install + +or + +.terminal command + mvn package + +If you wish to build without running the tests you can append the option `-DskipTests` + + +After building and installing the maven dependencies you should eventually see the following lines +generated by the Maven build process: + +.output +---- +[INFO] ------------------------------------------------------------------------ +[INFO] Reactor Summary for convex 0.7.0-SNAPSHOT: +[INFO] +[INFO] convex ............................................. SUCCESS [ 0.146 s] +[INFO] convex-core ........................................ SUCCESS [ 5.003 s] +[INFO] convex-peer ........................................ SUCCESS [ 0.027 s] +[INFO] convex-gui ......................................... SUCCESS [ 2.474 s] +[INFO] convex-cli ......................................... SUCCESS [ 4.665 s] +[INFO] convex-benchmarks .................................. SUCCESS [ 1.644 s] +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 14.463 s +[INFO] Finished at: 0000-00-00T00:00:00+00:00 +[INFO] ------------------------------------------------------------------------ +---- + +=== Files needed by CLI run a local Network or Peer +The CLI needs 3 types of files before running a local Convex network or as a Peer on any network. +The type of files are: + +. _Etch Storage database_ file. This contains the stored state of the Convex network. Usually when starting up the initial cluster the first set of peers share the same Etch database. CLI Parameter: *--etch* + +. _Keystore database_ file. This file contains the private/public key pairs used for the peers and any subsequent users. CLI Parameters: *--keystore*, *--password* + +. _Session_ file. This is created by the CLI to keep track of the locally running peers, so that if you want to access the local network or add another peer to the local network, the CLI will look at the session file for a randomly available peer to connect too. CLI Parameter: *--session* + +[CAUTION] +==== +The GUI version and the CLI run the same local network. The only difference is that the GUI does not create a session file. This means that some of the CLI features cannot be used with the GUI local network. +==== + + +== Running the CLI +Once you have successfully compiled and built Convex projects, you can now run the command line tool. + +.Mac +[source,bash,role="primary"] +---- +./convex help +---- + +.Linux +[source,bash,role="secondary"] +---- +./convex help +---- + +.Windows +[source,bash,role="secondary"] +---- +convex help + +---- + +=== Commands +The CLI is split into command the following commands and subcommands: + +Account Commands:: + +[cols="1,1,2"] +|=== +|Command|Sub command|Description + +|account, ac| |Manages convex accounts. +||balance, bal, ba |Get an account balance. + +||create, cr| Creates an account on a local network using a public/private key from the keystore. +||fund, fu|Transfers funds to an account using a public/private key from the keystore. +||information, info, in|Get account information. +|=== + +Key Commands:: +[cols="1,1,2"] +|=== +|Command|Sub command|Description + +|key, ke| |Manage local Convex key store. + +||import, im|Import key pairs to the keystore. +||generate, ge|Generate one or more key pairs. +||list, li|List available key pairs. +||export, ex|Export key pair from the keystore. +|=== + +Local Commands:: +[cols="1,1,2"] +|=== +|Command|Sub command|Description + +|local, lo||Operates a local convex network. +||gui|Starts a local convex test network using the peer manager GUI application. +||start, st|Starts a local convex test network, same as GUI but using a command line. +|=== + +Peer Commands:: +[cols="1,1,2"] +|=== +|Command|Sub command|Description + +|peer, pe||Operates a local peer. +||create, cr|Creates a keypair, new account and a funding stake: to run a local peer. +||start, st|Starts a local peer. +|=== + +Query Command:: +[cols="1,1"] +|=== +|Command|Description + +|query, qu|Execute a query on the current peer. +|=== + +Status Command:: +[cols="1,1"] +|=== +|Command|Description + +|status, st|Reports on the current status of the network. +|=== + +Transaction Command:: +[cols="1,1"] +|=== +|Command|Description + + +|transaction, transact, tr|Execute a transaction on the network via a peer. +|=== + +Help Command:: +[cols="1,1"] +|=== +|Command|Description + +|help|Displays help information about the specified command +|=== + +=== Shared Options +There are a few common options that can be used with any command or sub command. They are as follows: + +[cols="1,2,4"] +|=== +|Short Option|Long Option|Description + +|-c|--config= |Use the specified config file. +|-e|--etch= |Convex state storage filename. The default is to use a temporary storage filename. +|-k|--keystore= |keystore filename. Default: ~/.convex/keystore.pfx +|-p|--password= |Password to read/write to the Keystore +|-s|--session= |Session filename. Defaults ~/.convex/session.conf +|-v|--verbose |Show more verbose log information. You can increase verbosity by using multiple -v or -vvv +|-h|--help |Show this help message and exit. +|-V|--version |Print version information and exit. +|=== + +=== Requesting Help +The CLI supports help using the *-h* or *--help* options or the command *help*. For each sub command there are more help options. + +So for example + +.terminal command + ./convex --help + +will show the common options for all commands, and the list of available commands. + +.terminal command + ./convex local start --help + +will show the common options as well as the specific options for the *convex.local.start* command + +[#command-local-start] +== Starting a local network +The CLI is designed to start a local Convex network. This will allow for the developer/tester to try out Convex in a local environment without +effecting any other networks. + +=== Simple local start +The simplest way to start up the local Convex network is to run the following command: + +.terminal command + ./convex local start --password=my-password + + +[WARNING] +==== +In this document the password option will always be shown as `--password=my-password`. This is an example of a not very good password to use for storing your keys. We suggest that you use a more secure password instead of `my-password`. +==== + +You wil always need to pass the password to the *keystore* file since the CLI will need access the keys to create and start up the local peers. + +The CLI will automatically create 4 keypairs and place them in the keystore. The CLI will then start up 4 peers all sharing a single +temporary local _Etch Database_ in the /tmp folder. + +The Simple local start consists of the following steps: + +. Create the _count_ number of peer keypairs. +. Store the new keypairs in the keystore. +. Start up the local network using the new created keys. + + + +=== Local start with peer keys +While the simple local network start will auto generate public keys for the local peers and create the peer accounts. You have the option instead to start the local network using a predetermined set of keys from your keystore. To do this you need to provide a list of public keys that you want the CLI to use to start up the local network. + + +If you have already used the simple local start, you can get the list of keys created by running the <>, +this will show you the list of keys that have been stored in the key store. + +.terminal session +---- +./convex key list --password=my-password + +Index Public Key +1 6e89035fce6d842b65e7831433fb3426928865a3c8de9536cfa50a1928eb0276 <1> +2 13e691e05dee5a2c5ad90f6802f4ac5c274582ca5332516dc4740ae55d817856 +3 8291e8976e0ee0363f98f819712552924e1dd1d8ab77c4dc8577765ee3eb2d36 +4 ce55bb850cefaf87c5a16ab7c410f942e11463d0000eb71e8a22e6ce76301b5c +5 21076aa0c88baba170e62196b5735316f6cc1c5bfe672c0c1e5f9b85d8aaf8cb + +---- + +<1> First keypair stored in the keystore with the public key starting with `6e89035fce6...` or at index position #1 + +See <> for more informaton. + + +To start up the local Convex network with the first 4 public keys for the first 4 peers you can run the following command: + +.terminal command + ./convex local start --public-key=6e89035 --public-key=13e691e --public-key=8291e89 --public-key=ce55bb8 --password=my-password + +or you can combine the public key fields together into a single comma seperated list option such as: + +.terminal command + ./convex local start --public-key=6e89035,13e691e,8291e89,ce55bb8 --password=my-password + +This will now start up a local Convex network with 4 peers each using a public key from the list provided in the keystore. + +[TIP] +==== +To start the same peers using the same public keys you can also use the index number in the keystore. So the line: + + ./convex local start --index-key=1,2,3,4 --password=my-password + +Will start the same set of peers as above using the first 4 key pairs from the keystore. +==== + +=== Local start with port numbers +By default the CLI start a local network with each peer assigned a random port number. You can specify the port numbers used for each peer, by setting the `--ports` option. + +The `--ports` option takes a list or range of port numbers. + +You can use multiple `--ports` options such as: + + ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081 --ports=8082 --ports=8083 --ports=8084 + +or you can provide a list of ports to use for each peer: + + ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081,8082,8083,8084 + +or a range of port numbers: + + ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081-8084 + +or an open range for any number of peers: + + ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081- + +or a combination of the above, where the first peer uses port 8088, and all subsequent peers use ports from 8090: + + ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8088 --ports=8090- + + +=== Local start with config file +You can create a config file and assign the command options as config items. You can then start your +local network using a config file, instead of providing a list of keys. + +.terminal command + ./convex local start --config=example_convex_local_start.conf + + +==== Config Parameters for convex.local.start +.file: example_convex_local_start.conf +---- + # etch storage database + convex.etch = <.> + + # default keystore filename + convex.keystore =$HOME/.convex/keystore.pfx + + # default session filename + convex.session = $HOME/.convex/session.conf + + # number of peers to start + convex.local.start = 4 + + # comma list of index of keys or items <.> + convex.local.start.index-key= + + # comma list of public-key hex values, or multiple items + convex.local.start.public-key=6e89035 + convex.local.start.public-key=13e691e + convex.local.start.public-key=8291e89 + convex.local.start.public-key=ce55bb8 + + convex.local.start.ports=8090- <.> + + # keystore password + convex.local.password = <.> +---- + +<.> If no filename is provided, then the CLI will create a temporary etch storage database in the temp folder. +<.> You can provide a list of public keys or indexes or duplicate settings with different values. + + convex.local.index-key = 1,2,3 + # is the same as + convex.local.index-key = 1 + convex.local.index-key = 2 + convex.local.index-key = 3 + +<.> The peers will use port 8090 onwards +<.> If you do not provide a password, then the CLI will request a password on starting the local network. + +[#command-peer-start-local] +== Starting a local Peer +How to start a local peer, and join a local Convex network. + +To start a local peer you first need to do the following: + +. Start a local Convex network. see <>. + +. Create a keypair, or select an unused keypair to use for the peer. + +. Create an account for the peer. + +. Assign funds to the peer account. + +. Assign the peer account funds for the peer stake. + + +[NOTE] +==== +This type of block chain technology uses Convergent Proof of Stake (CPoS) algorithm, where each peer has a public key and a stake amount. The stake amount decides the peers voting control in the CPoS algorithm. See https://convex.world/technology[Convex Technology] +==== + +The following command does all of the above except step #1: + + ./convex peer create --password=my-password + +You will then get back from the `peer create` command something like this: + + Public Peer Key: 0xbc1290834e1953b2952624ab8ce34e87d308ba975d655163f9fe47283f0436aa + Address: 45 + Balance: 199945799 + Inital stake amount: 9800000000 + Peer start line: ./convex peer start --password=my-password --address=45 --public-key=bc1290 + +you can then copy the *Peer start line:* and run a peer with the local network. + + ./convex peer start --password=my-password --address=45 --public-key=bc1290 + +[#command-peer-start-remote] +== Starting a local Peer to a remote Convex network +How to start a local peer, that connects to a remote Convex network. + +If you whish to connect to your own remote peer, you can by adding the `--peer` option. This tells the new peer you are starting where a remote peer is located. Once found the started peer will try and sync with the remote peer. + +To connect to someone elses remote network or to connect to the test network at https://convex.world[convex] you will need to obtain a peer keypair, account with sufficient funds and the peer registered with a stake amount. + + +[#command-local-gui] +== Starting the GUI local network +How to start the gui local network. + +To start the local GUI network, you can call the command: + + ./convex local gui + +This starts the a local network in GUI mode. At the moment the GUI local network does not publish the keypairs used for the network, so the CLI cannot do the following when the GUI network is running: + +. Account Fund Request + +. Account Create + +. Peer Create + + +== Peer Output +Describes the output fields + +[.small] +.Sample output +---- +Starting network Id: 0xefe75ea61ad52b38f4455a88911b7bd851dc080090e1b1cb4ec75d85a44eb92d +#2: Peer:1770c3 URL: localhost:43849 Status: J NS Connections: 1/ 0 Consensus: 0 State:efe75e Belief:46bbe3 Msg: connection +#1: Peer:fa26c5 URL: localhost:41635 Status: J NS Connections: 1/ 0 Consensus: 0 State:efe75e Belief:7c7542 Msg: connection +#3: Peer:556deb URL: localhost:37985 Status: J NS Connections: 1/ 0 Consensus: 0 State:efe75e Belief:a43082 Msg: connection +#4: Peer:0fce50 URL: localhost:46559 Status: J NS Connections: 1/ 0 Consensus: 0 State:efe75e Belief:a98ea8 Msg: connection + +---- + +then later + +[.small] +.Sample output +---- + +#2: Peer:1770c3 URL: localhost:43849 Status: J S Connections: 3/ 3 Consensus: 20 State:cfa8fe Belief:2c6f2a Msg: trusted connection +#4: Peer:0fce50 URL: localhost:46559 Status: J S Connections: 3/ 2 Consensus: 20 State:cfa8fe Belief:2c6f2a Msg: connection +#3: Peer:556deb URL: localhost:37985 Status: J S Connections: 3/ 3 Consensus: 20 State:cfa8fe Belief:2c6f2a Msg: trusted connection +#4: Peer:0fce50 URL: localhost:46559 Status: J S Connections: 3/ 3 Consensus: 20 State:cfa8fe Belief:2c6f2a Msg: trusted connection +---- + +On every event that occurs for a peer in the cluster, on it's own an event is shown as a line. + +The event data can be split up into the following fields: + +[cols="1,2a,1m"] +|=== +|Name |Description|Example + +|Index |Peer index starting at 1 within the cluster of peers |#4 +|Peer |First 6 characters of the public key of the peer |Peer:0fce50 +|URL |URL of the peer|URL: localhost:46559 +|Status +| +[horizontal] +NJ:: Not Joined +J:: Joined +NS:: Not Synced +S:: Synced +|Status: J S + +|Connections |_Peer connection count_ / _Peer trusted connection count_|Connections: 3/ 2 +|Consensus |Consensus level |Consensus: 20 +|State | First 6 characters of the State hash |State:cfa8fe +|Belief |First 6 characters of the Belief hash |Belief:2c6f2a +|Msg |Short message of the event that occured on this peer |Msg: trusted connection +|=== + +[#command-keys] +== Managing your Keys - The Keystore +How to manage the local public/private key pairs. + +When using any of the `key` sub commands, you do not need to be connected to any network. + +The option `--keystore` can be used with any sub command to specify which keystore to use. + + +[#command-key-generate] +=== Generating keypairs +How to generate a new set of public/private keys. + +You need to generate keypairs when: + +. Creating an account + +. Creating a new peer + +This command allows you to create 1+ keypairs in the keystore. + +So for example this will create 10 keypairs: + + ./convex key generate 10 --password=my-password + +[#command-key-list] +=== List keys +How to list the keys store in the keystore. + +To list out your keystore and view the public keys of each keypair. + + ./convex key list --password=my-password + + +[#command-key-export] +=== Exporting keys +How to export the keys from your keystore to encrypted text. + +You can export a keypair from the keystore to an encrypted PEM formated text. This is usefull if you need +to give another user, or application access to your network. + +You need to provide an `--export-password` option with the password of the encrypted PEM formated text. + +You also need to provide the location of the keypair you wish to export, this can be done using the `--index-key` or `--public-key` option. + +In this example first list out the keys from the keystore. + + ./convex key list --password=my-password + + 1 e7fdcb0bfdfb786b51eedf33b575.... + 2 373d2a583695ff367dd986e12785.... + .. + + +If we now want to export the key #2, then we can use the following command: + + ./convex key export --index-key=2 --export-password=my-password --password=my-password + +or a more safer way is to use the first hex of the public key + + ./convex key export --publi-key=373d2a583695ff --export-password=my-password --password=my-password + + +[WARNING] +==== +In this example we have used a insecure password of `my-password` to encrypt the exported key. We suggest that you use a better password when exporting your keys, and keep the exported PEM formated text secure. +==== + +[#command-key-import] +=== Importing keys +How to import keys into the keystore. + +You would need to import keys, when you want to run a peer or send a transaction for an account on another network. + +To import a keypair you need to set the options `--import-file` or `--import-file` and `--import-password`. + +So for example: + + ./convex key import --import-file=my_key.pem --import-password=my-password --password=my-password + +If the import password is successfull, this will import the keypair into the keystore, and show the public key of the imported keypair. + + +[#command-accounts] +== Managing Accounts +Information on how to create, fund and get information about the local accounts. + +This set of sub commands manage accounts on the local network. You need to have a local network running on the same computer for these commands to work. + +The reason is that the keystore needs to contain the keys for the first genesis accounts in the network. With the access to the genesis keypair, the account commands can create an account, and transfer sufficient funds to the new account. + + +[#command-account-create] +=== Create an account +How to create a local account. + +To create a new account and new keypair, you can just run: + + ./convex account create --password=my-password + +If you wish to use an already defined keypair in your keystore, you can set the `--index-key` or `--public-key` options. + + ./convex account create --public-key=eb1234 --password=my-password + + +The command returns the account address and public key used to create the account. + + +[#command-account-balance] +=== Get an accounts balance +How to get an account's balance. + +To obtain the balance of an account, you just need to provide an address of the account. + +So to run: + + ./convex account balance 45 + +Returns the balance for account #45 + + +[#command-account-fund] +=== Request funds for an account +How to request funds for an account. + +[#command-account-info] +=== Get information about an account +How to get information about an account. + +[#command-status] +== Status +How to get the local network status. + +[#command-query] +== Queries +How to execute queries on a local Convex network. + +[#command-transaction] +== Trancsactions +How to execute transactions on a local Convex network. + + + + + + + diff --git a/convex-cli/src/main/java/convex/cli/Account.java b/convex-cli/src/main/java/convex/cli/Account.java new file mode 100644 index 000000000..b7f1a2e6a --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/Account.java @@ -0,0 +1,38 @@ +package convex.cli; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.ParentCommand; + + +/** + * + * Convex account sub commands + * + * convex.account + * + */ +@Command(name="account", + aliases={"ac"}, + subcommands = { + AccountBalance.class, + AccountCreate.class, + AccountFund.class, + AccountInformation.class, + CommandLine.HelpCommand.class + }, + mixinStandardHelpOptions=true, + description="Manages convex accounts.") +public class Account implements Runnable { + + // private static final Logger log = Logger.getLogger(Account.class.getName()); + + @ParentCommand + protected Main mainParent; + + @Override + public void run() { + // sub command run with no command provided + CommandLine.usage(new Account(), System.out); + } +} diff --git a/convex-cli/src/main/java/convex/cli/AccountBalance.java b/convex-cli/src/main/java/convex/cli/AccountBalance.java new file mode 100644 index 000000000..485f49fc4 --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/AccountBalance.java @@ -0,0 +1,75 @@ +package convex.cli; + +import convex.api.Convex; +import convex.core.Result; +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.lang.Reader; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.ParentCommand; + +/** + * + * Convex account balance command + * + * convex.account.balance + * + */ + +@Command(name="balance", + aliases={"bal", "ba"}, + mixinStandardHelpOptions=true, + description="Get account balance.") +public class AccountBalance implements Runnable { + private static final Logger log = LoggerFactory.getLogger(AccountBalance.class); + + @ParentCommand + private Account accountParent; + + @Option(names={"--port"}, + description="Port number to connect to a peer.") + private int port = 0; + + @Option(names={"--host"}, + defaultValue=Constants.HOSTNAME_PEER, + description="Hostname to connect to a peer. Default: ${DEFAULT-VALUE}") + private String hostname; + + + @Parameters(paramLabel="address", + description="Address of the account to get the balance .") + private long addressNumber; + + @Option(names={"-t", "--timeout"}, + description="Timeout in miliseconds.") + private long timeout = Constants.DEFAULT_TIMEOUT_MILLIS; + + + @Override + public void run() { + + Main mainParent = accountParent.mainParent; + + if (addressNumber == 0) { + log.warn("You need to provide a valid address number"); + return; + } + + Convex convex = null; + Address address = Address.create(addressNumber); + try { + convex = mainParent.connectToSessionPeer(hostname, port, address, null); + String queryCommand = String.format("(balance #%d)", address.longValue()); + ACell message = Reader.read(queryCommand); + Result result = convex.querySync(message, timeout); + mainParent.output.setResult(result); + } catch (Throwable t) { + mainParent.showError(t); + } + } +} diff --git a/convex-cli/src/main/java/convex/cli/AccountCreate.java b/convex-cli/src/main/java/convex/cli/AccountCreate.java new file mode 100644 index 000000000..59141f4f5 --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/AccountCreate.java @@ -0,0 +1,117 @@ +package convex.cli; + +import java.util.List; + +import convex.api.Convex; +import convex.core.crypto.AKeyPair; +import convex.core.data.Address; +import convex.core.util.Utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParentCommand; + +/** + * + * Convex account create command + * + * convex.account.create + * + */ + +@Command(name="create", + aliases={"cr"}, + mixinStandardHelpOptions=true, + description="Creates an account using a public/private key from the keystore.%n" + + "You must provide a valid keystore password to the keystore.%n" + + "If the keystore is not at the default location also the keystore filename.") +public class AccountCreate implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(AccountCreate.class); + + @ParentCommand + private Account accountParent; + + @Option(names={"-i", "--index-key"}, + defaultValue="0", + description="Keystore index of the public/private key to use to create an account.") + private int keystoreIndex; + + @Option(names={"--public-key"}, + defaultValue="", + description="Hex string of the public key in the Keystore to use to create an account.%n" + + "You only need to enter in the first distinct hex values of the public key.%n" + + "For example: 0xf0234 or f0234") + private String keystorePublicKey; + + @Option(names={"--port"}, + description="Port number to connect to a peer.") + private int port = 0; + + @Option(names={"--host"}, + defaultValue=Constants.HOSTNAME_PEER, + description="Hostname to connect to a peer. Default: ${DEFAULT-VALUE}") + private String hostname; + + @Option(names={"-f", "--fund"}, + description="Fund the account with the default fund amount.") + private boolean isFund; + + @Override + public void run() { + + Main mainParent = accountParent.mainParent; + + AKeyPair keyPair = null; + + if (keystoreIndex > 0 || !keystorePublicKey.isEmpty()) { + try { + keyPair = mainParent.loadKeyFromStore(keystorePublicKey, keystoreIndex); + } catch (Error e) { + mainParent.showError(e); + return; + } + if (keyPair == null) { + log.warn("cannot find the provided public key"); + return; + } + } + if (keyPair == null) { + try { + List keyPairList = mainParent.generateKeyPairs(1); + keyPair = keyPairList.get(0); + mainParent.output.setField("Public Key", keyPair.getAccountKey().toHexString()); + } + catch (Error e) { + mainParent.showError(e); + return; + } + } + + Convex convex = null; + try { + + convex = mainParent.connectAsPeer(0); + + Address address = convex.createAccountSync(keyPair.getAccountKey()); + mainParent.output.setField("Address", address.longValue()); + if (isFund) { + convex.transferSync(address, Constants.ACCOUNT_FUND_AMOUNT); + convex = mainParent.connectToSessionPeer(hostname, port, address, keyPair); + Long balance = convex.getBalance(address); + mainParent.output.setField("Balance", balance); + } + mainParent.output.setField("Account usage", + String.format( + "to use this key can use the options --address=%d --public-key=%s", + address.toLong(), + Utils.toFriendlyHexString(keyPair.getAccountKey().toHexString(), 6) + ) + ); + } catch (Throwable t) { + mainParent.showError(t); + } + } +} diff --git a/convex-cli/src/main/java/convex/cli/AccountFund.java b/convex-cli/src/main/java/convex/cli/AccountFund.java new file mode 100644 index 000000000..13400cb42 --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/AccountFund.java @@ -0,0 +1,96 @@ +package convex.cli; + +import convex.api.Convex; +import convex.core.crypto.AKeyPair; +import convex.core.data.Address; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.ParentCommand; + +/** + * + * Convex account fund command + * + * convex.account.fund + * + */ + +@Command(name="fund", + aliases={"fu"}, + mixinStandardHelpOptions=true, + description="Transfers funds to account using a public/private key from the keystore.%n" + + "You must provide a valid keystore password to the keystore and a valid address.%n" + + "If the keystore is not at the default location also the keystore filename.") +public class AccountFund implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(AccountFund.class); + + @ParentCommand + private Account accountParent; + + @Option(names={"-i", "--index"}, + defaultValue="-1", + description="Keystore index of the public/private key to use to create an account.") + private int keystoreIndex; + + @Option(names={"--public-key"}, + defaultValue="", + description="Hex string of the public key in the Keystore to use to create an account.%n" + + "You only need to enter in the first distinct hex values of the public key.%n" + + "For example: 0xf0234 or f0234") + private String keystorePublicKey; + + @Option(names={"--port"}, + description="Port number to connect to a peer.") + private int port = 0; + + @Option(names={"--host"}, + defaultValue=Constants.HOSTNAME_PEER, + description="Hostname to connect to a peer. Default: ${DEFAULT-VALUE}") + private String hostname; + + @Option(names={"-a", "--address"}, + description="Account address to use to request funds.") + private long addressNumber; + + + @Parameters(paramLabel="amount", + defaultValue=""+Constants.ACCOUNT_FUND_AMOUNT, + description="Amount to fund the account") + private long amount; + + @Override + public void run() { + + Main mainParent = accountParent.mainParent; + + AKeyPair keyPair = null; + try { + keyPair = mainParent.loadKeyFromStore(keystorePublicKey, keystoreIndex); + } catch (Error e) { + mainParent.showError(e); + return; + } + + if (addressNumber == 0) { + log.warn("--address. You need to provide a valid address number"); + return; + } + + Convex convex = null; + Address address = Address.create(addressNumber); + try { + convex = mainParent.connectAsPeer(0); + convex.transferSync(address, amount); + convex = mainParent.connectToSessionPeer(hostname, port, address, keyPair); + Long balance = convex.getBalance(address); + mainParent.output.setField("Balance", balance); + } catch (Throwable t) { + mainParent.showError(t); + } + } +} diff --git a/convex-cli/src/main/java/convex/cli/AccountInformation.java b/convex-cli/src/main/java/convex/cli/AccountInformation.java new file mode 100644 index 000000000..0ac08f9e5 --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/AccountInformation.java @@ -0,0 +1,77 @@ +package convex.cli; + +import convex.api.Convex; +import convex.core.Result; +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.lang.Reader; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.ParentCommand; + +/** + * + * Convex account infomation command + * + * convex.account.infomation + * + */ + +@Command(name="information", + aliases={"info", "in"}, + mixinStandardHelpOptions=true, + description="Get account information.") +public class AccountInformation implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(AccountInformation.class); + + @ParentCommand + private Account accountParent; + + @Option(names={"--port"}, + description="Port number to connect to a peer.") + private int port = 0; + + @Option(names={"--host"}, + defaultValue=Constants.HOSTNAME_PEER, + description="Hostname to connect to a peer. Default: ${DEFAULT-VALUE}") + private String hostname; + + + @Parameters(paramLabel="address", + description="Address of the account to get information.") + private long addressNumber; + + @Option(names={"-t", "--timeout"}, + description="Timeout in miliseconds.") + private long timeout = Constants.DEFAULT_TIMEOUT_MILLIS; + + + @Override + public void run() { + + Main mainParent = accountParent.mainParent; + + if (addressNumber == 0) { + log.warn("You need to provide a valid address number"); + return; + } + + Convex convex = null; + Address address = Address.create(addressNumber); + try { + convex = mainParent.connectToSessionPeer(hostname, port, address, null); + String queryCommand = String.format("(account #%d)", address.longValue()); + ACell message = Reader.read(queryCommand); + Result result = convex.querySync(message, timeout); + mainParent.output.setResult(result); + } catch (Throwable t) { + mainParent.showError(t); + } + + } +} diff --git a/convex-cli/src/main/java/convex/cli/Constants.java b/convex-cli/src/main/java/convex/cli/Constants.java new file mode 100644 index 000000000..640a574e1 --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/Constants.java @@ -0,0 +1,27 @@ +package convex.cli; + + +/** +* +* Static constants for the CLI interface +* +*/ +public class Constants { + + public static final String HOSTNAME_REMOTE = "convex.world"; + + public static final String HOSTNAME_PEER = "localhost"; + + public static final String KEYSTORE_FILENAME = "~/.convex/keystore.pfx"; + + public static final int KEY_GENERATE_COUNT = 1; + + public static final String SESSION_FILENAME = "~/.convex/session.conf"; + + public static final int ACCOUNT_FUND_AMOUNT = 100000000; + + public static final int LOCAL_START_PEER_COUNT = 4; + + public static final long DEFAULT_TIMEOUT_MILLIS = 5000; + +} diff --git a/convex-cli/src/main/java/convex/cli/Helpers.java b/convex-cli/src/main/java/convex/cli/Helpers.java new file mode 100644 index 000000000..60b7f88b8 --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/Helpers.java @@ -0,0 +1,114 @@ +package convex.cli; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import convex.cli.peer.Session; +import convex.cli.peer.SessionItem; + +/** + * + * Helpers + * + * Helper functions for the CLI classes. + * +*/ +public class Helpers { + + /** + * Expand a path string with a '~'. The tilde is expanded to the users home path. + * + * @param path Path string to expand. + * + * @return Expanded string if a tilde is present. + * + */ + public static String expandTilde(String path) { + if (path!=null) { + return path.replaceFirst("^~", System.getProperty("user.home")); + } + return null; + } + + /** + * Create a path from a File object. This is to provide a feature to add the + * default `.convex` folder if it does not exist. + * + * @param file File object to see if the path part of the filename exists, if not then create it. + * + */ + public static void createPath(File file) { + File path = file.getParentFile(); + if (!path.exists()) { + path.mkdir(); + } + } + + /** + * Return a random session hostname, by looking at the session file. + * The session file has a list of local peers open. + * This helper will find a random peer in the collection and returns hostname. + * + * @param sessionFilename Session filename to open and get the random port nummber. + * + * @return A random hostname or null if none can be found + * @throws IOException + * + */ + public static SessionItem getSessionItem(String sessionFilename) throws IOException { + return getSessionItem(sessionFilename, -1); + } + + /** + * Return an indexed session item, by looking at the session file. + * The session file has a list of local peers open. + * This helper will find a random peer in the collection and returns session item. + * + * @param sessionFilename Session filename to open and get the random port nummber. + * + * @param index The index of the peer in the session list or if -1 a random selection is made. + * + * @return A random session item or null if none can be found + * @throws IOException + * + */ + public static SessionItem getSessionItem(String sessionFilename, int index) throws IOException { + SessionItem item = null; + Session session = new Session(); + Random random = new Random(); + File sessionFile = new File(sessionFilename); + session.load(sessionFile); + int sessionCount = session.getSize(); + if (sessionCount > 0) { + if (index < 0) { + index = random.nextInt(sessionCount - 1); + } + item = session.getItemFromIndex(index); + } + return item; + } + + public static List splitArrayParameter(String[] parameterValue) { + List result = new ArrayList<>(parameterValue.length); + for (int index = 0; index < parameterValue.length; index ++) { + String value = parameterValue[index]; + String[] items = new String[1]; + items[0] = value; + if (value.indexOf(",") > 0) { + items = value.split(","); + } + for (int itemIndex = 0; itemIndex < items.length; itemIndex ++ ) { + String newValue = items[itemIndex].trim(); + if (newValue.length() > 0) { + result.add(newValue); + } + } + } + return result; + } +} + + diff --git a/convex-cli/src/main/java/convex/cli/Key.java b/convex-cli/src/main/java/convex/cli/Key.java new file mode 100644 index 000000000..081a9127b --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/Key.java @@ -0,0 +1,36 @@ +package convex.cli; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.ParentCommand; + +/** + * + * Convex key sub commands + * + * convex.key + * + */ +@Command(name="key", + aliases={"ke"}, + subcommands = { + KeyImport.class, + KeyGenerate.class, + KeyList.class, + KeyExport.class, + CommandLine.HelpCommand.class + }, + mixinStandardHelpOptions=true, + description="Manage local Convex key store.") +public class Key implements Runnable { + + @ParentCommand + protected Main mainParent; + + @Override + public void run() { + // sub command run with no command provided + CommandLine.usage(new Key(), System.out); + } +} + diff --git a/convex-cli/src/main/java/convex/cli/KeyExport.java b/convex-cli/src/main/java/convex/cli/KeyExport.java new file mode 100644 index 000000000..761feaf12 --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/KeyExport.java @@ -0,0 +1,98 @@ +package convex.cli; + + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.ParentCommand; +import picocli.CommandLine.Option; + +import convex.core.crypto.AKeyPair; +import convex.core.crypto.PEMTools; + + +/** + * + * Convex key sub commands + * + * convex.key.export + * + * + */ +@Command(name="export", + aliases={"ex"}, + mixinStandardHelpOptions=true, + description="Export 1 or more key pairs from the keystore.") +public class KeyExport implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(KeyExport.class); + + @ParentCommand + protected Key keyParent; + + @Option(names={"-i", "--index-key"}, + description="Keystore index of the public/private key to use for the peer.") + + private int[] keystoreIndex; + + @Option(names = {"--public-key" }, + description = "Hex string of the public key in the Keystore to use for the peer.%n" + + "You only need to enter in the first distinct hex values of the public key.%n" + + "For example: 0xf0234 or f0234") + private String[] keystorePublicKey ; + + @Option(names={"--export-password"}, + description="Password of the exported key.") + private String exportPassword; + + + @Override + public void run() { + // sub command to generate keys + Main mainParent = keyParent.mainParent; + + if (keystoreIndex == null && keystorePublicKey == null) { + log.warn("You need to provide at least on --index-key or --public-key parameter"); + return; + } + + if (exportPassword == null || exportPassword.length() == 0) { + log.warn("You need to provide an export password '--export-password' of the exported key"); + return; + } + + try { + int index = 0; + int count = 0; + if (keystoreIndex != null) { + count = keystoreIndex.length; + } + if (keystorePublicKey != null ) { + count = keystorePublicKey.length; + } + while (index < count) { + String publicKey = null; + int indexKey = 0; + if (keystoreIndex != null) { + indexKey = keystoreIndex[index]; + } + if (keystorePublicKey != null) { + publicKey = keystorePublicKey[index]; + } + AKeyPair keyPair = mainParent.loadKeyFromStore(publicKey, indexKey); + String pemText = PEMTools.encryptPrivateKeyToPEM(keyPair.getPrivate(), exportPassword.toCharArray()); + + mainParent.output.setField("index", String.format("%5d", index + 1)); + mainParent.output.setField("publicKey", keyPair.getAccountKey().toHexString()); + mainParent.output.setField("export", pemText); + mainParent.output.addRow(); + + index ++; + } + + } catch (Error e) { + mainParent.showError(e); + } + } +} diff --git a/convex-cli/src/main/java/convex/cli/KeyGenerate.java b/convex-cli/src/main/java/convex/cli/KeyGenerate.java new file mode 100644 index 000000000..7ff709767 --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/KeyGenerate.java @@ -0,0 +1,62 @@ +package convex.cli; + +import java.util.List; + +import convex.core.crypto.AKeyPair; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.ParentCommand; + + +/** + * + * Convex key sub commands + * + * convex.key.generate + * + * + */ +@Command(name="generate", + aliases={"ge"}, + mixinStandardHelpOptions=true, + description="Generate 1 or more private key pairs.") +public class KeyGenerate implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(KeyGenerate.class); + + @ParentCommand + protected Key keyParent; + + + @Parameters(paramLabel="count", + defaultValue="" + Constants.KEY_GENERATE_COUNT, + description="Number of keys to generate. Default: ${DEFAULT-VALUE}") + private int count; + + @Override + public void run() { + // sub command to generate keys + Main mainParent = keyParent.mainParent; + // check the number of keys to generate. + if (count <= 0) { + log.warn("You to provide 1 or more count of keys to generate"); + return; + } + log.info("Generating {} keys",count); + + try { + List keyPairList = mainParent.generateKeyPairs(count); + for ( int index = 0; index < keyPairList.size(); index ++) { + String publicKeyHexString = keyPairList.get(index).getAccountKey().toHexString(); + mainParent.output.setField("Index", String.format("%5d", index)); + mainParent.output.setField("Public Key", publicKeyHexString); + mainParent.output.addRow(); + } + } catch (Error e) { + mainParent.showError(e); + } + } +} diff --git a/convex-cli/src/main/java/convex/cli/KeyImport.java b/convex-cli/src/main/java/convex/cli/KeyImport.java new file mode 100644 index 000000000..4d36f11d4 --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/KeyImport.java @@ -0,0 +1,83 @@ +package convex.cli; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.ParentCommand; +import picocli.CommandLine.Option; + +import convex.core.crypto.AKeyPair; +import convex.core.crypto.Ed25519KeyPair; +import convex.core.crypto.PEMTools; + + +/** + * + * Convex key sub commands + * + * convex.key.import + * + * + */ +@Command(name="import", + aliases={"im"}, + mixinStandardHelpOptions=true, + description="Import key pairs to the keystore.") +public class KeyImport implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(KeyImport.class); + + @ParentCommand + protected Key keyParent; + + @Option(names={"-i", "--import-text"}, + description="Import format PEM text of the keypair.") + private String importText; + + + @Option(names={"-f", "--import-file"}, + description="Import file name of the keypair PEM file.") + private String importFilename; + + @Option(names={"--import-password"}, + description="Password of the imported key.") + private String importPassword; + + @Override + public void run() { + // sub command to generate keys + Main mainParent = keyParent.mainParent; + if (importFilename != null && importFilename.length() > 0) { + try { + importText = Files.readString(Paths.get(importFilename), StandardCharsets.UTF_8); + } catch ( IOException e) { + mainParent.showError(e); + return; + } + } + if (importText == null || importText.length() == 0) { + log.warn("You need to provide an import text '--import' or import filename '--import-file' to import a private key"); + return; + } + + if (importPassword == null || importPassword.length() == 0) { + log.warn("You need to provide an import password '--import-password' of the imported encrypted PEM data"); + } + + try { + PrivateKey privateKey = PEMTools.decryptPrivateKeyFromPEM(importText, importPassword.toCharArray()); + AKeyPair keyPair = Ed25519KeyPair.create(privateKey); + mainParent.addKeyPairToStore(keyPair); + mainParent.output.setField("public key", keyPair.getAccountKey().toHexString()); + + } catch (Error e) { + mainParent.showError(e); + } + } +} diff --git a/convex-cli/src/main/java/convex/cli/KeyList.java b/convex-cli/src/main/java/convex/cli/KeyList.java new file mode 100644 index 000000000..07b2ddb7d --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/KeyList.java @@ -0,0 +1,63 @@ +package convex.cli; + +import java.io.File; +import java.security.KeyStore; +import java.util.Enumeration; + +import convex.core.crypto.PFXTools; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.ParentCommand; + +/** + * + * Convex key sub commands + * + * convex.key.list + * + * + */ +@Command(name="list", + aliases={"li"}, + mixinStandardHelpOptions=true, + description="List available key pairs.") +public class KeyList implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(KeyList.class); + + @ParentCommand + protected Key keyParent; + + @Override + public void run() { + Main mainParent = keyParent.mainParent; + + String password = mainParent.getPassword(); + if (password == null) { + log.warn("You need to provide a keystore password"); + return; + } + File keyFile = new File(mainParent.getKeyStoreFilename()); + try { + if (!keyFile.exists()) { + log.error("Cannot find keystore file {}", keyFile.getCanonicalPath()); + } + KeyStore keyStore = PFXTools.loadStore(keyFile, password); + Enumeration aliases = keyStore.aliases(); + int index = 1; + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + mainParent.output.setField("Index", String.format("%5d", index)); + mainParent.output.setField("Public Key", alias); + mainParent.output.addRow(); + index ++; + } + + } catch (Throwable t) { + mainParent.showError(t); + } + } + +} diff --git a/convex-cli/src/main/java/convex/cli/Local.java b/convex-cli/src/main/java/convex/cli/Local.java new file mode 100644 index 000000000..f1c293dda --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/Local.java @@ -0,0 +1,35 @@ +package convex.cli; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.ParentCommand; + + +/** + * + * Convex local sub commands + * + * convex.local + * + * + */ +@Command(name="local", + aliases={"lo"}, + subcommands = { + LocalGUI.class, + LocalStart.class, + CommandLine.HelpCommand.class + }, + mixinStandardHelpOptions=true, + description="Operates a local convex network.") +public class Local implements Runnable { + + @ParentCommand + protected Main mainParent; + + @Override + public void run() { + // sub command run with no command provided + CommandLine.usage(new Local(), System.out); + } +} diff --git a/convex-cli/src/main/java/convex/cli/LocalGUI.java b/convex-cli/src/main/java/convex/cli/LocalGUI.java new file mode 100644 index 000000000..031bc52ee --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/LocalGUI.java @@ -0,0 +1,41 @@ +package convex.cli; + +import convex.api.Applications; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.ParentCommand; + +/** + * + * Convex Local Manager sub command + * + * convex.local.manager + * + * + */ +@Command(name="gui", + aliases={}, + mixinStandardHelpOptions=true, + description="Starts a local convex test network using the peer manager GUI application.") +public class LocalGUI implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(LocalGUI.class); + + @ParentCommand + protected Local localParent; + + @Override + public void run() { + Main mainParent = localParent.mainParent; + + log.warn("You will not be able to use some of the CLI 'account' and 'peer' commands."); + // sub command to launch peer manager + try { + Applications.launchApp(convex.gui.manager.PeerGUI.class); + } catch (Throwable t) { + mainParent.showError(t); + } + } +} diff --git a/convex-cli/src/main/java/convex/cli/LocalStart.java b/convex-cli/src/main/java/convex/cli/LocalStart.java new file mode 100644 index 000000000..80974b8bb --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/LocalStart.java @@ -0,0 +1,129 @@ +package convex.cli; + +import java.lang.NumberFormatException; +import java.util.ArrayList; +import java.util.List; + +import convex.cli.peer.PeerManager; +import convex.core.crypto.AKeyPair; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParentCommand; + +/* + * local start command + * + * convex.local.start + * + */ + +@Command(name="start", + aliases={"st"}, + mixinStandardHelpOptions=true, + description="Starts a local convex test network.") +public class LocalStart implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(LocalStart.class); + + @ParentCommand + private Local localParent; + + @Option(names={"--count"}, + defaultValue = "" + Constants.LOCAL_START_PEER_COUNT, + description="Number of local peers to start. Default: ${DEFAULT-VALUE}") + private int count; + + @Option(names={"-i", "--index-key"}, + defaultValue="0", + description="One or more keystore index of the public/private key to use to run a peer.") + private String[] keystoreIndex; + + @Option(names={"--public-key"}, + defaultValue="", + description="One or more hex string of the public key in the Keystore to use to run a peer.%n" + + "You only need to enter in the first distinct hex values of the public key.%n" + + "For example: 0xf0234 or f0234") + private String[] keystorePublicKey; + + @Option(names={"--ports"}, + description="Range or list of ports to assign each peer in the cluster. This can be a multiple of --ports %n" + + "or a single --ports=8081,8082,8083 or --ports=8080-8090") + private String[] ports; + + @Override + public void run() { + Main mainParent = localParent.mainParent; + PeerManager peerManager = PeerManager.create(mainParent.getSessionFilename()); + + List keyPairList = new ArrayList(); + + // load in the list of public keys to use as peers + if (keystorePublicKey.length > 0) { + List values = Helpers.splitArrayParameter(keystorePublicKey); + for (int index = 0; index < values.size(); index ++) { + String publicKeyText = values.get(index); + try { + AKeyPair keyPair = mainParent.loadKeyFromStore(publicKeyText, 0); + if (keyPair != null) { + keyPairList.add(keyPair); + } + } catch (Error e) { + mainParent.showError(e); + return; + } + } + } + + // load in a list of key indexes to use as peers + if (keystoreIndex.length > 0) { + List values = Helpers.splitArrayParameter(keystoreIndex); + for (int index = 0; index < values.size(); index ++) { + int indexKey = Integer.parseInt(values.get(index)); + if (indexKey > 0) { + try { + AKeyPair keyPair = mainParent.loadKeyFromStore("", indexKey); + if (keyPair != null) { + keyPairList.add(keyPair); + } + } catch (Error e) { + mainParent.showError(e); + return; + } + } + } + } + + if (keyPairList.size() == 0) { + keyPairList = mainParent.generateKeyPairs(count); + } + + if (count > keyPairList.size()) { + log.error( + "Not enougth public keys provided. " + + "You have requested {} peers to start, but only provided {} public keys", + count, + keyPairList.size() + ); + } + int peerPorts[] = null; + if (ports != null) { + try { + peerPorts = mainParent.getPortList(ports, count); + } catch (NumberFormatException e) { + log.warn("cannot convert port number " + e); + return; + } + if (peerPorts.length < count) { + log.warn("you need only provided {} ports you need to provide at least {} ports", peerPorts.length, count); + return; + } + } + log.info("Starting local network with "+count+" peer(s)"); + peerManager.launchLocalPeers(keyPairList, peerPorts); + log.info("Local Peers launched"); + peerManager.showPeerEvents(); + } +} diff --git a/convex-cli/src/main/java/convex/cli/Main.java b/convex-cli/src/main/java/convex/cli/Main.java new file mode 100644 index 000000000..7b55726fa --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/Main.java @@ -0,0 +1,389 @@ +package convex.cli; + +import java.io.File; +import java.lang.NumberFormatException; +import java.net.InetSocketAddress; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +import convex.api.Convex; +import convex.cli.peer.SessionItem; +import convex.cli.output.Output; +import convex.core.crypto.AKeyPair; +import convex.core.crypto.PFXTools; +import convex.core.data.AccountKey; +import convex.core.data.Address; +import convex.core.init.Init; + + +import ch.qos.logback.classic.Level; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.PropertiesDefaultProvider; +import picocli.CommandLine.ScopeType; + +/** +* Convex CLI implementation +*/ +@Command(name="convex", + subcommands = { + Account.class, + Key.class, + Local.class, + Peer.class, + Query.class, + Status.class, + Transaction.class, + CommandLine.HelpCommand.class + }, + mixinStandardHelpOptions=true, + usageHelpAutoWidth=true, + sortOptions = false, + // headerHeading = "Usage:", + // synopsisHeading = "%n", + descriptionHeading = "%nDescription:%n%n", + parameterListHeading = "%nParameters:%n", + optionListHeading = "%nOptions:%n", + commandListHeading = "%nCommands:%n", + description="Convex Command Line Interface") + +public class Main implements Runnable { + + private static Logger log = LoggerFactory.getLogger(Main.class); + + + private static CommandLine commandLine; + public Output output; + + @Option(names={ "-c", "--config"}, + scope = ScopeType.INHERIT, + description="Use the specified config file.%n All parameters to this app can be set by removing the leading '--', and adding" + + " a leading 'convex.'.%n So to set the keystore filename you can write 'convex.keystore=my_keystore_filename.dat'%n" + + "To set a sub command such as `./convex peer start index=4` index parameter you need to write 'convex.peer.start.index=4'") + private String configFilename; + + @Option(names={"-e", "--etch"}, + scope = ScopeType.INHERIT, + description="Convex state storage filename. The default is to use a temporary storage filename.") + private String etchStoreFilename; + + @Option(names={"-k", "--keystore"}, + defaultValue=Constants.KEYSTORE_FILENAME, + scope = ScopeType.INHERIT, + description="keystore filename. Default: ${DEFAULT-VALUE}") + private String keyStoreFilename; + + @Option(names={"-p", "--password"}, + scope = ScopeType.INHERIT, + //defaultValue="", + description="Password to read/write to the Keystore") + private String password; + + @Option(names={"-s", "--session"}, + defaultValue=Constants.SESSION_FILENAME, + scope = ScopeType.INHERIT, + description="Session filename. Defaults ${DEFAULT-VALUE}") + private String sessionFilename; + + @Option(names={ "-v", "--verbose"}, + scope = ScopeType.INHERIT, + description="Show more verbose log information. You can increase verbosity by using multiple -v or -vvv") + private boolean[] verbose = new boolean[0]; + + + public Main() { + output = new Output(); + } + + @Override + public void run() { + // no command provided - so show help + CommandLine.usage(new Main(), System.out); + } + + public static void main(String[] args) { + Main mainApp = new Main(); + int result = mainApp.execute(args); + System.exit(result); + } + + public int execute(String[] args) { + commandLine = new CommandLine(this) + .setUsageHelpLongOptionsMaxWidth(40) + .setUsageHelpWidth(40 * 4); + + // do a pre-parse to get the config filename. We need to load + // in the defaults before running the full execute + try { + commandLine.parseArgs(args); + loadConfig(); + } catch (Throwable t) { + System.err.println("unable to parse arguments " + t); + } + + ch.qos.logback.classic.Logger parentLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + + Level[] verboseLevels = {Level.WARN, Level.INFO, Level.DEBUG, Level.TRACE, Level.ALL}; + + parentLogger.setLevel(Level.WARN); + if (verbose.length > 0 && verbose.length <= verboseLevels.length) { + parentLogger.setLevel(verboseLevels[verbose.length]); + log.info("set level to {}", parentLogger.getLevel()); + } + + int result = 0; + try { + result = commandLine.execute(args); + output.writeToStream(commandLine.getOut()); + + } catch (Throwable t) { + log.error("Error executing command line: {}",t.getMessage()); + return 2; + } + return result; + } + + protected void loadConfig() { + if (configFilename != null && !configFilename.isEmpty()) { + String filename = Helpers.expandTilde(configFilename); + File configFile = new File(filename); + if (configFile.exists()) { + PropertiesDefaultProvider defaultProvider = new PropertiesDefaultProvider(configFile); + commandLine.setDefaultValueProvider(defaultProvider); + } + } + } + + public String getSessionFilename() { + if (sessionFilename != null) { + return Helpers.expandTilde(sessionFilename.strip()); + } + return null; + } + + public String getPassword() { + return password; + } + + public String getKeyStoreFilename() { + if ( keyStoreFilename != null) { + return Helpers.expandTilde(keyStoreFilename).strip(); + } + return null; + } + + public String getEtchStoreFilename() { + if ( etchStoreFilename != null) { + return Helpers.expandTilde(etchStoreFilename).strip(); + } + return null; + } + + public KeyStore loadKeyStore(boolean isCreate) throws Error { + KeyStore keyStore = null; + if (password == null || password.isEmpty()) { + throw new Error("You need to provide a keystore password"); + } + File keyFile = new File(getKeyStoreFilename()); + try { + if (keyFile.exists()) { + keyStore = PFXTools.loadStore(keyFile, password); + } + else { + if (isCreate) { + Helpers.createPath(keyFile); + keyStore = PFXTools.createStore(keyFile, password); + } + else { + throw new Error("Cannot find keystore file "+keyFile.getCanonicalPath()); + } + } + } catch(Throwable t) { + new Error(t); + } + return keyStore; + } + + public AKeyPair loadKeyFromStore(String publicKey, int indexKey) throws Error { + + AKeyPair keyPair = null; + + String publicKeyClean = ""; + if (publicKey != null) { + publicKeyClean = publicKey.toLowerCase().replaceAll("^0x", "").strip(); + } + + if ( publicKeyClean.isEmpty() && indexKey <= 0) { + return null; + } + + String searchText = publicKeyClean; + if (indexKey > 0) { + searchText += " " + indexKey; + } + if (password == null || password.isEmpty()) { + throw new Error("You need to provide a keystore password"); + } + + + File keyFile = new File(getKeyStoreFilename()); + try { + if (!keyFile.exists()) { + throw new Error("Cannot find keystore file "+keyFile.getCanonicalPath()); + } + KeyStore keyStore = PFXTools.loadStore(keyFile, password); + + int counter = 1; + Enumeration aliases = keyStore.aliases(); + + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + if (counter == indexKey || alias.indexOf(publicKeyClean) == 0) { + log.trace("found keypair " + indexKey + " " + counter + " " + alias + " " + publicKeyClean + " " + alias.indexOf(publicKeyClean)); + keyPair = PFXTools.getKeyPair(keyStore, alias, password); + break; + } + counter ++; + } + } catch (Throwable t) { + throw new Error("Cannot load key store "+t); + } + + if (keyPair==null) { + throw new Error("Cannot find key in keystore '" + searchText + "'"); + } + return keyPair; + } + + public Convex connectToSessionPeer(String hostname, int port, Address address, AKeyPair keyPair) throws Error { + SessionItem item; + Convex convex = null; + try { + if (port == 0) { + item = Helpers.getSessionItem(getSessionFilename()); + if (item != null) { + port = item.getPort(); + } + } + if (port == 0) { + throw new Error("Cannot find a local port or you have not set a valid port number"); + } + InetSocketAddress host=new InetSocketAddress(hostname.strip(), port); + convex = Convex.connect(host, address, keyPair); + } catch (Throwable t) { + throw new Error("Cannot connect to a local peer " + t); + } + return convex; + } + + public Convex connectAsPeer(int peerIndex) throws Error { + Convex convex = null; + try { + SessionItem item = Helpers.getSessionItem(getSessionFilename(), peerIndex); + AccountKey peerKey = item.getAccountKey(); + log.debug("peer public key {}", peerKey.toHexString()); + AKeyPair keyPair = loadKeyFromStore(peerKey.toHexString(), 0); + log.debug("peer key pair {}", keyPair.getAccountKey().toHexString()); + Address address = Init.getGenesisPeerAddress(peerIndex); + log.debug("peer address {}", address); + InetSocketAddress host = item.getHostAddress(); + log.debug("connect to peer {}", host); + convex = Convex.connect(host, address, keyPair); + } catch (Throwable t) { + throw new Error("Cannot connect as a peer " + t); + } + return convex; + } + + public List generateKeyPairs(int count) throws Error { + List keyPairList = new ArrayList<>(count); + + // generate `count` keys + for (int index = 0; index < count; index ++) { + AKeyPair keyPair = AKeyPair.generate(); + keyPairList.add(keyPair); + addKeyPairToStore(keyPair); + } + + return keyPairList; + } + + public void addKeyPairToStore(AKeyPair keyPair) { + // get the password of the key store file + String password = getPassword(); + if (password == null) { + throw new Error("You need to provide a keystore password"); + } + // get the key store file + File keyFile = new File(getKeyStoreFilename()); + + KeyStore keyStore = null; + try { + // try to load the keystore file + if (keyFile.exists()) { + keyStore = PFXTools.loadStore(keyFile, password); + } else { + // create the path to the new key file + Helpers.createPath(keyFile); + keyStore = PFXTools.createStore(keyFile, password); + } + } catch (Throwable t) { + throw new Error("Cannot load key store: "+t); + } + try { + // save the key in the keystore + PFXTools.setKeyPair(keyStore, keyPair, password); + } catch (Throwable t) { + throw new Error("Cannot store the key to the key store "+t); + } + // save the keystore file + try { + PFXTools.saveStore(keyStore, keyFile, password); + } catch (Throwable t) { + throw new Error("Cannot save the key store file "+t); + } + } + + int[] getPortList(String ports[], int count) throws NumberFormatException { + Pattern rangePattern = Pattern.compile(("([0-9]+)\\s*-\\s*([0-9]*)")); + List portTextList = Helpers.splitArrayParameter(ports); + List portList = new ArrayList(); + int countLeft = count; + for (int index = 0; index < portTextList.size() && countLeft > 0; index ++) { + String item = portTextList.get(index); + Matcher matcher = rangePattern.matcher(item); + if (matcher.matches()) { + int portFrom = Integer.parseInt(matcher.group(1)); + int portTo = portFrom + count + 1; + if (!matcher.group(2).isEmpty()) { + portTo = Integer.parseInt(matcher.group(2)); + } + for ( int portIndex = portFrom; portIndex <= portTo && countLeft > 0; portIndex ++, --countLeft ) { + portList.add(portIndex); + } + } + else if (item.strip().length() == 0) { + } + else { + portList.add(Integer.parseInt(item)); + countLeft --; + } + } + return portList.stream().mapToInt(Integer::intValue).toArray(); + } + + void showError(Throwable t) { + log.error(t.getMessage()); + if (verbose.length > 0) { + t.printStackTrace(); + } + } +} diff --git a/convex-cli/src/main/java/convex/cli/Peer.java b/convex-cli/src/main/java/convex/cli/Peer.java new file mode 100644 index 000000000..fe54efaa7 --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/Peer.java @@ -0,0 +1,37 @@ +package convex.cli; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.ParentCommand; + + +/** + * + * Convex peer sub commands + * + * convex.peer + * + */ +@Command(name="peer", + aliases={"pe"}, + subcommands = { + PeerCreate.class, + PeerStart.class, + CommandLine.HelpCommand.class + }, + mixinStandardHelpOptions=true, + description="Operates a local peer.") +public class Peer implements Runnable { + + // private static final Logger log = Logger.getLogger(Peer.class.getName()); + + @ParentCommand + protected Main mainParent; + + @Override + public void run() { + // sub command run with no command provided + CommandLine.usage(new Peer(), System.out); + } + +} diff --git a/convex-cli/src/main/java/convex/cli/PeerCreate.java b/convex-cli/src/main/java/convex/cli/PeerCreate.java new file mode 100644 index 000000000..10cadfbcc --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/PeerCreate.java @@ -0,0 +1,146 @@ +package convex.cli; + +import java.io.File; +import java.security.KeyStore; + +import convex.api.Convex; +import convex.core.crypto.AKeyPair; +import convex.core.crypto.PFXTools; +import convex.core.data.Address; +import convex.core.data.ACell; +import convex.core.lang.Reader; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Invoke; +import convex.core.Result; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParentCommand; +import picocli.CommandLine.Spec; + +/** + * peer create command + * + * convex.peer.create + * + * This creates an account and provides enougth funds, for a new peer account + * + * + */ + +@Command(name="create", + aliases={"cr"}, + mixinStandardHelpOptions=true, + description="Creates a keypair, new account and a funding stake: to run a local peer.") +public class PeerCreate implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(PeerCreate.class); + + @ParentCommand + private Peer peerParent; + + @Spec CommandSpec spec; + + @Option(names={"-i", "--index-key"}, + defaultValue="0", + description="Keystore index of the public/private key to use for the peer.") + private int keystoreIndex; + + @Option(names={"--public-key"}, + defaultValue="", + description="Hex string of the public key in the Keystore to use for the peer.%n" + + "You only need to enter in the first distinct hex values of the public key.%n" + + "For example: 0xf0234 or f0234") + private String keystorePublicKey; + + @Option(names={"--port"}, + description="Port number of nearest peer to connect too.") + private int port = 0; + + @Option(names={"--host"}, + defaultValue=Constants.HOSTNAME_PEER, + description="Hostname to connect to a peer. Default: ${DEFAULT-VALUE}") + private String hostname; + + @Option(names={"-t", "--timeout"}, + description="Timeout in miliseconds.") + private long timeout = Constants.DEFAULT_TIMEOUT_MILLIS; + + + @Override + public void run() { + + Main mainParent = peerParent.mainParent; + + int port = 0; + long peerStake = 10000000000L; + + AKeyPair keyPair = null; + KeyStore keyStore; + + try { + // create a keystore if it does not exist + keyStore = mainParent.loadKeyStore(true); + } catch (Error e) { + log.info(e.getMessage()); + return; + } + + try { + keyPair = AKeyPair.generate(); + + // save the new keypair in the keystore + PFXTools.setKeyPair(keyStore, keyPair, mainParent.getPassword()); + + File keyFile = new File(mainParent.getKeyStoreFilename()); + + // save the store to a file + PFXTools.saveStore(keyStore, keyFile, mainParent.getPassword()); + + // connect using the default first user + Convex convex = mainParent.connectAsPeer(0); + // create an account + Address address = convex.createAccountSync(keyPair.getAccountKey()); + convex.transferSync(address, peerStake); + + convex = mainParent.connectToSessionPeer(hostname, port, address, keyPair); + long stakeBalance = convex.getBalance(address); + String accountKeyString = keyPair.getAccountKey().toHexString(); + long stakeAmount = (long) (stakeBalance * 0.98); + + String transactionCommand = String.format("(create-peer 0x%s %d)", accountKeyString, stakeAmount); + ACell message = Reader.read(transactionCommand); + ATransaction transaction = Invoke.create(address, -1, message); + Result result = convex.transactSync(transaction, timeout); + if (result.isError()) { + mainParent.output.setResult(result); + return; + } + long currentBalance = convex.getBalance(address); + + mainParent.output.setField("Public Peer Key", keyPair.getAccountKey().toString()); + mainParent.output.setField("Address", address.longValue()); + mainParent.output.setField("Balance", currentBalance); + mainParent.output.setField("Inital stake amount", stakeAmount); + String shortAccountKey = accountKeyString.substring(0, 6); + // System.out.println("You can now start this peer by executing the following line:\n"); + + // WARNING not sure about showing the users password.. + // to make the starting of peers easier, I have left it in for a simple copy/paste + + mainParent.output.setField("Peer start line", + String.format( + "./convex peer start --password=%s --address=%d --public-key=%s", + mainParent.getPassword(), + address.toLong(), + shortAccountKey + ) + ); + } catch (Throwable t) { + mainParent.showError(t); + } + } +} diff --git a/convex-cli/src/main/java/convex/cli/PeerStart.java b/convex-cli/src/main/java/convex/cli/PeerStart.java new file mode 100644 index 000000000..b7935b889 --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/PeerStart.java @@ -0,0 +1,142 @@ +package convex.cli; + +import java.io.File; +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import convex.cli.peer.PeerManager; +import convex.cli.peer.SessionItem; +import convex.core.crypto.AKeyPair; +import convex.core.data.Address; +import convex.core.store.AStore; +import convex.core.store.Stores; +import etch.EtchStore; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParentCommand; +import picocli.CommandLine.Spec; + +/** + * peer start command + * + * convex.peer.start + * + */ + +@Command(name = "start", aliases = { "st" }, mixinStandardHelpOptions = true, description = "Starts a local peer.") +public class PeerStart implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(PeerStart.class); + + @ParentCommand + private Peer peerParent; + + @Spec + CommandSpec spec; + + @Option(names={"-i", "--index-key"}, + defaultValue="0", + description="Keystore index of the public/private key to use for the peer.") + + private int keystoreIndex; + + @Option(names = { + "--public-key" }, defaultValue = "", description = "Hex string of the public key in the Keystore to use for the peer.%n" + + "You only need to enter in the first distinct hex values of the public key.%n" + + "For example: 0xf0234 or f0234") + private String keystorePublicKey; + + @Option(names = { "-r", + "--reset" }, description = "Reset and delete the etch database if it exists. Default: ${DEFAULT-VALUE}") + private boolean isReset; + + @Option(names = { "--port" }, description = "Port number of this local peer.") + private int port = 0; + + @Option(names = { + "--host" }, defaultValue=Constants.HOSTNAME_PEER, description = "Hostname of this peer. Default: ${DEFAULT-VALUE}") + private String hostname = Constants.HOSTNAME_PEER; + + @Option(names = { + "--peer" }, description = "Hostname and port number of remote peer. If not provided then try to connect to a local peer") + private String remotePeerHostname; + + @Option(names = { "-a", "--address" }, description = "Account address to use for the peer.") + private long addressNumber; + + @Override + public void run() { + + Main mainParent = peerParent.mainParent; + PeerManager peerManager = null; + + AKeyPair keyPair = null; + try { + keyPair = mainParent.loadKeyFromStore(keystorePublicKey, keystoreIndex); + } catch (Error e) { + mainParent.showError(e); + return; + } + + if (keyPair == null) { + log.warn("cannot load a valid key pair to perform peer start"); + return; + } + + if (port != 0) { + port = Math.abs(port); + } + if ( addressNumber == 0) { + log.warn("please provide an account address to run the peer from."); + return; + } + Address peerAddress = Address.create(addressNumber); + + if (remotePeerHostname == null) { + try { + SessionItem item = Helpers.getSessionItem(mainParent.getSessionFilename()); + if (item != null) { + remotePeerHostname = item.getHostname(); + } + else { + log.warn("Cannot find a local peer to connect too"); + return; + } + } catch (IOException e) { + log.warn("Cannot load the session control file"); + return; + } + } + else { + remotePeerHostname = remotePeerHostname.strip(); + } + if (hostname == null) { + log.warn("you need to provide a host name for this peer"); + return; + } + hostname = hostname.strip(); + + try { + AStore store = null; + String etchStoreFilename = mainParent.getEtchStoreFilename(); + if (etchStoreFilename != null && !etchStoreFilename.isEmpty()) { + File etchFile = new File(etchStoreFilename); + if (isReset && etchFile.exists()) { + log.info("reset: removing old etch storage file {}", etchStoreFilename); + etchFile.delete(); + } + store = EtchStore.create(etchFile); + } else { + store = Stores.getGlobalStore(); + } + peerManager = PeerManager.create(mainParent.getSessionFilename(), keyPair, peerAddress, store); + peerManager.launchPeer(hostname, port, remotePeerHostname); + peerManager.showPeerEvents(); + } catch (Throwable t) { + mainParent.showError(t); + } + } +} diff --git a/convex-cli/src/main/java/convex/cli/Query.java b/convex-cli/src/main/java/convex/cli/Query.java new file mode 100644 index 000000000..6832122b4 --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/Query.java @@ -0,0 +1,82 @@ +package convex.cli; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +import convex.api.Convex; +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.lang.Reader; +import convex.core.Result; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.ParentCommand; + +/** + * + * Convex Query sub command + * + * convex.query + * + */ +@Command(name="query", + aliases={"qu"}, + mixinStandardHelpOptions=true, + description="Execute a query on the current peer.") +public class Query implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(Query.class); + + @ParentCommand + protected Main mainParent; + + + @Option(names={"--port"}, + description="Port number to connect to a peer.") + private int port = 0; + + @Option(names={"--host"}, + defaultValue=Constants.HOSTNAME_PEER, + description="Hostname to connect to a peer. Default: ${DEFAULT-VALUE}") + private String hostname; + + @Option(names={"-t", "--timeout"}, + description="Timeout in miliseconds.") + private long timeout = Constants.DEFAULT_TIMEOUT_MILLIS; + + @Option(names={"-a", "--address"}, + description = "Address to make the query from. Default: First peer address.") + private long address = 11; + + @Parameters(paramLabel="queryCommand", description="Query Command") + private String queryCommand; + + + @Override + public void run() { + // sub command run with no command provided + log.info("query command: {}", queryCommand); + + Convex convex = null; + + try { + convex = mainParent.connectToSessionPeer(hostname, port, Address.create(address), null); + } catch (Error e) { + mainParent.showError(e); + return; + } + try { + log.info("Executing query: %s\n", queryCommand); + ACell message = Reader.read(queryCommand); + Result result = convex.querySync(message, timeout); + mainParent.output.setResult(result); + } catch (IOException | TimeoutException e) { + mainParent.showError(e); + } + } + +} diff --git a/convex-cli/src/main/java/convex/cli/Status.java b/convex-cli/src/main/java/convex/cli/Status.java new file mode 100644 index 000000000..b5b046a9a --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/Status.java @@ -0,0 +1,108 @@ +package convex.cli; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import convex.api.Convex; +import convex.cli.peer.SessionItem; +import convex.core.Result; +import convex.core.State; +import convex.core.data.ABlob; +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.AccountStatus; +import convex.core.data.BlobMap; +import convex.core.data.Hash; +import convex.core.data.PeerStatus; +import convex.core.store.Stores; +import convex.core.util.Text; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParentCommand; + +/** + * + * Convex Status sub command + * + * convex.status + * + */ +@Command(name="status", + aliases={"st"}, + mixinStandardHelpOptions=true, + description="Reports on the current status of the network.") +public class Status implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(Status.class); + + @ParentCommand + protected Main mainParent; + + @Option(names={"--port"}, + description="Port number to connect or create a peer.") + private int port = 0; + + @Option(names={"--host"}, + defaultValue=Constants.HOSTNAME_PEER, + description="Hostname to connect to a peer. Default: ${DEFAULT-VALUE}") + private String hostname; + + @Option(names={"-t", "--timeout"}, + description="Timeout in miliseconds.") + private long timeout = Constants.DEFAULT_TIMEOUT_MILLIS; + + @SuppressWarnings("unchecked") + @Override + public void run() { + + if (port == 0) { + try { + SessionItem item = Helpers.getSessionItem(mainParent.getSessionFilename()); + port = item.getPort(); + } catch (IOException e) { + log.warn("Cannot load the session control file"); + } + } + if (port == 0) { + log.warn("Cannot find a local port or you have not set a valid port number"); + return; + } + + Convex convex = null; + try { + convex = mainParent.connectAsPeer(0); + } catch (Throwable t) { + mainParent.showError(t); + return; + } + + try { + Result result = convex.requestStatus().get(timeout, TimeUnit.MILLISECONDS); + AVector resultVector = (AVector) result.getValue(); + ABlob stateHash = (ABlob) resultVector.get(1); + Hash hash = Hash.wrap(stateHash.getBytes()); + AVector stateWrapper = (AVector) convex.acquire(hash, Stores.current()).get(3000,TimeUnit.MILLISECONDS); + State state = (State) stateWrapper.get(0); + + state.validate(); + AVector accountList = state.getAccounts(); + BlobMap peerList = state.getPeers(); + + mainParent.output.setField("State hash", stateHash.toString()); + mainParent.output.setField("Timestamp",state.getTimeStamp().toString()); + mainParent.output.setField("Timestamp value", Text.dateFormat(state.getTimeStamp().longValue())); + mainParent.output.setField("Global Fees", Text.toFriendlyBalance(state.getGlobalFees().longValue())); + mainParent.output.setField("Juice Price", Text.toFriendlyBalance(state.getJuicePrice().longValue())); + mainParent.output.setField("Total Funds", Text.toFriendlyBalance(state.computeTotalFunds())); + mainParent.output.setField("Number of accounts", accountList.size()); + mainParent.output.setField("Number of peers", peerList.size()); + } catch (Throwable t) { + mainParent.showError(t); + } + } + +} diff --git a/convex-cli/src/main/java/convex/cli/Transaction.java b/convex-cli/src/main/java/convex/cli/Transaction.java new file mode 100644 index 000000000..40365d8d0 --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/Transaction.java @@ -0,0 +1,107 @@ +package convex.cli; + +import convex.api.Convex; +import convex.core.crypto.AKeyPair; +import convex.core.data.Address; +import convex.core.data.ACell; +import convex.core.lang.Reader; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Invoke; +import convex.core.Result; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.ParentCommand; + +/** + * + * Convex Transaction sub command + * + * convex.transaction + * + */ +@Command(name="transaction", + aliases={"transact", "tr"}, + mixinStandardHelpOptions=true, + description="Execute a transaction on the network via a peer.") +public class Transaction implements Runnable { + + @ParentCommand + protected Main mainParent; + + private static final Logger log = LoggerFactory.getLogger(Transaction.class); + + @Option(names={"-i", "--index-key"}, + defaultValue="0", + description="Keystore index of the public/private key to use to run the transaction.") + private int keystoreIndex; + + @Option(names={"--public-key"}, + defaultValue="", + description="Hex string of the public key in the Keystore to use to run the transaction.%n" + + "You only need to enter in the first distinct hex values of the public key.%n" + + "For example: 0xf0234 or f0234") + private String keystorePublicKey; + + @Option(names={"--port"}, + description="Port number to connect or create a peer.") + private int port = 0; + + @Option(names={"--host"}, + defaultValue=Constants.HOSTNAME_PEER, + description="Hostname to connect to a peer. Default: ${DEFAULT-VALUE}") + private String hostname; + + @Option(names={"-a", "--address"}, + description="Account address to use for the transaction request.") + private long addressNumber; + + @Option(names={"-t", "--timeout"}, + description="Timeout in miliseconds.") + private long timeout = Constants.DEFAULT_TIMEOUT_MILLIS; + + @Parameters(paramLabel="transactionCommand", + description="Transaction Command") + private String transactionCommand; + + @Override + public void run() { + + AKeyPair keyPair = null; + try { + keyPair = mainParent.loadKeyFromStore(keystorePublicKey, keystoreIndex); + } catch (Error e) { + mainParent.showError(e); + return; + } + + if (keyPair == null) { + log.warn("cannot load a valid key pair to perform this transaction"); + return; + } + + if (addressNumber == 0) { + log.warn("--address. You need to provide a valid address number"); + return; + } + + Address address = Address.create(addressNumber); + + Convex convex = null; + try { + convex = mainParent.connectToSessionPeer(hostname, port, address, keyPair); + log.info("Executing transaction: %s\n", transactionCommand); + ACell message = Reader.read(transactionCommand); + ATransaction transaction = Invoke.create(address, -1, message); + + Result result = convex.transactSync(transaction, timeout); + mainParent.output.setResult(result); + } catch (Throwable t) { + mainParent.showError(t); + } + } + +} diff --git a/convex-cli/src/main/java/convex/cli/output/Output.java b/convex-cli/src/main/java/convex/cli/output/Output.java new file mode 100644 index 000000000..9046ba3d2 --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/output/Output.java @@ -0,0 +1,70 @@ +package convex.cli.output; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import convex.core.data.ACell; +import convex.core.Result; + +/* + * Output class to show results from the CLI + * + */ + + + +public class Output { + + protected List fieldList = new ArrayList(); + + protected List> rowList = new ArrayList>(); + + public OutputField setField(String name, long value) { + return setField(name, String.valueOf(value)); + } + + public OutputField setField(String name, ACell value) { + return setField(name, value.toString()); + } + + public OutputField setField(String name, String value) { + OutputField field = OutputField.create(name, value); + fieldList.add(field); + return field; + } + + public void setResult(Result result) { + ACell value = result.getValue(); + setField("Result", value); + if (result.isError()) { + setField("Error code", result.getErrorCode()); + if (result.getTrace() != null) { + setField("Trace", result.getTrace()); + } + return; + } + setField("Data type", value.getType().toString()); + } + + public void addRow() { + rowList.add(fieldList); + fieldList = new ArrayList(); + } + + public void writeToStream(PrintWriter out) { + if (rowList.size() > 0) { + List firstRow = rowList.get(0); + out.println(firstRow.stream().map(OutputField::getDescription).collect(Collectors.joining(" "))); + for ( ListfieldList : rowList) { + out.println(fieldList.stream().map(OutputField::getValue).collect(Collectors.joining(" "))); + } + } + else { + for (OutputField field : fieldList) { + out.println(String.format("%s: %s", field.getDescription(), field.getValue())); + } + } + } +} diff --git a/convex-cli/src/main/java/convex/cli/output/OutputField.java b/convex-cli/src/main/java/convex/cli/output/OutputField.java new file mode 100644 index 000000000..9330c5c49 --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/output/OutputField.java @@ -0,0 +1,25 @@ +package convex.cli.output; + + +public class OutputField { + + protected String description; + protected String value; + + private OutputField(String description, String value) { + this.description = description; + this.value = value; + } + + public static OutputField create(String description, String value) { + return new OutputField(description, value); + } + + public String getDescription() { + return description; + } + + public String getValue() { + return value; + } +} diff --git a/convex-cli/src/main/java/convex/cli/peer/PeerManager.java b/convex-cli/src/main/java/convex/cli/peer/PeerManager.java new file mode 100644 index 000000000..e4c46da9c --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/peer/PeerManager.java @@ -0,0 +1,349 @@ +package convex.cli.peer; + +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.stream.Collectors; + +import convex.api.Convex; +import convex.core.util.Shutdown; +import convex.cli.Helpers; +import convex.core.Belief; +import convex.core.Result; +import convex.core.State; +import convex.core.crypto.AKeyPair; +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.Address; +import convex.core.data.Hash; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.SignedData; +import convex.core.init.Init; +import convex.core.lang.RT; +import convex.core.store.AStore; +import convex.core.util.Utils; +import convex.peer.API; +import convex.peer.IServerEvent; +import convex.peer.Server; +import convex.peer.ServerEvent; +import convex.peer.ServerInformation; +import etch.EtchStore; + + +/** +* +* Convex CLI PeerManager +* +*/ + +public class PeerManager implements IServerEvent { + + private static final Logger log = LoggerFactory.getLogger(PeerManager.class.getName()); + + private static final long TRANSACTION_TIMEOUT_MILLIS = 50000; + private static final int FRIENDLY_HEX_STRING_SIZE = 6; + + protected List peerServerList = new ArrayList(); + + protected Session session = new Session(); + + protected String sessionFilename; + + protected AKeyPair keyPair; + + protected Address address; + + protected AStore store; + + protected BlockingQueue serverEventQueue = new ArrayBlockingQueue(1024); + + + private PeerManager(String sessionFilename, AKeyPair keyPair, Address address, AStore store) { + this.sessionFilename = sessionFilename; + this.keyPair = keyPair; + this.address = address; + this.store = store; + } + + public static PeerManager create(String sessionFilename) { + return new PeerManager(sessionFilename, null, null, null); + } + + public static PeerManager create(String sessionFilename, AKeyPair keyPair, Address address, AStore store) { + return new PeerManager(sessionFilename, keyPair, address, store); + } + + public void launchLocalPeers(List keyPairList, int peerPorts[]) { + List keyList=keyPairList.stream().map(kp->kp.getAccountKey()).collect(Collectors.toList()); + + State genesisState=Init.createState(keyList); + peerServerList = API.launchLocalPeers(keyPairList,genesisState, peerPorts, this); + } + + public List getNetworkHashList(String remotePeerHostname) { + InetSocketAddress remotePeerAddress = Utils.toInetSocketAddress(remotePeerHostname); + int retryCount = 5; + Convex convex = null; + Result result = null; + while (retryCount > 0) { + try { + convex = Convex.connect(remotePeerAddress, address, keyPair); + Future cf = convex.requestStatus(); + result = cf.get(TRANSACTION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + retryCount = 0; + } catch (IOException | InterruptedException | ExecutionException | TimeoutException e ) { + // raiseServerMessage("unable to connect to remote peer at " + remoteHostname + ". Retrying " + e); + retryCount --; + } + } + if ((convex==null)||(result == null)) { + throw new Error("Failed to join network: Cannot connect to remote peer at "+remotePeerHostname); + } + convex.close(); + List hashList = new ArrayList(5); + AVector values = result.getValue(); + hashList.add(RT.ensureHash(values.get(0))); // beliefHash + hashList.add(RT.ensureHash(values.get(1))); // stateHash + hashList.add(RT.ensureHash(values.get(2))); // netwokIdHash + hashList.add(RT.ensureHash(values.get(4))); // consensusHash + + return hashList; + } + + public State aquireState(String remotePeerHostname, Hash stateHash) { + InetSocketAddress remotePeerAddress = Utils.toInetSocketAddress(remotePeerHostname); + Convex convex = null; + State state = null; + try { + convex = Convex.connect(remotePeerAddress, address, keyPair); + Future bf = convex.acquire(stateHash, store); + state = bf.get(TRANSACTION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + convex.close(); + } catch (IOException | InterruptedException | ExecutionException | TimeoutException e) { + throw new Error("cannot aquire network state: " + e); + } + return state; + + } + public SignedData aquireBelief(String remotePeerHostname, Hash beliefHash) { + // sync the etch db with the network state + Convex convex = null; + SignedData signedBelief = null; + InetSocketAddress remotePeerAddress = Utils.toInetSocketAddress(remotePeerHostname); + try { + convex = Convex.connect(remotePeerAddress, address, keyPair); + Future> cf = convex.acquire(beliefHash, store); + signedBelief = cf.get(TRANSACTION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + convex.close(); + } catch (IOException | InterruptedException | ExecutionException | TimeoutException e) { + throw new Error("cannot acquire belief: " + e); + } + + return signedBelief; + } + + public void launchPeer( + String hostname, + int port, + String remotePeerHostname + ) { + Map config = new HashMap<>(); + if (port > 0 ) { + config.put(Keywords.PORT, port); + } + config.put(Keywords.STORE, store); + config.put(Keywords.SOURCE, remotePeerHostname); + config.put(Keywords.KEYPAIR, keyPair); + config.put(Keywords.EVENT_HOOK, this); // Add this as IServerEvent hook + Server server = API.launchPeer(config); + + peerServerList.add(server); + } + + /** + * Load in a session from a session file. + * + * @param sessionFilename Filename to load. + * + */ + protected void loadSession() { + File sessionFile = new File(sessionFilename); + try { + session.load(sessionFile); + } catch (IOException e) { + log.error("Cannot load the session control file"); + } + } + + /** + * Add a peer to the session list of peers. + * + * @param peerServer Add the peerServer to the list of peers for this session. + * + */ + protected void addToSession(Server peerServer) { + EtchStore store = (EtchStore) peerServer.getStore(); + + session.addPeer( + peerServer.getPeerKey(), + peerServer.getHostname(), + store.getFileName() + ); + } + + /** + * Add all peers started in this session to the session list. + * + */ + protected void addAllToSession() { + for (Server peerServer: peerServerList) { + addToSession(peerServer); + } + } + + /** + * Remove all peers added by this manager from the session list of peers. + * + */ + protected void removeAllFromSession() { + for (Server peerServer: peerServerList) { + session.removePeer(peerServer.getPeerKey()); + } + } + + /** + * Store the session details to file. + * + * @param sessionFilename Fileneame to save the session. + * + */ + protected void storeSession() { + File sessionFile = new File(sessionFilename); + try { + Helpers.createPath(sessionFile); + if (session.getSize() > 0) { + session.store(sessionFile); + } + else { + sessionFile.delete(); + } + } catch (IOException e) { + log.error("Cannot store the session control data"); + } + } + + /** + * Once the manager has launched 1 or more peers. The manager now needs too loop and show any events generated by the peers + * + */ + public void showPeerEvents() { + + loadSession(); + addAllToSession(); + storeSession(); + + /* + Go through each started peer server connection and make sure + that each peer is connected to the other peer. + */ + /* + for (Server peerServer: peerServerList) { + connectToPeers(peerServer, session.getPeerAddressList()); + } + */ + + // shutdown hook to remove/update the session file + Shutdown.addHook(Shutdown.CLI,new Runnable() { + public void run() { + // System.out.println("peers stopping"); + // remove session file + loadSession(); + removeAllFromSession(); + storeSession(); + } + }); + + Server firstServer = peerServerList.get(0); + System.out.println("Starting network Id: "+ firstServer.getPeer().getNetworkID().toString()); + while (true) { + try { + ServerEvent event = serverEventQueue.take(); + ServerInformation information = event.getInformation(); + int index = getServerIndex(information.getPeerKey()); + if (index >=0) { + String item = toServerInformationText(information); + System.out.println(String.format("#%d: %s Msg: %s", index + 1, item, event.getReason())); + } + } catch (InterruptedException e) { + System.out.println("Peer manager interrupted!"); + return; + } + } + } + + protected String toServerInformationText(ServerInformation serverInformation) { + String shortName = Utils.toFriendlyHexString(serverInformation.getPeerKey().toHexString(), FRIENDLY_HEX_STRING_SIZE); + String hostname = serverInformation.getHostname(); + String joined = "NJ"; + String synced = "NS"; + if (serverInformation.isJoined()) { + joined = " J"; + } + if (serverInformation.isSynced()) { + synced = " S"; + } + String stateHash = Utils.toFriendlyHexString(serverInformation.getStateHash().toHexString(), FRIENDLY_HEX_STRING_SIZE); + String beliefHash = Utils.toFriendlyHexString(serverInformation.getBeliefHash().toHexString(), FRIENDLY_HEX_STRING_SIZE); + int connectionCount = serverInformation.getConnectionCount(); + int trustedConnectionCount = serverInformation.getTrustedConnectionCount(); + long consensusPoint = serverInformation.getConsensusPoint(); + String item = String.format("Peer:%s URL: %s Status:%s %s Connections:%2d/%2d Consensus:%4d State:%s Belief:%s", + shortName, + hostname, + joined, + synced, + connectionCount, + trustedConnectionCount, + consensusPoint, + stateHash, + beliefHash + ); + + return item; + } + + protected int getServerIndex(AccountKey peerKey) { + for (int index = 0; index < peerServerList.size(); index ++) { + Server server = peerServerList.get(index); + if (server.getPeer().getPeerKey().equals(peerKey)) { + return index; + } + } + return -1; + } + + /** + * Implements for IServerEvent + * + */ + + public void onServerChange(ServerEvent serverEvent) { + // add in queue if space available + serverEventQueue.offer(serverEvent); + } +} diff --git a/convex-cli/src/main/java/convex/cli/peer/Session.java b/convex-cli/src/main/java/convex/cli/peer/Session.java new file mode 100644 index 000000000..1dd76aff7 --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/peer/Session.java @@ -0,0 +1,147 @@ +package convex.cli.peer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import convex.core.data.AccountKey; + +public class Session { + + + protected List items = new ArrayList(); + /** + * Load a session data from file. + * + * @param filename Filename of the session file to load. + * + * @throws IOException + */ + public void load(File filename) throws IOException { + items.clear(); + if (filename.exists()) { + FileInputStream stream = new FileInputStream(filename); + Properties values = new Properties(); + values.load(stream); + for (String name: values.stringPropertyNames()) { + String line = values.getProperty(name, ""); + SessionItem item = SessionItem.createFromString(line); + items.add(item); + } + } + } + + /** + * Add a peer to the list of peers kept in the session data. + * + * @param accountKey Public key of Peer + * @param hostname Hostname of the peer. This includes the port number. + * @param etchFilename Filename that the peer is using to store the peer's state. + * + */ + public void addPeer(AccountKey accountKey, String hostname, String etchFilename){ + SessionItem item = SessionItem.create(accountKey, hostname, etchFilename); + items.add(item); + } + + /** + * Remove a peer from the list of peers held by this session. + * + * @param accountKey Address of the peer, this is the public key used by the peer. + * + */ + public void removePeer(AccountKey accountKey) { + for (SessionItem item: items) { + if (item.getAccountKey().equals(accountKey)) { + items.remove(item); + return; + } + } + } + + /** + * Store the session list to a file. + * + * @param filename Filename to save the session too. + * @throws IOException if the file data cannot be writtern. + * + */ + public void store(File filename) throws IOException { + FileOutputStream stream = new FileOutputStream(filename); + Properties values = new Properties(); + int index = 0; + for (SessionItem item: items) { + values.setProperty(String.valueOf(index), item.toString()); + index ++; + } + values.store(stream, "Convex Session"); + } + + /** + * Return the number of session items added to this session. + * + * @return number of items found for this session. + * + */ + public int getSize() { + return items.size(); + } + + /** + * Return true of false if the peer name exists in the list of peers for this session + * + * @param accountKey Public Key of the peer to check to see if it exists. + * @return true if the peer name exists, flase otherwise + */ + public boolean isPeer(AccountKey accountKey) { + SessionItem item = getItemFromAccountKey(accountKey); + return item != null; + } + + /** + * Return a session item based on the peer index. + * + * @param index The index of the peer in the list, starting from 0. + * @return Session Item if the item is found at the index, if not then return null. + */ + public SessionItem getItemFromIndex(int index) { + return items.get(index); + } + + /** + * Get a session item based on the peers AccountKey. + * + * @param accountKey AccountKey of the peer to get the session item for. + * + * @return SessionItem object or null if the peer account key cannot be found + * + */ + public SessionItem getItemFromAccountKey(AccountKey accountKey) { + for (SessionItem item: items) { + if (item.getAccountKey().equals(accountKey)) { + return item; + } + } + return null; + } + + /** + * Get a list of peer hostnames. + * + * @return List hostname item for each stored peer. + * + */ + public String[] getPeerHostnameList() { + String[] result = new String[items.size()]; + int index = 0; + for (SessionItem item: items) { + result[index] = item.getHostname(); + index ++; + } + return result; + } +} diff --git a/convex-cli/src/main/java/convex/cli/peer/SessionItem.java b/convex-cli/src/main/java/convex/cli/peer/SessionItem.java new file mode 100644 index 000000000..7f522967b --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/peer/SessionItem.java @@ -0,0 +1,106 @@ +package convex.cli.peer; + +import java.net.InetSocketAddress; + +import convex.core.data.AccountKey; +import convex.core.util.Utils; + +public class SessionItem { + + protected static final String DELIMITER = ","; + + protected AccountKey accountKey; + protected String hostname; + protected String etchFilename; + + /** + * Creates a new SessionItem object + */ + private SessionItem(AccountKey accountKey, String hostname, String etchFilename) { + this.accountKey = accountKey; + this.hostname = hostname; + this.etchFilename = etchFilename; + } + + /** + * Create a new SessionItem object using the following fields: + * + * @param accountKey AccountKey of the peer. + * + * @param hostname Hostname and port of the peer. + * + * @param etchFilename Etch filename the peer is using. + * + * @return a new SessionItem object. + * + */ + public static SessionItem create(AccountKey accountKey, String hostname, String etchFilename) { + return new SessionItem(accountKey, hostname, etchFilename); + } + + /** + * Create a new SessionItem from a comma delimited string. + * + * @param value String that contain the session item data. + * + * @return a new SessionItem object. + * + */ + public static SessionItem createFromString(String value) { + String[] values = value.split(DELIMITER); + return create(AccountKey.fromChecksumHex(values[0]), values[1], values[2]); + } + + /** + * @return the peer AccountKey. + * + */ + public AccountKey getAccountKey() { + return accountKey; + } + + /** + * @return the string name to use for this session item. + * + */ + public String getName() { + return accountKey.toChecksumHex(); + } + + /** + * @return the peers hostname and port number. + * + */ + public String getHostname() { + return hostname; + } + + public int getPort() { + InetSocketAddress address = Utils.toInetSocketAddress(hostname); + return address.getPort(); + } + + public InetSocketAddress getHostAddress() { + return Utils.toInetSocketAddress(hostname); + } + + /** + * @return the used Etch Filename for this peer. + * + */ + public String getEtchFilename() { + return etchFilename; + } + + /** + * @return the encoded data for this session as a commar delimited string. + * + */ + public String toString() { + String[] values = new String[3]; + values[0] = accountKey.toChecksumHex(); + values[1] = hostname; + values[2] = etchFilename; + return String.join(DELIMITER, values); + } +} diff --git a/convex-cli/src/test/java/convex/cli/CLICommandKeyExportTest.java b/convex-cli/src/test/java/convex/cli/CLICommandKeyExportTest.java new file mode 100644 index 000000000..cdd5ce4e7 --- /dev/null +++ b/convex-cli/src/test/java/convex/cli/CLICommandKeyExportTest.java @@ -0,0 +1,68 @@ +package convex.cli; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +public class CLICommandKeyExportTest { + + private static final String KEYSTORE_FILENAME = "/tmp/tempKeystore.dat"; + private static final String KEYSTORE_PASSWORD = "testPassword"; + private static final String EXPORT_PASSWORD = "testExportPassword"; + + @Test + public void testKeyGenerateList() { + + // command key.generate + CommandLineTester tester = new CommandLineTester( + "key", "generate", + "--password", KEYSTORE_PASSWORD, + "--keystore", KEYSTORE_FILENAME + ); + tester.assertOutputMatch("^Index Public Key\\s+0"); + String publicKey = tester.getField("0 "); + assertFalse(publicKey.isEmpty()); + publicKey = publicKey.stripLeading(); + + File fp = new File(KEYSTORE_FILENAME); + assertTrue(fp.exists()); + + // command key.export index + tester = new CommandLineTester( + "key", + "export", + "--password", KEYSTORE_PASSWORD, + "--keystore", KEYSTORE_FILENAME, + "--index-key", "1", + "--export-password", EXPORT_PASSWORD + ); + tester.assertOutputMatch("ENCRYPTED PRIVATE KEY"); + + // command key.export publicKey + tester = new CommandLineTester( + "key", + "export", + "--password", KEYSTORE_PASSWORD, + "--keystore", KEYSTORE_FILENAME, + "--public-key", publicKey, + "--export-password", EXPORT_PASSWORD + ); + tester.assertOutputMatch("ENCRYPTED PRIVATE KEY"); + + // command key.export publicKey with leading 0x + tester = new CommandLineTester( + "key", + "export", + "--password", KEYSTORE_PASSWORD, + "--keystore", KEYSTORE_FILENAME, + "--public-key", "0x" + publicKey, + "--export-password", EXPORT_PASSWORD + ); + tester.assertOutputMatch("ENCRYPTED PRIVATE KEY"); + + + } +} + diff --git a/convex-cli/src/test/java/convex/cli/CLICommandKeyImportTest.java b/convex-cli/src/test/java/convex/cli/CLICommandKeyImportTest.java new file mode 100644 index 000000000..0e323fac4 --- /dev/null +++ b/convex-cli/src/test/java/convex/cli/CLICommandKeyImportTest.java @@ -0,0 +1,34 @@ +package convex.cli; + +import org.junit.jupiter.api.Test; + +import convex.core.crypto.AKeyPair; +import convex.core.crypto.Ed25519KeyPair; +import convex.core.crypto.PEMTools; + +public class CLICommandKeyImportTest { + + private static final String KEYSTORE_FILENAME = "/tmp/tempKeystore.dat"; + private static final String KEYSTORE_PASSWORD = "testPassword"; + private static final String IMPORT_PASSWORD = "testImportPassword"; + + @Test + public void testKeyImport() { + + AKeyPair keyPair = Ed25519KeyPair.generate(); + String pemText = PEMTools.encryptPrivateKeyToPEM(keyPair.getPrivate(), IMPORT_PASSWORD.toCharArray()); + + // command key.list + CommandLineTester tester = new CommandLineTester( + "key", + "import", + "--password", KEYSTORE_PASSWORD, + "--keystore", KEYSTORE_FILENAME, + "--import-text", pemText, + "--import-password", IMPORT_PASSWORD + ); + + tester.assertOutputMatch("public key: " + keyPair.getAccountKey().toHexString()); + + } +} diff --git a/convex-cli/src/test/java/convex/cli/CLICommandKeyTest.java b/convex-cli/src/test/java/convex/cli/CLICommandKeyTest.java new file mode 100644 index 000000000..42821e377 --- /dev/null +++ b/convex-cli/src/test/java/convex/cli/CLICommandKeyTest.java @@ -0,0 +1,28 @@ +package convex.cli; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +public class CLICommandKeyTest { + + private static final String KEYSTORE_FILENAME = "/tmp/tempKeystore.dat"; + private static final String KEYSTORE_PASSWORD = "testPassword"; + + @Test + public void testKeyGenerateList() { + + // command key.generate + CommandLineTester tester = new CommandLineTester("key", "generate", "--password", KEYSTORE_PASSWORD, "--keystore", KEYSTORE_FILENAME); + tester.assertOutputMatch("^Index Public Key\\s+0"); + + File fp = new File(KEYSTORE_FILENAME); + assertTrue(fp.exists()); + + // command key.list + tester = new CommandLineTester("key", "list", "--password", KEYSTORE_PASSWORD, "--keystore", KEYSTORE_FILENAME); + tester.assertOutputMatch("^Index Public Key\\s+1"); + + } +} diff --git a/convex-cli/src/test/java/convex/cli/CLIHelpTest.java b/convex-cli/src/test/java/convex/cli/CLIHelpTest.java new file mode 100644 index 000000000..67682569b --- /dev/null +++ b/convex-cli/src/test/java/convex/cli/CLIHelpTest.java @@ -0,0 +1,33 @@ +package convex.cli; + +import org.junit.jupiter.api.Test; + +import static convex.cli.Helper.assertExecuteCommandLineResult; + +public class CLIHelpTest { + + @Test + public void testHelp() { + assertExecuteCommandLineResult(0, "^Usage: convex \\[-hVv\\]", "--help"); + assertExecuteCommandLineResult(0, "^Usage: convex \\[-hVv\\]", "-h"); + assertExecuteCommandLineResult(0, "^Usage: convex \\[-hVv\\]", "help"); + assertExecuteCommandLineResult(0, "^Usage: convex account \\[-hVv\\]", "account", "help"); + assertExecuteCommandLineResult(0, "^Usage: convex account balance \\[-hVv\\]", "account", "balance", "--help"); + assertExecuteCommandLineResult(0, "^Usage: convex account create \\[-fhVv\\]", "account", "create", "--help"); + assertExecuteCommandLineResult(0, "^Usage: convex account information \\[-hVv\\]", "account", "information", "--help"); + assertExecuteCommandLineResult(0, "^Usage: convex account fund \\[-hVv\\]", "account", "fund", "--help"); + assertExecuteCommandLineResult(0, "^Usage: convex key \\[-hVv\\]", "key", "help"); + assertExecuteCommandLineResult(0, "^Usage: convex key generate \\[-hVv\\]", "key", "generate", "--help"); + assertExecuteCommandLineResult(0, "^Usage: convex key list \\[-hVv\\]", "key", "list", "--help"); + assertExecuteCommandLineResult(0, "^Usage: convex peer \\[-hVv\\]", "peer", "help"); + assertExecuteCommandLineResult(0, "^Usage: convex peer start \\[-hrVv\\]", "peer", "start", "--help"); + assertExecuteCommandLineResult(0, "^Usage: convex local \\[-hVv\\]", "local", "--help"); + assertExecuteCommandLineResult(0, "^Usage: convex local start \\[-hVv\\]", "local", "start", "--help"); + assertExecuteCommandLineResult(0, "^Usage: convex local gui \\[-hVv\\]", "local", "gui", "--help"); + assertExecuteCommandLineResult(0, "^Usage: convex query \\[-hVv\\]", "query", "--help"); + assertExecuteCommandLineResult(0, "^Usage: convex status \\[-hVv\\]", "status", "--help"); + assertExecuteCommandLineResult(0, "^Usage: convex transaction \\[-hVv\\]", "transaction", "--help"); + assertExecuteCommandLineResult(0, "^Usage: convex transaction \\[-hVv\\]", "transact", "--help"); + } + +} diff --git a/convex-cli/src/test/java/convex/cli/CLIMainTest.java b/convex-cli/src/test/java/convex/cli/CLIMainTest.java new file mode 100644 index 000000000..4f99a3482 --- /dev/null +++ b/convex-cli/src/test/java/convex/cli/CLIMainTest.java @@ -0,0 +1,51 @@ +package convex.cli; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class CLIMainTest { + + @Test + public void testMainGetPortList() { + Main mainApp = new Main(); + String basicList[] = {"80", "90", "100", "101", "200"}; + int result[] = mainApp.getPortList(basicList, 4); + assertArrayEquals(new int[]{80, 90, 100, 101}, result); + + String commaList[] = {"80,90,100,101,200"}; + result = mainApp.getPortList(commaList, 4); + assertArrayEquals(new int[]{80, 90, 100, 101}, result); + + String closedRange[] = {"100-200"}; + result = mainApp.getPortList(closedRange, 4); + assertArrayEquals(new int[]{100, 101, 102, 103}, result); + + String openRange[] = {"100-"}; + result = mainApp.getPortList(openRange, 6); + assertArrayEquals(new int[]{100, 101, 102, 103, 104, 105}, result); + + String combinedClosedRange[] = {"80", "100-103", "200"}; + result = mainApp.getPortList(combinedClosedRange, 6); + assertArrayEquals(new int[]{80, 100, 101, 102, 103, 200}, result); + + String combinedOpenRange[] = {"80", "100-", "200", "300"}; + result = mainApp.getPortList(combinedOpenRange, 6); + assertArrayEquals(new int[]{80, 100, 101, 102, 103, 104}, result); + + String combinedCommaClosedRange[] = {"80,100-103,200"}; + result = mainApp.getPortList(combinedCommaClosedRange, 6); + assertArrayEquals(new int[]{80, 100, 101, 102, 103, 200}, result); + + String combinedCommaOpenRange[] = {"80,100-,200,300"}; + result = mainApp.getPortList(combinedCommaOpenRange, 6); + assertArrayEquals(new int[]{80, 100, 101, 102, 103, 104}, result); + + assertThrows(NumberFormatException.class, () -> { + String badNumberValue[] = {"80,100+,200,300"}; + mainApp.getPortList(badNumberValue, 6); + }); + } + +} diff --git a/convex-cli/src/test/java/convex/cli/CommandLineTester.java b/convex-cli/src/test/java/convex/cli/CommandLineTester.java new file mode 100644 index 000000000..3f1e5397c --- /dev/null +++ b/convex-cli/src/test/java/convex/cli/CommandLineTester.java @@ -0,0 +1,69 @@ + +package convex.cli; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import picocli.CommandLine; + + +public class CommandLineTester { + protected String output; + protected int result; + protected String args[]; + + public CommandLineTester(String ... args) { + this.args = args; + StringWriter outputWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(outputWriter); + Main mainApp = new Main(); + + CommandLine commandLine = new CommandLine(mainApp); + + commandLine.setOut(printWriter); + + this.result = commandLine.execute(args); + mainApp.output.writeToStream(printWriter); + this.output = new String(outputWriter.toString()); + } + + public void assertOutputMatch(String patternText) { + Pattern regex = Pattern.compile(patternText, Pattern.MULTILINE + Pattern.DOTALL); + Matcher matcher = regex.matcher(output); + + String assertText = "\nCommand: convex " + String.join(" ", args) + + "\nMatch: '" + patternText + "'" + + "\nOutput: '" + output.substring(0, Math.min(132, output.length())) + "'" + + "\n"; + assertEquals(true, matcher.find(), assertText); + } + + + public String getField(String name) { + String lines[] = output.split("\\r?\\n"); + for (int index = 0; index < lines.length; index ++) { + String line = lines[index]; + int position = line.indexOf(name); + if (position > 0) { + return line.substring(position + name.length()); + } + } + return ""; + } + + public String getOutput() { + return output; + } + + public int getResult() { + return result; + } + + public String[] getArgs() { + return args; + } +} diff --git a/convex-cli/src/test/java/convex/cli/Helper.java b/convex-cli/src/test/java/convex/cli/Helper.java new file mode 100644 index 000000000..f72216de2 --- /dev/null +++ b/convex-cli/src/test/java/convex/cli/Helper.java @@ -0,0 +1,19 @@ +package convex.cli; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class Helper { + + + public static void assertExecuteCommandLineResult(int returnCode, String patternText, String ... args) { + CommandLineTester tester = new CommandLineTester(args); + assertEquals(returnCode, tester.getResult()); + tester.assertOutputMatch(patternText); + } + + public static void assertCommandLineResult(int returnCode, String patternText, CommandLineTester tester) { + assertEquals(returnCode, tester.getResult()); + tester.assertOutputMatch(patternText); + } +} + diff --git a/convex-cli/src/test/java/convex/cli/output/OutputTest.java b/convex-cli/src/test/java/convex/cli/output/OutputTest.java new file mode 100644 index 000000000..f6ea30d59 --- /dev/null +++ b/convex-cli/src/test/java/convex/cli/output/OutputTest.java @@ -0,0 +1,68 @@ +package convex.cli.output; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.junit.jupiter.api.Test; + +import convex.core.Result; +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.data.prim.CVMLong; + +public class OutputTest { + + public static final long TEST_LONG_VALUE = 123456789L; + public static final String TEST_STRING_VALUE = "TEST_VALUE"; + public static final long TEST_ERROR_CODE = 98989898L; + + @Test + public void testSetField() { + + StringWriter outputWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(outputWriter); + ACell cellValue = Address.create(TEST_LONG_VALUE); + Output output = new Output(); + output.setField("Test-Long", TEST_LONG_VALUE); + output.setField("Test-Cell", cellValue); + output.setField("Test-String", TEST_STRING_VALUE); + output.writeToStream(printWriter); + String outputResult = outputWriter.toString(); + String fullText = String.format("Test-Long: %d\nTest-Cell: #%d\nTest-String: %s\n", TEST_LONG_VALUE, TEST_LONG_VALUE, TEST_STRING_VALUE); + assertEquals(fullText, outputResult.replaceAll("\r\n", "\n")); + } + + @Test + public void testSetResult() { + StringWriter outputWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(outputWriter); + ACell cellValue = Address.create(TEST_LONG_VALUE); + + Output output = new Output(); + Result result = Result.create(CVMLong.create(1), cellValue, null); + output.setResult(result); + output.writeToStream(printWriter); + String outputResult = outputWriter.toString(); + String fullText = String.format("Result: #%d\nData type: Address\n", TEST_LONG_VALUE); + assertEquals(fullText, outputResult.replaceAll("\r\n", "\n")); + } + + @Test + public void testSetResultError() { + StringWriter outputWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(outputWriter); + ACell cellValue = Address.create(TEST_LONG_VALUE); + ACell errorCode = CVMLong.create(TEST_ERROR_CODE); + Output output = new Output(); + Result result = Result.create(CVMLong.create(1), cellValue, errorCode); + output.setResult(result); + output.writeToStream(printWriter); + String outputResult = outputWriter.toString(); + String fullText = String.format("Result: #%d\nError code: %d\n", TEST_LONG_VALUE, TEST_ERROR_CODE); + assertEquals(fullText, outputResult.replaceAll("\r\n", "\n")); + } + + +} diff --git a/convex-cli/src/test/java/convex/cli/peer/SessionTest.java b/convex-cli/src/test/java/convex/cli/peer/SessionTest.java new file mode 100644 index 000000000..30bc2e3e5 --- /dev/null +++ b/convex-cli/src/test/java/convex/cli/peer/SessionTest.java @@ -0,0 +1,107 @@ +package convex.cli.peer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import convex.core.crypto.AKeyPair; + + +public class SessionTest { + + private static final String SESSION_FILENAME = "/tmp/session.dat"; + private static final String KEYSTORE_FILENAME = "/tmp/keystore.dat"; + + public List generateSessionList(Session session, int itemCount) { + List keyPairList = new ArrayList(itemCount); + for (int index = 0; index < 10; index ++ ) { + String hostname = String.format("testhostname_%d.com", index); + AKeyPair keyPair = AKeyPair.generate(); + keyPairList.add(keyPair); + session.addPeer(keyPair.getAccountKey(), hostname, KEYSTORE_FILENAME); + } + return keyPairList; + } + + @Test + public void sessionCreate() { + Session session = new Session(); + int itemCount = 10; + List keyPairList = generateSessionList(session, itemCount); + assertEquals(session.getSize(), itemCount); + for (int index = 0; index < itemCount; index ++ ) { + AKeyPair keyPair = keyPairList.get(index); + assertTrue(session.isPeer(keyPair.getAccountKey())); + SessionItem item = session.getItemFromIndex(index); + assertTrue(item.getAccountKey().equals(keyPair.getAccountKey())); + item = session.getItemFromAccountKey(keyPair.getAccountKey()); + assertTrue(item.getAccountKey().equals(keyPair.getAccountKey())); + } + } + + @Test + public void sessionGetHostNameList() { + Session session = new Session(); + int itemCount = 10; + generateSessionList(session, itemCount); + String[] hostnameList = session.getPeerHostnameList(); + for (int index = 0; index < hostnameList.length; index ++ ) { + String expectedHostname = String.format("testhostname_%d.com", index); + assertEquals(hostnameList[index], expectedHostname); + } + } + + @Test + public void sessionStoreAndLoad() { + File fp = new File(SESSION_FILENAME); + if (fp.exists()) { + fp.delete(); + } + Session session = new Session(); + int itemCount = 10; + generateSessionList(session, itemCount); + + try { + session.store(fp); + + } catch (IOException e) { + fail(e); + } + assertTrue(fp.exists()); + + Session savedSession = new Session(); + try { + savedSession.load(fp); + } catch (IOException e) { + fail(e); + } + assertEquals(session.getSize(), savedSession.getSize()); + for (int index = 0; index < savedSession.getSize(); index ++) { + SessionItem item = session.getItemFromIndex(index); + SessionItem savedItem = savedSession.getItemFromIndex(index); + assertTrue(item.getAccountKey().equals(savedItem.getAccountKey())); + } + fp.delete(); + } + + @Test + public void sessionRemovePeer() { + Session session = new Session(); + int itemCount = 10; + List keyPairList = generateSessionList(session, itemCount); + + for (int index = 0; index < keyPairList.size(); index ++ ) { + AKeyPair keyPair = keyPairList.get(index); + session.removePeer(keyPair.getAccountKey()); + } + assertEquals(session.getSize(), 0); + } + +} diff --git a/convex-core/.gitignore b/convex-core/.gitignore new file mode 100644 index 000000000..e450bc81d --- /dev/null +++ b/convex-core/.gitignore @@ -0,0 +1,5 @@ +/target/ +/.project +/.classpath +/.settings/ +/pom.xml.versionsBackup diff --git a/convex-core/pom.xml b/convex-core/pom.xml new file mode 100644 index 000000000..ddddc7b83 --- /dev/null +++ b/convex-core/pom.xml @@ -0,0 +1,125 @@ + + + world.convex + convex + 0.7.0-rc3 + + + 4.0.0 + + convex-core + + Convex Core + Convex core libraries and common utilities + https://convex.world + + + + + org.antlr + antlr4-maven-plugin + 4.9.2 + + ${basedir}/lib/src/main/antlr4/convex/core/lang/reader/antlr + + + + + antlr4 + + + + + + + + src/main/cvx + + + src/main/antlr4 + + + + + + + + org.bouncycastle + bcprov-jdk15on + 1.69 + + + org.bouncycastle + bcpkix-jdk15on + 1.69 + + + + com.goterl + lazysodium-java + 5.1.1 + + + + net.java.dev.jna + jna + 5.8.0 + + + + org.apache.commons + commons-text + 1.9 + + + com.pholser + junit-quickcheck-core + 1.0 + test + + + com.pholser + junit-quickcheck-generators + 1.0 + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.junit.vintage + junit-vintage-engine + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + + + + org.antlr + antlr4-runtime + 4.9.2 + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-jdk14 + ${slf4j.version} + test + + + + diff --git a/convex-core/src/main/antlr4/convex/core/lang/reader/antlr/Convex.g4 b/convex-core/src/main/antlr4/convex/core/lang/reader/antlr/Convex.g4 new file mode 100644 index 000000000..1815dacb4 --- /dev/null +++ b/convex-core/src/main/antlr4/convex/core/lang/reader/antlr/Convex.g4 @@ -0,0 +1,189 @@ +grammar Convex; + +form + : literal + | symbol + | pathSymbol + | dataStructure + | syntax + | quoted + ; + +singleForm: form EOF; + +forms: (form | commented) * ; + +dataStructure: + list | vector | set | map; + +list : '(' forms ')'; + +vector : '[' forms ']'; + +set : HASH '{' forms '}'; + +map : '{' forms '}'; + +literal + : nil + | bool + | blob + | character + | keyword + | symbol + | address + | string + | longValue + | doubleValue + | specialLiteral + ; + +longValue: + DIGITS | SIGNED_DIGITS; + +doubleValue: + DOUBLE; + +specialLiteral: HASH HASH symbol; + +address: HASH DIGITS; + +nil: NIL; + +blob: BLOB; + +bool: BOOL; + +character: CHARACTER; + +keyword: KEYWORD; + +symbol: SYMBOL; + +pathSymbol: SYMBOL_PATH; + +syntax: META form form; + +quoted: QUOTING form; + +string: STRING; + +commented: COMMENTED form; + +/* ========================================= + * Lexer stuff below here + * ========================================= + */ + +SYMBOL_PATH: + (NAME | HASH DIGITS) ('/' NAME)+; + +COMMENTED: '#_'; + +HASH: '#'; + +META: '^'; + +NIL: 'nil'; + +BOOL : 'true' | 'false' ; + +// Number. Needs to go before Symbols! + +DOUBLE: + (DIGITS | SIGNED_DIGITS) DOUBLE_TAIL; + +fragment +DOUBLE_TAIL: + DECIMAL EPART | DECIMAL | EPART; + +fragment +DECIMAL: + '.' DIGITS; + +fragment +EPART: + [eE] (DIGITS | SIGNED_DIGITS); + +DIGITS: + [0-9]+; + +SIGNED_DIGITS: + '-' DIGITS; + +BLOB: '0x' HEX_DIGIT*; + +fragment +HEX_BYTE: HEX_DIGIT HEX_DIGIT; + +fragment +HEX_DIGIT: [0-9a-fA-F]; + +STRING : '"' ( ~'"' | '\\' '"' )* '"' ; + +// Quoting + +QUOTING: '\'' | '`' | '~' | '~@'; + +// Symbols and Keywords + + +KEYWORD: + ':' NAME; + +SYMBOL + : NAME + ; + +fragment +NAME + : '/' + | SYMBOL_FIRST SYMBOL_FOLLOWING*; + +CHARACTER + : '\\u' HEX_BYTE HEX_BYTE + | '\\' . + | SPECIAL_CHARACTER; + +fragment +SPECIAL_CHARACTER + : '\\' ( 'newline' + | 'return' + | 'space' + | 'tab' + | 'formfeed' + | 'backspace' ) ; + + +// Test case "a*+!-_?<>=!" should be a symbol + +fragment +SYMBOL_FIRST + : ALPHA + | '.' | '*' | '+' | '!' | '-' | '_' | '?' | '$' | '%' | '&' | '=' | '<' | '>' + ; + +fragment +SYMBOL_FOLLOWING + : SYMBOL_FIRST + | [0-9] + | ':' | '#' + ; + +fragment +ALPHA: [a-z] | [A-Z]; + +/* + * Whitespace and comments + */ + +fragment +WS : [ \n\r\t,] ; + +fragment +COMMENT: ';' ~[\r\n]* ; + +TRASH + : ( WS | COMMENT ) -> channel(HIDDEN) + ; + diff --git a/convex-core/src/main/assembly/full.xml b/convex-core/src/main/assembly/full.xml new file mode 100644 index 000000000..e8be999d2 --- /dev/null +++ b/convex-core/src/main/assembly/full.xml @@ -0,0 +1,34 @@ + + full + + jar + + + + ${project.basedir} + / + + README* + LICENSE* + NOTICE* + + + + ${project.build.directory} + / + + *.jar + + + + + + / + true + true + true + test + + + \ No newline at end of file diff --git a/convex-core/src/main/assembly/testing.xml b/convex-core/src/main/assembly/testing.xml new file mode 100644 index 000000000..cf7092fbb --- /dev/null +++ b/convex-core/src/main/assembly/testing.xml @@ -0,0 +1,30 @@ + + testing + + jar + + false + + + / + true + true + true + test + + + + + + ${project.build.directory}/test-classes + / + + **/*.class + + true + + + \ No newline at end of file diff --git a/convex-core/src/main/cvx/asset/box.cvx b/convex-core/src/main/cvx/asset/box.cvx new file mode 100644 index 000000000..6e1f17f5e --- /dev/null +++ b/convex-core/src/main/cvx/asset/box.cvx @@ -0,0 +1,100 @@ +'asset.box + +(call *registry* + (register {:description ["A box acts as a holder of arbitrary assets, bundling them together." + "Assets are described in `convex.asset` and created using accounts such as `convex.fungible`." + "The owner of the box has exclusive rights to put assets in or take assets out." + "The box itself is an asset with the designator `[box-actor id]`." + "This library uses `asset.box.actor` as default actor. An alternative implementation can be provided if required."] + :name "Asset box API."})) + + +;;;;;;;;;; Setup + + +(import asset.box.actor :as box.actor) +(import convex.asset :as asset-lib) + + +;;;;;;;;;; Public API + + +(defn burn + + ^{:doc {:description "Destroys a set of box ids which must be owned and empty." + :signature [{:params [set-box-ids]} + {:params [actor set-box-ids]}]}} + + + ([set-box-ids] + + (burn box.actor + set-box-ids)) + + + ([actor set-box-ids] + + (call actor + (burn set-box-ids)))) + + + +(defn create + + ^{:doc {:description "Creates a new box and returns its id." + :signature [{:params []} + {:params [actor]}]}} + + + ([] + + (create box.actor)) + + + ([actor] + + (call actor + (create)))) + + + +(defn insert + + ^{:doc {:description "Inserts an asset into a box." + :signature [{:params [box-id asset]} + {:params [actor box-id asset]}]}} + + + ([box-id asset] + + (insert box.actor + box-id + asset)) + + + ([actor box-id asset] + + (asset-lib/transfer actor + asset + box-id))) + + + +(defn remove + + ^{:doc {:description "Removes an asset from the given box." + :signature [{:params [box-id]} + {:params [actor box-id asset]}]}} + + ([box-id asset] + + (remove box.actor + box-id + asset)) + + + ([actor box-id asset] + + (call actor + (remove box-id + asset)))) diff --git a/convex-core/src/main/cvx/asset/box/actor.cvx b/convex-core/src/main/cvx/asset/box/actor.cvx new file mode 100644 index 000000000..ca81b0235 --- /dev/null +++ b/convex-core/src/main/cvx/asset/box/actor.cvx @@ -0,0 +1,301 @@ +'asset.box.actor + +(call *registry* + (register {:description ["Default actor for `asset.box`." + "Implements callable functions for `asset.box` and `convex.asset`."] + :name "Asset box actor."})) + + +;;;;;;;;;; Setup + + +(import convex.asset :as asset-lib) + + +;;;;;;;;;; Values + + +(def boxes + + ^{:doc {:description "Map of `box id` -> `asset quantity`."}} + + {}) + + + +(def counter + + ^{:doc {:description "Used for creating box ids."}} + + 0) + + + +(def offers + + ^{:doc {:description "Map of `owner` -> (Map of `recipient address` -> `set of box ids`."}} + + {}) + + + +(def ownership + + ^{:doc {:descrption "Map of `owner` -> `set of box ids`."}} + + {}) + + +;;;;;;;;;; Private helpers + + +(defn -direct-transfer + + ^{:private? true} + + ;; Internal implementation for executing a direct transfer. + + [sender receiver quantity] + + (let [receiver (address receiver) + sender-balance (get ownership + sender + #{}) + _ (assert (subset? quantity + sender-balance)) ;; TODO. Replace with `fail` for better error messages? + receiver-balance (get ownership + receiver + #{}) + new-sender-balance (difference sender-balance + quantity) + new-receiver-balance (union receiver-balance + quantity)] + (def ownership + (assoc ownership + sender new-sender-balance + receiver new-receiver-balance)) + quantity)) + + +;;;;;;;;;; Implementation of `convex.asset` interface + + +(defn accept + + ^{:callable? true + :private? true} + + [sender quantity] + + (let [sender (address sender) + sender-offers (get offers + sender + {}) + offer (or (get-in offers + [sender *caller*]) + #{}) + _ (assert (subset? quantity + offer)) + receiver-balance (get ownership + *caller* + #{}) + new-offer (difference offer + quantity)] + (def offers + (assoc offers sender + (assoc sender-offers + *caller* + new-offer))) + (-direct-transfer sender + *caller* + quantity))) + + + +(defn balance + + ^{:callable? true + :private? true} + + [owner] + + (or (get ownership + owner) + #{})) + + + +(defn direct-transfer + + ^{:callable? true + :private? true} + + [receiver quantity] + + (-direct-transfer *caller* + receiver + quantity)) + + + +(defn offer + + ^{:callable? true + :private? true} + + [receiver quantity] + + (let [caller-offers (get offers + *caller* + {})] + (def offers + (assoc offers + *caller* + (assoc caller-offers + receiver + quantity))))) + + + +(defn receive-asset + + ^{:callable? true + :private? true} + + [asset box-id] + + (let [box-id (long box-id)] + ;; Accepting first solves the problem of putting a box into itself. + ;; + (asset-lib/accept *caller* + asset) + (cond + (not (contains-key? boxes + box-id)) + (fail :STATE + "Target box does not exist") + + (not (contains-key? (get ownership + *caller*) + box-id)) + (fail :TRUST + (str "Box " box-id " not owned"))) + (def boxes + (assoc boxes + box-id + (asset-lib/quantity-add (get boxes + box-id) + asset))))) + + + +(def quantity-add + + ^{:callable? true + :private? true} + + union) + + + +(def quantity-sub + + ^{:callable? true + :private? true} + + difference) + + + +(def quantity-subset? + + ^{:callable? true + :private? true} + + subset?) + + +;;;;;;;;;; Implementation of `asset.box` interface + + +(defn burn + + ^{:callable? true + :private? true} + + [set-box-ids] + + (let [owned-boxes (ownership *caller*)] + (when-not (subset? set-box-ids + owned-boxes) + (fail :TRUST + "Burning boxes requires ownership")) + (for [id set-box-ids] + (let [contents (boxes id)] + (if (empty? contents) + (def boxes + (dissoc boxes + id)) + (fail :STATE + (str "Trying to delete non-empty box: " id))))) + (def ownership + (assoc ownership + *caller* + (difference owned-boxes + set-box-ids))) + nil)) + + + +(defn create + + ^{:callable? true + :private? true} + + [] + + (let [id counter + owner *caller* + owned-boxes (or (get ownership + owner) + #{})] + (def ownership + (assoc ownership + owner + (conj owned-boxes + id))) + (def boxes + (assoc boxes + id + {})) ;; New box contains no assets + (def counter + (inc counter)) + id)) + + + +(defn remove + + ^{:callable? true + :private? true} + + [box-id asset] + + (let [current-asset (get boxes + box-id)] + (when-not (asset-lib/quantity-contains? current-asset + asset) + (fail "Box does not contain quantity of asset specified for removal")) + (when-not (contains-key? (ownership *caller*) + box-id) + (fail :TRUST + (str "Box not owned: " box-id))) + (def boxes + (assoc boxes + box-id + (asset-lib/quantity-sub current-asset + asset))) + ;; Delivers the asset to the caller. + ;; + (asset-lib/transfer *caller* + asset))) diff --git a/convex-core/src/main/cvx/asset/nft/simple.cvx b/convex-core/src/main/cvx/asset/nft/simple.cvx new file mode 100644 index 000000000..71ca90c17 --- /dev/null +++ b/convex-core/src/main/cvx/asset/nft/simple.cvx @@ -0,0 +1,208 @@ +'asset.nft.simple + + +(call *registry* + (register {:description ["Enables the creation of minimal NFT tokens." + "An NFT is merely a long. Users can build an additional layer so that this id points to anything." + "Follows the interface described in `convex.asset`."] + :name "Simple NFT creation and management"})) + + +;;;;;;;;;; Values + + +(def counter + + ^{:doc {:description "Used for creating NFT ids."}} + + 0) + + + +(def offers + + ^{:doc {:description "Map of `owner` -> map of `recipient address` -> `set of NFT ids`"}} + + {}) + + +;;;;;;;;;; Implementation of `convex.asset` interface + + +(defn -direct-transfer + + ^{:private? true} + + ;; Used internally by [[accept]] and [[direct-transfer]]. + + [sender receiver quantity] + + (let [receiver (address receiver) + sender-balance (or (get-holding sender) + #{}) + _ (assert (subset? quantity + sender-balance)) + receiver-balance (or (get-holding receiver) + #{}) + new-sender-balance (difference sender-balance + quantity) + new-receiver-balance (union receiver-balance + quantity)] + (set-holding sender + new-sender-balance) + (set-holding receiver + new-receiver-balance)) + quantity) + + + +(defn accept + + ^{:callable? true + :private? true} + + [sender quantity] + + (let [sender (address sender) + sender-offers (or (get offers + sender) + {}) + offer (or (get-in offers + [sender + *caller*]) + #{}) + _ (assert (subset? quantity + offer)) + receiver-balance (or (get-holding *caller*) + #{}) + new-offer (difference offer + quantity)] + + (def offers + (assoc offers + sender + (assoc sender-offers + *caller* + new-offer))) + + (-direct-transfer sender + *caller* + quantity))) + + + +(defn balance + + ^{:callable? true + :private? true} + + [owner] + + (or (get-holding owner) + #{})) + + + +(defn direct-transfer + + ^{:callable? true + :private? true} + + [receiver quantity] + + (-direct-transfer *caller* + receiver + quantity)) + + + +(defn offer + + ^{:callable? true + :private? true} + + [receiver quantity] + + (let [caller-offers (get offers + *caller* + {})] + (def offers + (assoc offers + *caller* + (assoc caller-offers + receiver + quantity))))) + + + +(def quantity-add + + ^{:callable? true + :private? true} + + union) + + + +(def quantity-sub + + ^{:callable? true + :private? true} + + difference) + + + +(def quantity-subset? + + ^{:callable? true + :private? true} + + subset?) + + +;;;;;;;;;; Callable functions + + +(defn burn + + ^{:callable? true + :doc {:description "Destroys a set of NFTs. NFTs must be owned by the caller." + :signature [{:params [nft-set]}]}} + + [nft-set] + + (let [owned-nfts (get-holding *caller*) + nft-set (cond + (long? nft-set) #{nft-set} + (set? nft-set) nft-set + :else (set nft-set))] + (when-not (subset? nft-set + owned-nfts) + (fail :TRUST + "Can only burn owned NFTs")) + (set-holding *caller* + (difference owned-nfts + nft-set)) + nft-set)) + + + +(defn create + + ^{:callable? true + :doc {:description "Creates a new NFT with a fresh ID and arbitrary metadata." + :signature [{:params []}]}} + + [] + + (let [id counter + owner *caller* + owned-nfts (or (get-holding owner) + #{})] + (set-holding owner + (conj owned-nfts + id)) + (def counter + (inc counter)) + id)) diff --git a/convex-core/src/main/cvx/asset/nft/tokens.cvx b/convex-core/src/main/cvx/asset/nft/tokens.cvx new file mode 100644 index 000000000..27e938750 --- /dev/null +++ b/convex-core/src/main/cvx/asset/nft/tokens.cvx @@ -0,0 +1,747 @@ +'asset.nft.tokens + + +(call *registry* + (register {:description ["Actor for storing and handling Non-Fungible Tokens, where a token is an map containing:" + "- `:creator`, address of the creator" + "- `:data`, arbitrary value describing the NFT (eg. a URL)" + "- `:owner`, current owner" + "At creation, rights can be specified as to who can transfer, destroy, or update the NFT data in anyway (see `create-token`)." + "Each NFT implements the asset interface described in `convex.asset`." + "Hence, while this account offers specific functions such as `destroy-token`, all other matters are resolved using `convex.asset`."] + :name "Advanced NFT tokens creation and management"})) + + +;; +;; +;; The primary notion of "quantity" for this contract is a set of token ids. +;; As a convenience, we also recognize a single id as shorthand for the set of that id. +;; Therefore, the contract recognizes the following two types of asset descriptions: +;; +;; `[nft-contract-address id-set]` +;; `[nft-contract-address id]`, which is shorthand for `[nft-contract-address #{id}]` +;; +;; This smart contract is designed to be exceptionally simple to use for one-off "singleton tokens", +;; while also providing numerous extension points for creating a class of tokens with shared custom behavior. +;; The goal is that most custom classes of nft tokens will not require building an entire contract +;; from scratch, and instead can be implemented by providing a "class actor" that contains the custom logic. +;; This makes it easier to verify that custom nfts are correct and trustworthy. +;; +;; Each NFT token has a creator, a current owner, and a data map. +;; Note that the owner doesn't necessarily have rights to do anything with the token other than to assert ownership. +;; Singleton tokens utilize a "policy map" to determine who has the right to destroy token, +;; transfer token, or update the data map. By default, these rights reside with the token owner, but +;; the policy map may assign these rights to someone other than the owner, or possibly to no one. +;; One particularly interesting thing you can do with the policy map is to assign a right to +;; the holder of some other token id or asset, thus turning a right into something that is itself transferable. + +;; If you desire more complex policy behavior than what the policy map provide, you can implement a +;; "class actor" defining a new class of non-fungible-tokens with some shared behavior. +;; A class actor must implement `check-trusted?`, similarly to what is defined here. +;; +;; As part of the class actor, you may also implement the following "hooks": +;; (create-token caller id initial-data) - Called when token is created +;; (destroy-token caller id) - Called when token is destroyed +;; (set-token-data caller id data) - Called when token's data is replaced with a new data map +;; (merge-token-data caller id data) - Called when data is merged in with token's existing data. +;; (get-uri id) - If implemented, this is preferred over :uri field in token's data map. +;; (check-transfer caller sender receiver id-set) - Additional restrictions beyond basic permission testing. +;; (perform-transfer caller sender receiver id-set) - Called when transfer takes place +;; +;; Note that a class actor's check-transfer and perform-transfer functions +;; are called with the set of all token ids involved in the transfer that have this class actor. +;; So when transferring a mixed set of tokens from several classes, they are grouped by class, +;; and then each class actor gets called with the set of ids relevant to it. +;; +;; Transfer is implemented using the offer/accept model, following `convex.asset`. +;; So, sender `offer`s a set of ids to the receiver. +;; The receiver's `receive-asset` function is then called to notify them of the offer, +;; and if they want to accept the offer, they call the asset's `accept` function, accepting +;; whatever subset of the offered tokens they want. +;; If an account doesn't implement receive-asset, default behavior is to go ahead and perform the transfer. +;; If an actor doesn't implement receive-asset, default behavior is to fail. +;; +;; + + +(import convex.asset :as asset) + + +;;;;;;;;;; Values + + +(def next-token-id + + ^{:private? true} + + 0) + + + +(def token-records + + ^{:private? true} + + ;; {id {:creator, :owner, :data, :policies, :class}} + + {}) + + + +(def offers + + ^{:private? true} + + ;; {sender {receiver id-set}} + + {}) + + +;;;;;;;;;; Private - Generic + + +(defn empty->nil + + ^{:private? true} + + [s] + + (if (empty? s) + nil + s)) + + + +(defn every? + + ^{:private? true} + + [f coll] + + (reduce (fn [_ x] + (if (f x) + true + (reduced false))) + true + coll)) + + +(defn group-by-class + + ^{:private? true} + + ;; Groups ids by class, only including them if they export a given symbol. + + [id-set required-export] + + (reduce (fn [m id] + (let [class (get-token-class id)] + (if (and class + (callable? class + required-export)) + (assoc m + class + (conj (get m + class + #{}) + id)) + m))) + {} + id-set)) + + + +(defn num->set + + ^{:private? true} + + ;; For convenience, our asset path understands a long to be a singleton set. + ;; Here's a helper function to do that conversion + + [n] + + (if (long? n) + #{n} + n)) + + +;;;;;;;;;; Private - API high-level helpers + + +(defn cancel-offer + + ^{:private? true} + + ;; Optimizes memory use in offers. + + [sender receiver] + + (def offers + (if-let [my-offers (empty->nil (dissoc (get offers + sender) + receiver))] + (assoc offers + sender + my-offers) + (dissoc offers + sender)))) + + + +(defn perform-transfer + + ^{:private? true} + + [sender receiver id-set] + + (when-let [msg (check-transfer sender + receiver + id-set)] + (fail (str msg))) + (def token-records + (reduce (fn [records id] + (assoc-in records + [id + :owner] + receiver)) + token-records + id-set)) + (set-holding sender + (empty->nil (difference (get-holding sender) + id-set))) + (set-holding receiver + (union (get-holding receiver) + id-set)) + (let [perform-transfer-map (group-by-class id-set + 'perform-transfer)] + (reduce (fn [_ [class id-set]] + (call class + (perform-transfer *caller* + sender + receiver + id-set))) + nil + perform-transfer-map) + [*address* + id-set])) + + +;;;;;;;;;; Callable API + + +(defn create-token + + ^{:callable? true + :doc {:description ["Creates token with initial data map." + "A policy map can provided (see `check-trusted?`) or the address of an account acting as a \"class\"." + "A class implements a version of `check-trusted?` and optionally any of the functions found it this account." + "Eg. `destroy-token`, `set-token-data`, ..." + "Classes are meant for advanced use cases, most of the time a policy map or nil will be sufficient." + "Nil means that only the owner can any perform operation on that token (eg. destruction, transfer)"] + :examples [{:code "(create-token {:name \"My Amazing Artwork\"} nil)"} + {:code "(create-token {:name \"Concert ticket\", :redeemed? false} {:destroy :owner, :update :creator, :transfer :creator})"} + {:code "(create-token {:name \"House\"} real-estate-class-actor)"}] + :signature [{:params [initial-data policy-map-or-class]}]}} + + [initial-data policy-map-or-class] + + (assert (or (nil? initial-data) + (map? initial-data)) + (or (nil? policy-map-or-class) + (map? policy-map-or-class) + (and (actor? policy-map-or-class) + (callable? policy-map-or-class + 'check-trusted?)))) + (let [holdings (or (get-holding *caller*) + #{}) + id next-token-id + token-record {:creator *caller* + :owner *caller*} + token-record (if initial-data + (assoc token-record + :data + initial-data) + token-record) + token-record (cond + (nil? policy-map-or-class) token-record + (map? policy-map-or-class) (assoc token-record + :policies + policy-map-or-class) + :else (assoc token-record + :class + policy-map-or-class)) + class (:class token-record)] + (set-holding *caller* + (conj holdings + id)) + (when (and class + (callable? class + 'create-token)) + (call class + (create-token *caller* + id + initial-data))) + (def next-token-id + (inc next-token-id)) + (def token-records + (assoc token-records + id + token-record)) + id)) + + + +(defn destroy-token + + ^{:callable? true + :doc {:description "Destroys token if `(check-trusted? *caller* :destroy id)` returns true." + :examples [{:code "(destroy-token 1234)"}] + :signature [{:params [id]}]}} + + [id] + + (when-not (check-trusted? *caller* + :destroy + id) + (fail "No right to destroy token")) + (let [record (get token-records + id) + class (:class record) + owner (:owner record)] + (set-holding owner (empty->nil (disj (get-holding owner) + id))) + (def token-records + (dissoc token-records + id)) + (when (and class + (callable? class + 'destroy-token)) + (call class + (destroy-token *caller* + id))) + true)) + + + +(defn get-offer + + ^{:callable? true + :doc {:description "Gets the offer from a given sender to a given receiver." + :examples [{:code "(get-offer sender receiver)"}] + :signature [{:params [sender receiver]}]}} + + [sender receiver] + + (get-in offers + [sender + receiver])) + + + +(defn get-offers + + ^{:callable? true + :doc {:description "Gets all the offers from a given sender." + :examples [{:code "(get-offers sender)"}] + :signature [{:params [sender]}]}} + [sender] + + (get offers + sender)) + + + +(defn get-token-class + + ^{:callable? true + :doc {:description "Gets class actor for token, returns nil if it is a singleton token with policy map.", + :examples [{:code "(get-token-class 1234)"}] + :signature [{:params [id]}]}} + + [id] + + (get-in token-records + [id + :class])) + + + +(defn get-token-creator + + ^{:callable? true + :doc {:description "Gets creator of token.", + :examples [{:code "(get-token-creator 1234)"}] + :signature [{:params [id]}]}} + + [id] + + (get-in token-records + [id + :creator])) + + + +(defn get-token-data + + ^{:callable? true + :doc {:description "Gets data map associated with token." + :examples [{:code "(get-token-data 1234)"}] + :signature [{:params [id]}]}} + + [id] + + (get-in token-records + [id + :data])) + + + +(defn get-token-owner + + ^{:callable? true + :doc {:description "Gets owner of token.", + :examples [{:code "(get-token-owner 1234)"}] + :signature [{:params [id]}]}} + + [id] + + (get-in token-records + [id + :owner])) + + + +(defn get-uri + + ^{:callable? true + :doc {:description ["Gets the URI associth with token." + "Not all NFTs have a URI pointing to data off-chain but it is common."]}} + + ;; The class actor handles this call, if available, otherwise we look in the token's data for :uri. + + [id] + + (let [record (get token-records + id) + class (:class record)] + (if (and class + (callable? class + 'get-uri)) + (call class + (get-uri id)) + (:uri (:data record))))) + + + +(defn merge-token-data + + ^{:callable? true + :doc {:description ["Merges data into token's data map." + "Caller must have the right to do so. Either:" + "- `(check-trusted? *caller* :update id)` returns true" + "- `(check-trusted? *caller* [:update key] id) returns true for all key-values"] + :examples [{:code "(merge-token-data 1234 {:redeemed? true})"}] + :signature [{:params [id data]}]}} + + [id data] + + (when-not (or (check-trusted? *caller* + :update + id) + (every? (fn [k] (check-trusted? *caller* + [:update + k] + id)) + (keys data))) + (fail "No right to update token's data fields")) + (let [class (get-token-class id) + new-data (merge (get-in token-records + [id :data]) + data)] + (def token-records + (assoc-in token-records + [id + :data] + new-data)) + (when (and class + (callable? class + 'merge-token-data)) + (call class + (merge-token-data *caller* + id + data))) + new-data)) + + + +(defn set-token-data + + ^{:callable? true + :doc {:description "Replaces data map if `(check-trusted? *caller* :update id)` returns true." + :examples [{:code "(set-token-data 1234 {:name \"New name\", :redeemed? false})"}] + :signature [{:params [id data]}]}} + + [id data] + + (when-not (check-trusted? *caller* + :update + id) + (fail "No right to update token's data")) + (let [class (get-token-class id)] + (def token-records + (assoc-in token-records + [id + :data] + data)) + (when (and class + (callable? class + 'set-token-data)) + (call class + (set-token-data *caller* + id + data))) + data)) + + +;;;;;;;;;; Implementation of `convex.asset` interface +;; +;; `get-offer` is implemented as part of the public callable API. + + +(defn accept + + ^{:callable? true + :private? true} + + [sender accepted-id-set] + + (let [sender (address sender) + receiver *caller* + accepted-id-set (num->set accepted-id-set) + offered-id-set (or (get-in offers + [sender + receiver]) + #{})] + (when-not offer + (fail "Offer not found")) + ;; Assures receiver accepts a subset of offer. + ;; + (assert (subset? accepted-id-set + offered-id-set)) + (if (= accepted-id-set + offered-id-set) + (cancel-offer sender + receiver) + (def offers + (assoc-in offers + [sender + receiver] + (difference offered-id-set + accepted-id-set)))) + (perform-transfer sender + receiver + accepted-id-set))) + + + +(defn balance + + ^{:callable? true + :private? true} + + [owner] + + (or (get-holding (address owner)) + #{})) + + + +(defn direct-transfer + + ^{:callable? true + :private? true} + + ;; Interface for a sender to call the private perform-transfer function + ;; This does not use the offer/accept model, and does not check whether the receiver wants to receive it. + ;; Therefore, it is preferred to use `asset/transfer` to transfer assets. + + [receiver id-set] + + (perform-transfer *caller* + receiver + (num->set id-set))) + + + +(defn check-transfer + + ^{:callable? true + :private? true} + + ;; Returns string explaining restriction, or nil if no restriction on transfer. + + [sender receiver id-set] + + (let [id-set (num->set id-set) + sender (address sender) + msg (reduce (fn [_ id] + (when-not (check-trusted? sender + :transfer + id) + (reduced (str "No right to transfer token " + id)))) + nil + id-set)] + (or msg + (let [check-transfer-map (group-by-class id-set + 'check-transfer)] + (reduce (fn [_ [class id-set]] + (when-let [msg (call class + (check-transfer *caller* + sender + receiver + id-set))] + (reduced msg))) + nil + check-transfer-map))))) + + + + +(defn offer + + ^{:callable? true + :private? true} + + ;; Can cancel existing offer by passing in `[addr #{}]`. + + [receiver id-set] + + (let [sender *caller*, + receiver (address receiver) + id-set (num->set id-set)] + (cond + (empty? id-set) (cancel-offer sender + receiver) + :else (def offers + (assoc-in offers + [sender + receiver] + id-set))))) + + + +(def quantity-add + + ^{:callable? true + :private? true} + + union) + + + +(def quantity-sub + + ^{:callable? true + :private? true} + + difference) + + + +(defn quantity-subset? + + ^{:callable? true + :private? true} + + [a b] + + ;; Hack, treats longs as singleton set. + ;; + (if (long? a) + (contains-key? b + a) + (subset? a b))) + + +;;;;;;;;;; Private callable API + + +(defn check-trusted? + + ^{:callable? true + :doc {:description ["Determines whether a given address as certain rights.." + "A `policy-key` is either:" + "- `:destroy`, right to destroy token" + "- `:transfer`, right to transfer token" + "- `:update`, right to update token's data map" + "- `[:update key]`, right to update specific key in token's data map" + "Value for each policy key is determined at token creation (see `create-token`). Either:" + "- `:creator`, only creator" + "- `:owner`, only current owner" + "- `:none`, nobody allowed" + "- A specific address" + "- Another token id, meaning the owner of that other token has the right" + "- An asset as described in `convex.asset`"] + :signature [{:params [addr policy-key id]}]}} + + ;; For tokens that have a class actor, we hand off to the class's check-trusted? function, + ;; and the logic here is bypassed entirely. + ;; + ;; For singleton tokens, we check the token's policies (a map). + ;; The value associated with the given policy-key designates who has the rights to do that action. + ;; :creator or :owner or :none or a specific account/actor address or + ;; a token number (whoever owns that token number has the right) or + ;; asset description (whoever owns that asset/quantity has the right). + ;; [policy-key key] defaults to policy for policy-key + ;; Otherwise, a nil policy defaults to :owner + + [addr policy-key id] + + (let [token-record (get token-records + id)] + (if-let [class (:class token-record)] + (call class + (check-trusted? addr + policy-key + id)) + (let [policy (get-in token-record + [:policies + policy-key]) + policy (if (and (nil? policy) + (vector? policy-key)) + (get-in token-record + [:policies + (first policy-key)]) + policy)] + (cond + (or (nil? policy) + (= policy + :owner)) + (= addr + (get-token-owner id)) + + (= policy + :creator) + (= addr + (get-token-creator id)) + + (= policy + :none) + false + + ;; Policy is a token id. + ;; + (long? policy) + (= addr + (get-token-owner policy)) + + ;; Policy is an asset description. + ;; + (vector? policy) + (asset/owns? addr + policy) + + :else + (= addr + policy)))))) diff --git a/convex-core/src/main/cvx/convex/asset.cvx b/convex-core/src/main/cvx/convex/asset.cvx new file mode 100644 index 000000000..b93dc19bf --- /dev/null +++ b/convex-core/src/main/cvx/convex/asset.cvx @@ -0,0 +1,392 @@ +'convex.asset + +(call *registry* + (register {:description ["An asset is either:" + "- A vector `[asset-address quantity]` indicating an asset managed by an actor" + "- A map `asset-adress` -> `quantity` indicating assets managed by one or more actors" + "Quantities are arbitrary and asset-specific." + "For a fungible currency, quantity is the amount ; for a non-fungible token, quantity may be a set of token ids." + "Key functions from this library are `balance`, `owns?`, and `transfer`." + "Other functions such as `accept` and `offer` provide more fine-grained control when required." + "Implementing a new asset means deploying an actor which defines the following callable functions:" + "- `(accept sender quantity)`" + "- `(balance owner)`" + "- `(check-transfer sender receiver quantity)`" + "- `(direct-transfer receiver quantity)`" + "- `(get-offet sender receiver)`" + "- `(offer receiver quantity`" + "- `(quantity-add asset-address a b)`" + "- `(quantity-sub asset-address a b)`" + "- `(quantity-subset a b)` ; returns true if `a` is considered a subset of `b`" + "For more information, see those functions defined in this library whose purpose is to delegate to asset implementations in a generic way."] + :name "Asset abstraction library"})) + + +;;;;;;;;;; Private + + +(defn -make-map + + ^{:private? true} + + ;; Used by quantity functions. + + [a] + + (cond + (map? a) a + (vector? a) {(first a) (second a)} + (nil? a) {})) + + +;;;;;;;;;; Transfers + + +(defn accept + + ^{:doc {:description ["Accepts asset from sender." + "If asset contains multiple assets, accepts each in turn. MUST fail if the asset cannot be accepted."] + :examples [{:code "(accept sender [fungible-token-address 1000])"}] + :signature [{:params [sender [asset-address quantity]]}]}} + + [sender asset] + + (cond + (vector? asset) + (let [asset-address (first asset) + quantity (second asset)] + (call asset-address + (accept sender + quantity))) + + (map? asset) + (reduce (fn [m [asset-address quantity]] + (assoc m + asset-address + (call asset-address + (accept sender + quantity)))) + {} + asset))) + + + +(defn check-transfer + + ;; Independently of general transfer, you can test whether there are restrictions on transferring. + + ^{:doc {:description ["Checks whether sender can transfer this asset to receiver." + "Returns a descriptive failure message if there is a restriction prohibiting transfer, or nil if there is no restriction."] + :examples [{:code "(check-transfer sender receiver [fungible-token-address 1000])"} + {:code "(check-transfer sender receiver [non-fungible-token-address #{1 4 6}])"}] + :signature [{:params [sender receiver [asset-address quantity]]}]}} + + [sender receiver [asset-address quantity]] + + (query (call asset-address + (check-transfer sender + receiver + quantity)))) + + + +(defn get-offer + + ^{:doc {:description ["Gets the current offer from `sender` to `receiver` for a given asset." + "Returns the quantity representing the current offer. Will be the 'zero' quantity if no open offer exists."] + :examples [{:code "(get-offer asset-address sender receiver)"}] + :signature [{:params [asset-address sender receiver]}]}} + + [asset-address sender receiver] + + (query (call asset-address + (get-offer sender + receiver)))) + + + +(defn offer + + ;; For smart contract assets, you can offer and accept separately if you choose + + ^{:doc {:description ["Opens an offer of an `asset` to a `receiver`, which makes it possible for the receiver to 'accept' up to this quantity." + "May result in an error if the asset does not support open offers."] + :examples [{:code "(offer receiver [fungible-token-address 1000])"} + {:code "(offer receiver [non-fungible-token-address #{1 4 6}])"}] + :signature [{:params [receiver [asset-address quantity]]}]}} + + [receiver asset] + + (cond + (vector? asset) + (let [asset-address (first asset) + quantity (second asset)] + (call asset-address + (offer receiver + quantity))) + + (map? asset) + (reduce (fn [m [asset-address quantity]] + (assoc m + asset-address + (call asset-address + (offer receiver + quantity)))) + {} + asset))) + + + +(defn transfer + + ^{:doc {:description "Transfers asset to receiver. `data` is an arbitrary value, which will be passed to the receiver's `receive-asset` method." + :examples [{:code "(transfer receiver [fungible-token-address 1000])"} + {:code "(transfer receiver [non-fungible-token-address #{1 4 6}] optional-data)"}] + :signature [{:params [receiver asset]} + {:params [receiver asset data]}]}} + + + ([receiver asset] + + (transfer receiver + asset + nil)) + + + ([receiver asset data] + + (cond + (vector? asset) + (let [[asset-address + quantity] asset] + (cond + (callable? receiver + 'receive-asset) + (do + (call asset-address + (offer receiver + quantity)) + (call receiver + (receive-asset asset + data))) + + (actor? receiver) + (fail "Receiver does not have receive-asset function") + + (account? receiver) + (call asset-address + (direct-transfer receiver + quantity)) + + :else + (fail "Address cannot receive asset"))) + + (map? asset) + (reduce (fn [acc entry] + (assoc acc + (first entry) + (transfer receiver + entry + data))) + {} + asset) + + :else + (fail "Invalid asset")))) + + +;;;;;;;;;; Ownership + + +(defn balance + + ^{:doc {:description ["Returns asset balance for a specified owner, or the current address if not supplied." + "Return value will be in the quantity format as specified by the asset type."] + :examples [{:code "(balance asset-address owner)"}] + :signature [{:params [asset-address]} + {:params [asset-address owner]}]}} + + + ([asset-address] + + (query (call asset-address + (balance *address*)))) + + + ([asset-address owner] + + (query (call asset-address + (balance owner))))) + + + +(defn owns? + + ^{:doc {:description "Tests whether owner owns at least a given quantity of an asset", + :examples [{:code "(owns? owner [fungible-token-address 1000])"} + {:code "(owns? owner [non-fungible-token-address #{1 4 6}])"}] + :signature [{:params [owner asset]}]}} + + [owner asset] + + (query + (cond + (vector? asset) + (let [[asset-address + quantity] asset + bal (call asset-address + (balance owner))] + (call asset-address + (quantity-subset? quantity + bal))) + + (map? asset) + (reduce (fn [result [asset-address quantity]] + (if (call asset-address + (quantity-subset? quantity + (call asset-address + (balance owner)))) + true + (reduced false))) + true + asset) + + ;; Interpret nil as the 'zero' asset, which everybody owns + (nil? asset) + true))) + + +;;;;;;;;;; Quantities + + +(defn quantity-add + + ^{:doc {:description ["Adds two asset quantities. Quantities must be specified in the format required by the asset type." + "Nil may be used to indicate the 'zero' quantity."] + :examples [{:code "(quantity-add fungible-token 100 1000)"} + {:code "(quantity-add non-fungible-token #{1 2} #{3 4})"} + {:code "(quantity-add [token-a 100] [token-b 1000])"}] + :signature [{:params [asset-a asset-b]} + {:params [asset-address a b]}]}} + + + ([asset-a asset-b] + + (let [asset-a (-make-map asset-a) + asset-b (-make-map asset-b)] + (reduce (fn [m [asset-address qb]] + (let [qa (get m + asset-address)] + (assoc m + asset-address + (quantity-add asset-address + qa + qb)))) + asset-a + asset-b))) + + + ([asset-address a b] + + (query (call asset-address + (quantity-add a + b))))) + + + +(defn quantity-sub + + ^{:doc {:description ["Subracts a quantity from another quantity for a given asset. Quantities must be specified in the format required by the asset type." + "Subtracting a larger amount from a smaller amount should return 'zero' or equivalent, although the exact meaning of this operation may be asset-specific." + "Nil may be used to indicate the 'zero' quantity in inputs."] + :examples [{:code "(quantity-sub fungible-token 500 300)"} + {:code "(quantity-sub non-fungible-token #{1 2 3 4} #{2 3})"}] + :signature [{:params [asset-a asset-b]} + {:params [asset-address a b]}]}} + + + ([asset-a asset-b] + + (let [asset-a (-make-map asset-a) + asset-b (-make-map asset-b)] + (reduce (fn [m [asset-address qb]] + (let [qa (get m + asset-address)] + (if (= qa + qb) + (dissoc m + asset-address) + (assoc m + asset-address + (quantity-sub asset-address + qa + qb))))) + asset-a + asset-b))) + + + ([asset-address a b] + + (query (call asset-address + (quantity-sub a + b))))) + + + +(defn quantity-zero + + ^{:doc {:description "Returns the unique 'zero' quantity for the given asset." + :examples [{:code "(quantity-zero fungible-token)" + :result 0} + {:code "(quantity-zero non-fungible-token)" + :result #{}}] + :signature [{:params [asset-address]}]}} + + [asset-address] + + (query (call asset-address + (quantity-add nil + nil)))) + + + +(defn quantity-contains? + + ^{:doc {:description "Returns true if first quantity is >= second quantity. Any valid quantity must contain the 'zero' quantity." + :examples [{:code "(quantity-contains? fungible-token 100 60)" + :result true} + {:code "(quantity-contains? non-fungible-token #{1 2} #{2 3})" + :result false}] + :signature [{:params [asset-a asset-b]} + {:params [asset a b]}]}} + + + ([asset-a asset-b] + + (query + (let [asset-a (-make-map asset-a) + asset-b (-make-map asset-b)] + (reduce (fn [m [asset-address qb]] + (let [qa (get asset-a + asset-address)] + (cond + (= qa + qb) + true + + (call asset-address + (quantity-subset? qb + qa)) + true + + :else + (reduced false)))) + true + asset-b)))) + + + ([asset-address a b] + + (query (call asset-address + (quantity-subset? b + a))))) diff --git a/convex-core/src/main/cvx/convex/core.cvx b/convex-core/src/main/cvx/convex/core.cvx new file mode 100644 index 000000000..8f5e7d18f --- /dev/null +++ b/convex-core/src/main/cvx/convex/core.cvx @@ -0,0 +1,692 @@ +;; +;; +;; Core definitions executed as part of core runtime environment bootstrap, supplementing utilities defined in Java. +;; +;; First are defined core building blocks which must be kept at the beginning of the file. Order matters! +;; Then is defined the rest of the API in alphabetical order. +;; +;; + + +;;;;;;;;;; Values + + +(def *lang* + + ^{:doc {:description ["Advanced feature. Language font-end function." + "If set to a function via `def`, will be called with the code for each transaction instead of delegating to normal `eval` behavior." + "Pre-compiled operations (see `compile`) bypass this language setting."] + :examples [{:code "(def *lang* (fn [trx] (str trx)))"}]}} + + nil) + + + +(def *registry* + + ^{:doc {:description "Address of the Convex registry actor." + :examples [{:code "(call *registry* (register {:name \"My name\"}))"}]}} + + (address 9)) + + +;;;;;;;;;; Expanders, creating macros and defining functions + + +;; TODO. Review expanders and `macro`, API is not clear. + macros cannot be used within the transaction where they are created + + +(def defexpander + + ^{:doc {:description "Advanced feature. Defines an expander in the current environment." + :examples [{:code "(defexpander expand-once [x e] (e x (fn [x e] x)))"}] + :signature [{:params [a]}]} + :expander? true} + + (fn [x e] + (let [[_ + name + & decl] x + exp (cons 'fn + decl) + form `(def ~(syntax name + {:expander? true}) + ~exp)] + (e form + e)))) + + + +(def defmacro + + ^{:doc {:description ["Like `defn` but defines a macro instead of a regular function." + "A macro is a special function that is executed at expansion, before compilation, and produces valid Convex Lisp code for subsequent execution."] + :signature [{:params [name params & body]}]} + :expander? true} + + (fn [x e] + (let [[_ + name + & decl] x + mac (cons 'fn + decl) + mmeta (meta (first decl)) + form `(def ~(syntax name + (merge mmeta + {:expander? true})) + (let [m# ~mac] + (fn [x e] + (e (apply m# + (next x)) + e))))] + (e form + e)))) + + + +(defmacro defn + + ^{:doc {:description "Defines a function in the current environment." + :examples [{:code "(defn my-square [x] (* x x))"}] + :signature [{:params [name params & body]} + {:params [name & fn-decls]}]}} + + [name & decl] + + (let [fnform (cons 'fn + decl) + _ (cond + (empty? decl) + (fail :ARITY + "`defn` requires at lest one function definition")) + fst (first decl) + name (cond + (syntax fst) + (syntax name + (meta fst)) + + name)] + `(def ~name + ~fnform))) + + + +(defmacro macro + + ^{:doc {:description "Creates an anonymous macro function, suitable for use as an expander." + :examples [{:code "(macro [x] (if x :foo :bar))"}] + :signature [{:params [params & body]}]}} + + [& decl] + + (let [mac (cons 'fn + decl) + form `(let [m# ~mac] + (fn [x e] + (e (apply m# + (next (unsyntax x))) + e)))] + form)) + + +;;;;;;;;;; Logic + + +(defmacro and + + ^{:doc {:description ["Executes expressions in sequence, returning the first falsey value (false or nil), or the last value otherwise." + "Does not evaluate later expressions, so can be used to short circuit execution." + "Returns true with no expressions present."] + :examples [{:code "(and (< 1 2) :last)"}] + :signature [{:params [& exprs]}]}} + + [& exprs] + + (let [n (count exprs)] + (cond + (== n 0) true + (== n 1) (first exprs) + :else `(let [v# ~(first exprs)] + (cond v# + ~(cons 'and + (next exprs)) + v#))))) + + + +(defmacro or + + ^{:doc {:description ["Executes expressions in sequence, returning the first truthy value, or the last value if all were falsey (false or nil)." + "Does not evaluate later expressions, so can be used to short circuit execution." + "Returns nil with no expressions present."] + :examples [{:code "(or nil 1)"}] + :signature [{:params [& exprs]}]}} + + [& exprs] + + (let [n (count exprs)] + (cond + (== n 0) nil + (== n 1) (first exprs) + :else `(let [v# ~(first exprs)] + (cond + v# + v# + ~(cons 'or + (next exprs))))))) + + +;;;;;;;;;; `cond` variants + + +(defmacro if + + ^{:doc {:description ["If `test` expression evaluates to a truthy value (anything but false or nil), executes `expr-true`. Otherwise, executes `expr-false`." + "For a more general conditional expression that can handle multiple branches, see `cond.` Also see `when`."] + :examples [{:code "(if (< 1 2) :yes :no)"}] + :signature [{:params [test expr-true]} + {:params [test expr-true expr-false]}]}} + + [test & cases] + + (cond (<= 1 + (count cases) + 2) + nil + (fail :ARITY + "`if` requires 2 or 3 arguments")) + (cons 'cond + test + cases)) + + + +(defmacro if-let + + ^{:doc {:description "Similar to `if`, but the test expression in bound to a symbol so that it can be accessed in the `expr-true` branch." + :examples [{:code "(if-let [addr (some-function)] (transfer addr 1000) (fail \"Address missing\"))"}] + :signature [{:params [[sym exp] expr-true expr-false]}]}} + + [[sym exp] & branches] + + `(let [~sym ~exp] + ~(cons 'if + sym + branches))) + + + +(defmacro when + + ^{:doc {:description "Executes body expressions in an implicit `do` block if af and only if the `test` expression evaluates to a truthy value (anything but false or nil)." + :examples [{:code "(when (some-condition) (def foo 42) (+ 2 2))"}] + :signature [{:params [test & body]}]}} + + [test & body] + + `(cond + ~test + ~(cons 'do + body) + nil)) + + + +(defmacro when-let + + ^{:doc {:description ["Executes the body with the symbol bound to the value of evaluating a given expression, if and only if the result of the expression is truthy." + "Returns nil otherwise."] + :examples [{:code "(when-let [addr (some-function)] (transfer addr 1000))"}] + :signature [{:params [[sym exp] & body]}]}} + + [[sym exp] & body] + + (let [dobody (cons 'do + body)] + `(let [~sym ~exp] + (if ~sym + ~dobody + nil)))) + + + +(defmacro when-not + + ^{:doc {:description "Like `when` but the opposite: body is executed only if the result is false or nil." + :examples [{:code "(when-not (some-condition) :okay)"}] + :signature [{:params [test & body]}]}} + + [test & body] + + `(cond + ~test + nil + ~(cons 'do + body))) + + +;;;;;;;;;; Rest of the API + + +(defn account? + + ^{:doc {:description "Returns true if the given address refers to an existing actor or user account, false otherwise." + :examples [{:code "(account? *caller*)"}] + :signature [{:params [address] + :return Boolean}]}} + + [addr] + + (cond + (address? addr) + (boolean (account addr)) + false)) + + + +(defn actor? + + ^{:doc {:description "Returns true if the given address refers to an actor." + :examples [{:code "(actor? #1345)"}] + :signature [{:params [address] + :return Boolean}]}} + + [addr] + + (cond + (address? addr) + (let [act (account addr)] + (cond act + (nil? (:key act)) + false)) + false)) + + + +(defmacro assert + + ^{:doc {:description "Evaluates each test (a form), and raises an `:ASSERT` error if any are not truthy." + :errors {:ASSERT "If a `test` form evaluates to false or nil."} + :examples [{:code "(assert (= owner *caller*))"}] + :signature [{:params [& tests]}]}} + + [& tests] + + (cons 'do + (map (fn [test] + `(cond + ~test + nil + (fail :ASSERT + ~(str "Assert failed: " + (str test))))) + tests))) + + + +(defmacro call + + ^{:doc {:description ["Calls a function in another account, optionally offering coins which the account may receive using `accept`." + "Must refer to a callable function defined in the actor, called with appropriate arguments."] + :errors {:ARGUMENT "If the offer is negative." + :ARITY "If the supplied arguments are the wrong arity for the called function." + :CAST "If the address argument is an Address, the offer is not a Long, or the function name is not a Symbol." + :STATE "If the address does not refer to an Account with the callable function specified by fn-name."} + :examples [{:code "(call some-contract 1000 (contract-fn arg1 arg2))"}] + :signature [{:params [address call-form] + :return Any} + {:params [address offer call-form] + :return Any}]}} + + [addr & more] + + (let [addr (unsyntax addr)] + (if (empty? more) + (fail :ARITY + "Insufficient arguments to call")) + (let [n (count more) + fnargs (unsyntax (last more)) + _ (or (list? fnargs) + (fail :COMPILE + "`call` must have function call list form as last argument.")) + sym (unsyntax (first fnargs)) + fnlist (cons (list 'quote + sym) + (next fnargs))] + (cond + (== n 1) (cons 'call* + addr + 0 + fnlist) + (== n 2) (cons 'call* + addr + (first more) + fnlist))))) + + + +(defn comp + + ^{:doc {:description ["Returns a function that is the composition of the given functions." + "Functions are executed left to right, The righmost function may take a variable number of arguments." + "The result of each function is passed to the next one."] + :examples [{:code "((comp inc inc) 1)"}] + :signature [{:params [f & more] + :return Function}]}} + + + ([f] + + f) + + + ([f g] + + (fn [& args] + (f (apply g + args)))) + + + ([f g h] + + (fn [& args] + (f (g (apply h + args))))) + + + ([f g h & more] + + (apply comp + (fn [x] + (f (g (h x)))) + more))) + + + +(defn create-account + + ^{:doc {:description "Creates an account with the specified account public key and returns its address." + :errors {:CAST "If the argument is not a blob key of 32 bytes."} + :examples [{:code "(create-account 0x817934590c058ee5b7f1265053eeb4cf77b869e14c33e7f85b2babc85d672bbc)"}] + :signature [{:params [key] + :return Address}]}} + + [key] + + (or (blob? key) + (fail :CAST + "create-account requires a blob key")) + (deploy `(set-key ~key))) + + +(defmacro defined? + + ^{:doc {:description "Returns true if the given symbol name is defined in the current environment, false otherwise." + :examples [{:code "(defined? max)"}] + :signature [{:params [sym]}]}} + + [sym] + + (or (symbol? sym) + (fail :CAST + "defined? requires a Symbol")) + `(boolean (lookup-meta (quote ~sym)))) + + + +(defmacro doc + + ^{:doc {:description "Returns the documentation for a given definition." + :examples [{:code "(doc count)"}] + :signature [{:params [sym]}]}} + + ;; Accepts actual symbols or lookups. + + [sym] + + `(:doc ~(if (symbol? sym) + `(lookup-meta (quote ~sym)) + `(lookup-meta ~(nth sym + 1) + (quote ~(nth sym + 2)))))) + + + +(defmacro dotimes + + ^{:doc {:description ["Repeats execution of the body `count` times, binding the specified symbol from 0 to `(- count 1)` on successive iterations." + "Always Returns nil."] + :examples [{:code "(dotimes [i 10] (transfer *address* 10))"}] + :signature [{:params [[sym count] & body]}]}} + + [[sym count] & body] + + (let [n (long count) + sym (if (symbol? (unsyntax sym)) + sym + (fail :CAST + "`dotimes` requires a symbol for loop binding"))] + `(loop [~sym 0] + (if (< ~sym + ~n) + (do + ~(cons do + body) + (recur (inc ~sym))) + nil)))) + + + +(defn filter + + ^{:doc {:description ["Filters a collection by applying the given predicate to each element." + "Each element is included in in a new collection if and onfly if the predicate returns a truthy value (anything but false or nil)."] + :errors {:CAST "If the coll argeument is not a Data Structure."} + :examples [{:code "(filter (fn [x] (> 2 x)) [1 2 3 4])"}] + :signature [{:params [key] + :return Address}]}} + + [pred coll] + + (reduce (fn [acc e] + (cond (pred e) + (conj acc + e) + acc)) + (empty coll) + ;; Lists must be reversed so that elements are conjed in the correct order. + ;; + (cond (list? coll) + (reverse coll) + coll))) + + + +(defmacro for + + ^{:doc {:description "Executes the body with the symbol `sym` bound to each value of the given sequence. Returns a vector of results." + :examples [{:code "(for [x [1 2 3]] (inc x))"}] + :signature [{:params [[sym sequence] & body]}]}} + + [[sym sequence] & body] + + `(map ~(cons 'fn + (vector sym) + body) + (vec ~sequence))) + + + +(defn identity + + ^{:doc {:description "An identity function which returns a single argument unchanged. Most useful when you want a \"do nothing\" operation in higher order functions." + :examples [{:code "(identity :foo)"} + {:code "(map identity [1 2 3])"}] + :signature [{:params [x]}]}} + + [x] + + x) + + + +(defmacro import + + ^{:doc {:description ["Imports a library for use in the current environment." + "Creates an alias to the library so that symbols defined in the library can be addressed directly in the form 'alias/symbol-name'." + "Returns the address of the imported account."] + :examples [{:code "(import some.library :as alias)"}] + :signature [{:params [& args]}]}} + + [addr as sym] + + (let [code (cond (symbol? addr) + `(or (call* *registry* + 0 + 'cns-resolve + (quote ~addr)) + (fail :NOBODY + (str "Could not resolve library name for import: " + (quote ~addr)))) + `(address ~addr)) + sym (cond (symbol? sym) + sym + (fail "import: alias must be a symbol"))] + (assert (= :as + as)) + `(def ~sym + ~code))) + + + +(defn mapcat + + ^{:doc {:description "Maps a funcion across the given collections, then concatenates the results. Nil is treated as an empty collection. See `map`." + :examples [{:code "(mapcat vector [:foo :bar :baz] [1 2 3])"}] + :signature [{:params [test & body]}]}} + + [f coll & more] + + (apply concat + (empty coll) + (apply map + f + coll + more))) + + + +(defn mapv + + ^{:doc {:description "Like `map` but systematically returns the result as a vector." + :examples [{:code "(mapv inc '(1 2 3))"}] + :signature [{:params [f & colls] }]}} + + [f & colls] + + (vec (apply map + f + colls))) + + + +(defn max + + ^{:doc {:description "Returns the numerical maximum of the given values." + :examples [{:code "(max 1 2 3)"}] + :signature [{:params [& numbers]}]}} + + [fst & more] + + (let [n (count more)] + (loop [m (+ fst + 0) ;; Adds zero to ensure number. + i 0] + (cond (>= i + n) + m + (let [v (nth more i)] + (and (nan? v) + (return v)) + (recur (cond (> v + m) + v + m) + (inc i))))))) + + + +(defn min + + ^{:doc {:description "Returns the numerical minimum of the given values." + :examples [{:code "(min 1 2 3)"}] + :signature [{:params [& numbers]}]}} + + [fst & more] + + (let [n (count more)] + (loop [m (+ fst 0) + i 0] ;; Adds zero to ensure number. + (cond (>= i + n) + m + (let [v (nth more + i)] + (and (nan? v) + (return v)) + (recur (cond (< v + m) + v + m) + (inc i))))))) + + + +(defmacro schedule + + ^{:doc {:description "Schedules a transaction for future execution under this account. Expands and compiles code now, but does not execute until the specified timestamp." + :examples [{:code "(schedule (+ *timestamp* 1000) (transfer my-friend 1000000))"}] + :signature [{:params [timestamp code]}]}} + + [timestamp code] + + `(schedule* ~timestamp + (compile ~(list 'quote + code)))) + + + +(defmacro tailcall + + ^{:doc {:description ["Advanced feature. While `return` stops the execution of a function and return, `tailcall` calls another one without consuming additional stack depth." + "Rest of the current function will never be executed."] + :examples [{:code "(tailcall (some-function 1 2 3))"}] + :signature [{:params [[f & args]] }]}} + + [callspec] + + (let [] + (or (list? callspec) + (fail :ARGUMENT + "Tailcall requires a list representing function invocation")) + (let [n (count callspec)] + (cond (== n + 0) + (fail :ARGUMENT + "Tailcall requires at least a function argument in call list")) + (cons 'tailcall* + callspec)))) + + + +(defmacro undef + + ^{:doc {:description "Opposite of `def`. Undefines a symbol, removing the mapping from the current environment if it exists." + :examples [{:code "(do (def foo 1) (undef foo))"}] + :signature [{:params [sym]}]}} + + [sym] + + `(undef* ~(list 'quote + sym))) diff --git a/convex-core/src/main/cvx/convex/core/metadata.cvx b/convex-core/src/main/cvx/convex/core/metadata.cvx new file mode 100644 index 000000000..5761dbb6a --- /dev/null +++ b/convex-core/src/main/cvx/convex/core/metadata.cvx @@ -0,0 +1,1158 @@ +;; +;; +;; Metadata for core symbols that are implemented at the level of the CVM: +;; +;; - Functions defined in Java code +;; - Convex Lisp special forms +;; +;; + + +{*address* + {:doc {:description "Returns the address of the current account (user address in regular transaction, actor address in actor calls)." + :examples [{:code "(address? *address*)"}]} + :special? true} + + + *memory* + {:doc {:description "Returns the current memory Allowance for this account. May be zero - in which case any new memory allocations will be charged at the current memory exchange pool price." + :examples [{:code "*memory*"}]} + :special? true} + + + *balance* + {:doc {:description ["Returns the available balance of the current account (in Convex coins)." + "The available balance excludes reserved balance for transaction execution, so this number may be somewhat less than the total account balance during transaction execution."] + :examples [{:code "*balance*"}]} + :special? true} + + + *caller* + {:doc {:description "During an actor call, returns the address of the account doing the call. Nil otherwise." + :examples [{:code "*caller*"}]} + :special? true} + + + *depth* + {:doc {:description ["Returns the CVM execution stack depth, at the point the `*depth*` operation is executed. If the depth becomes too deep, the transaction will fail with a `:DEPTH` exception." + "InformativeIn most cases, the allowable depth should be sufficient."] + :examples [{:code "*depth*"}]} + :special? true} + + + *holdings* + {:doc {:description ["Returns the holdings blob map for this account." + "Holdings are data values controlled by other accounts (usually actors). They can be used to indicate that an account may have special rights or holdings with respect to a specific actor, for instance." + "See `get-holding`, `set-holding`."] + :examples [{:code "*holdings*"}]} + :special? true} + + + *initial-expander* + {:doc {:description "Initial expander used to expand forms, before compilation." + :examples [{:code "(expand '(if 1 2 3) *initial-expander*)"}] + :signature [{:params [form cont]}]}} + + + *juice* + {:doc {:description ["Returns the amount of execution juice remaining at this point of the current transaction." + "Juice is required for every CVM operation executed, and the transaction will fail immediately with a `:JUICE` error if an attempt is made to consume juice beyond this value."] + :examples [{:code "*juice*"}]} + :special? true} + + + *key* + {:doc {:description "Returns the public key for this account. Returns nil in the case of an actor since actors are accounts without keys." + :examples [{:code "*key*"}]} + :special? true} + + + *offer* + {:doc {:description "Returns the amount of native coin offered by `*caller*` during a call. See `call`. Will usually be zero, unless the caller has included an offer with a 'call' expression." + :examples [{:code "*offer*"}]} + :special? true} + + + *origin* + {:doc {:description ["Similar to `*caller*` but returns the address of the account that initially signed this transaction." + "In a chain of calls, this address is the very first link." + "Usually, should NOT be used for access control, since a rogue actor can potentially trick a user into creating a transaction that allows code to be indirectly executed. Consider using `*caller*` for access control instead."] + :examples [{:code "*origin*"}]} + :special? true} + + + *result* + {:doc {:description "Returns the result of the last CVM operation executed. Can be used, in some cases, to access the value of the previous expression. Will be nil for new transactions, or at the start of an actor call." + :examples [{:code "(do 1 *result*)"}]} + :special? true} + + + *sequence* + {:doc {:description "Returns the current sequence number for this account. The sequence number is equal to the number of signed transactions executed. The next valid sequence number is `(+ *sequence* 1)." + :examples [{:code "*sequence*"}]} + :special? true} + + + *state* + {:doc {:description "Returns the current CVM state record. This is a very large object, and should normally only be used temporarily to look up relevant values." + :examples [{:code "(keys *state*)"} + {:code "(get-in *state* [:accounts *address* :balance])"}]} + :special? true} + + + *timestamp* + {:doc {:description ["Returns the current timestamp." + "The timestamp is a `long` value that is equal to the greatest timestamp of any block executed (including the current block)." + "A timestamp can be interpreted as the number of milliseconds since January 1, 1970, 00:00:00 GMT. The block timestamp should always be less than or equal to the Unix timestamp of peers that are in consensus." + "Commonly used with `schedule`."] + :examples [{:code "*timestamp*"}]} + :special? true} + + + < + {:doc {:description "Tests if numeric arguments are in strict increasing order. Reads as 'less-than'." + :examples [{:code "(< 1 2 3)"}] + :signature [{:params [& xs] + :return Boolean}]}} + + + > + {:doc {:description "Tests if numeric arguments are in strict decreasing order. Reads as 'greater-than'." + :examples [{:code "(> 3 2 1)"}] + :signature [{:params [& xs] + :return Boolean}]}} + + + <= + {:doc {:description "Tests if numeric arguments are in increasing order. Reads as 'less-than-or-equal'." + :examples [{:code "(<= 1 1 3)"}] + :signature [{:params [& xs] + :return Boolean}]}} + + + >= + {:doc {:description "Tests if numeric arguments are in decreasing order. Reads as 'greater-than-or-equal'." + :examples [{:code "(>= 3 2 2)"}] + :signature [{:params [& xs] + :return Boolean}]}} + + + == + {:doc {:description ["Tests if arguments are equal in numerical value." + "Difference with `=` is that types are erased (eg. `long` value is comparable with a `double` value)."] + :examples [{:code "(== 2 2.0)" + :return "true"}] + :signature [{:params [& xs] + :return Boolean}]}} + + + = + {:doc {:description "Tests if arguments are equal in value." + :examples [{:code "(= :foo :foo)" + :return "true"}] + :signature [{:params [& vals] + :return Boolean}]}} + + + + + {:doc {:description "Adds numerical arguments. Result will be a `long` if all arguments are integers, or a double if any floating point values are included." + :examples [{:code "(+ 1 2 3)"}] + :signature [{:params [& xs]}]}} + + + - + {:doc {:description "Subtracts numerical arguments from the first argument. Negates a single argument." + :examples [{:code "(- 10 7)"}] + :signature [{:params [x]} + {:params [x y & more]}]}} + + + * + {:doc {:description "Multiplies numeric arguments. Result will be a `double` if any arguments are floating point values, otherwise it will be a `long`." + :examples [{:code "(* 1 2 3 4 5)"}] + :signature [{:params [& xs]}]}} + + + / + {:doc {:description "Double precision point divide. With a single argument, returns the reciprocal of a number. With multiple arguments, divides the first argument by the others in order." + :examples [{:code "(/ 10 3)"}] + :signature [{:params [divisor]} + {:params [numerator divisor]} + {:params [numerator divisor & more]}]}} + + + abs + {:doc {:description "Computes the absolute value of a numerical argument. Supports `double` and `long` results." + :errors {:CAST "If the parameter is not a number."} + :examples [{:code "(abs -1.5)"} + {:code "(abs 100)"}] + :signature [{:params [x] + :return Number}]}} + + + accept + {:doc {:description ["Used during a `call`, accepts offered coins up to the amount of `*offer*` from `*caller*`. Returns the amount accepted if successful." + "Amount must cast to `long`. If successful, the amount will be added immediately to the `*balance*` of the current `*address*`." + "This is the recommended way of transferring balance between actors, as it requires a positive action to confirm receipt."] + :errors {:ARGUMENT "If accepted amount is negative" + :CAST "If accepted amount is not a `long`" + :STATE "If ´*caller*´ has not offered sufficient coins to fulfil the offer"} + :examples [{:code "(accept *offer*)"}] + :signature [{:params [amount] + :return Number}]}} + + + account + {:doc {:description "Returns the account record for a given addess, or nil if the account does not exist. Argument must cast to `address`." + :errors {:CAST "If the argument is not a valid Address."} + :examples [{:code "(account *address*)"}] + :signature [{:params [address] + :return Account}]}} + + + address + {:doc {:description "Casts the argument to an `address`. Valid argument is a hex strings, a `long`, and `address` or a `blob` with the correct length (8 bytes)." + :errors {:CAST "If the argument is not castable to a valid `address`."} + :examples [{:code "(address 451)"}] + :signature [{:params [x] + :return Address}]}} + + + address? + {:doc {:description "Returns true if the argument is an actual `address`." + :examples [{:code "(address? #777)"} + {:code "(address? :foo)"}] + :signature [{:params [x] + :return Boolean}]}} + + + apply + {:doc {:description ["Applies a function to the specified arguments, after flattening the last argument. Last argument must be a sequential collection, or 'nil' which is considered an empty collection." + "Only useful in particular cases when interacting with variadic functions."] + :errors {:ARITY "If the additional arguments cause an arity error in the applied function." + :CAST "If the first argument is not castable to a valid function."} + :examples [{:code "(apply + [1 2 3])"} + {:code "(apply + 1 2 [3 4 5])"}] + :signature [{:params [f & args more-args] + :return Any}]}} + + assoc + {:doc {:description "Adds entries into an associative data structure, taking each two arguments as key/value pairs. A nil data structure is considered as an empty map." + :errors {:ARITY "If the additional arguments are not an even number (key and value pairs)." + :ARGUMENT "If one or more of the supplied keys is invalid for the data structure." + :CAST "If the first argument is not a valid DataStructure."} + :examples [{:code "(assoc {1 2} 3 4)"}] + :signature [{:params [coll & kvs] + :return DataStructure}]}} + + + assoc-in + {:doc {:description "Associates a value entries into an nested associative data structure, as if using `assoc` at each level." + :errors {:ARGUMENT "If one or more of the supplied keys is invalid for the data structure." + :CAST "If the first argument is not a valid data structure, or the second argument is not a a sequential data structure."} + :examples [{:code "(assoc-in {1 [1 2 3]} [1 2] 4)"}] + :signature [{:params [coll keys v] + :return DataStructure}]}} + + balance + {:doc {:description "Returns the coin balance of the specified account, which must be an `address`. Returns nil if and only the account does not exist." + :errors {:CAST "If the argument is not a valid address."} + :examples [{:code "(balance *caller*)"}] + :signature [{:params [address] + :return Long}]}} + + + blob + {:doc {:description "Casts the argument to a blob. Arugment can be an `address`, a hex string, or another `blob`." + :errors {:CAST "If the argument is not castable to a blob."} + :examples [{:code "(blob \"1234abcd\")"}] + :signature [{:params [address] + :return Blob}]}} + + + blob-map + {:doc {:description ["Creates a blob map. Blob maps support blob types as keys only (see `blob`)." + "Optional arguments must be pairs of blob keys and values to be included in the blob map."] + :errors {:ARGUMENT "If any of the keys supplied is not castable to a blob." + :ARITY "If there is not an even number of arguments (key and value pairs)."} + :examples [{:code "(blob-map 0x1234 :foo)"}] + :signature [{:params [& kvs] + :return BlobMap}]}} + + + blob? + {:doc {:description "Returns true if the argument is a `Blob`, false otherwise." + :examples [{:code "(blob? 0x1234)"}] + :signature [{:params [x] + :return Boolean}]}} + + + boolean + {:doc {:description "Casts any value to a `boolean`. Returns false if value is nil or actually `false`, true in any other case." + :examples [{:code "(boolean 123)"}] + :signature [{:params [x] + :return Boolean}]}} + + + boolean? + {:doc {:description "Returns true if the argument is a boolean (either `true` or `false`)." + :examples [{:code "(boolean? false)"}] + :signature [{:params [x] + :return Boolean}]}} + + + byte + {:doc {:description "Casts a value to a Byte. Discards high bits of larger integer types." + :errors {:CAST "If the argument is not castable to a Byte."} + :examples [{:code "(byte 1234)"}] + :signature [{:params [x] + :return Byte}]}} + + + call* + {:doc {:description ["Like `call` but lower-level. Instead of a form, takes a symbol referring to the function, and then arguments separately." + "Same kind of errors may happen."] + :errors {:ARITY "If the supplied arguments are the wrong arity for the called function." + :CAST "If the address argument is an Address, the offer is not a Long, or the function name is not a Symbol." + :STATE "If the address does not refer to an Account with the callable function specified by fn-name." + :ARGUMENT "If the offer is negative."} + :examples [{:code "(call* some-actor 1000 'contract-fn arg1 arg2)"}] + :signature [{:params [address offer fn-name & args] + :return Any}]}} + + + ceil + {:doc {:description "Computes the mathematical ceiling (rounding up towards positive infinity) for a numerical argument. Uses double precision mathematics." + :errors {:CAST "If the argument is not a number."} + :examples [{:code "(ceil 16.3)"}] + :signature [{:params [x] + :return Double}]}} + + + char + {:doc {:description "Casts a value to a `char`. Discards high bits of larger integer types." + :errors {:CAST "If the argument is not castable to a character."} + :examples [{:code "(char 97)"}] + :signature [{:params [x] + :return Character}]}} + + coll? + {:doc {:description "Returns true if the argument is a collection: list, map, set, or vector. Returns false otherwise." + :examples [{:code "(coll? [1 2 3])"}] + :signature [{:params [x] + :return Boolean}]}} + + + cond + {:doc {:description ["Performs conditional tests on successive pairs of `test` -> `result`, returning the `result` for the first `test` that succeeds." + "Performs short-circuit evaluation: result expressions that are not used and any test expressions after the first success will not be executed." + "In the case that no test succeeds, a single aditional argument may be added as a fallback value. If no fallback value is available, nil will be returned."] + :examples [{:code "(cond test-1 result-1 else-value)"} + {:code "(cond test-1 result-1 test-2 result-2 test-3 result--3)"}] + :signature [{:params []} + {:params [test]} + {:params [test result]} + {:params [test result fallback-value]} + {:params [test-1 result-1 test-2 result-2 & more]}]} + :special? true} + + + compile + {:doc {:description "Compiles a form, returning an operation. See `eval`." + :errors {:COMPILE "If a compiler error occurs."} + :examples [{:code "(compile '(fn [x] (* x 2)))"}] + :signature [{:params [form] + :return Op}]}} + + + concat + {:doc {:description "Concatenates sequential data structures (lists or maps), returning a new sequential data structure of the same type as the first non-nil argument. Nil is treated as an empty sequence." + :errors {:CAST "If any of the arguments is neither a sequential data structure nor nil."} + :examples [{:code "(concat [1 2] [3 4])"}] + :signature [{:params [& seqs] + :return DataStructure}]}} + + + conj + {:doc {:description "Adds elements to a data structure, in the natural mode of addition for the data structure. Supports sequential collections, sets and maps." + :errors {:ARGUMENT "If a provided element is not of correct type for the given data structure." + :CAST "If the first argument is not a data structure (or nil)."} + :examples [{:code "(conj [1 2] 3)"} + {:code "(conj {1 2} [3 4])"} + {:code "(conj #{1 2} 3)"}] + :signature [{:params [coll & elems] + :return DataStructure}]}} + + cons + {:doc {:description "Constructs a list, by prepending the leading arguments to the last argument. The last argument must be coercable to a sequential data structure." + :errors {:CAST "If the last argument is not a sequence."} + :examples [{:code "(cons 1 '(2 3))"} + {:code "(cons 1 2 '(3 4))"}] + :signature [{:params [arg & more-args coll]}]}} + + + contains-key? + {:doc {:description "Returns true if the given associative data structure contains the given key, false otherwise." + :examples [{:code "(contains-key? {:foo 1 :bar 2} :foo)"}] + :signature [{:params [coll key] + :return Boolean}]}} + + + create-peer + {:doc {:description "Creates a new peer on the network. The peer must have an account number and sufficient balance to place a stake amount" + :errors {:CAST "If the first argument is not a valid account-key."} + :examples [{:code "(create-peer account-key 700000000)"}] + :signature [{:params [account-key stake-amount] + :return stake-amount}]}} + + + count + {:doc {:description "Returns the number of elements in the given collection, blob, or string." + :errors {:CAST "If the argument is not a countable object."} + :examples [{:code "(count [1 2 3])"}] + :signature [{:params [coll] + :return Long}]}} + + dec + {:doc {:description "Decrements the given `long` by 1." + :errors {:CAST "If the argument is not a long."} + :examples [{:code "(dec 10)"}] + :signature [{:params [long] + :return Long}]}} + + + def + {:doc {:description ["Creates a definition in the current environment. This value will persist in the environment owned by the current account." + "The name argument must be a symbol, or a Symbol wrapped in a syntax object with optional metadata."] + :errors {:CAST "If the argument is neither a valid symbol name nor a syntax containing a symbol value."} + :examples [{:code "(def a 10)"}] + :signature [{:params [name value]}]} + :special? true} + + + deploy + {:doc {:description "Deploys an actor. The code provided will be executed to initialise the actor's account. Returns the Address of the deployed actor." + :errors {:COMPILE "If a compiler error occurred deploying the given code."} + :examples [{:code "(deploy '(defn my-fn [x y] (+ x y)))"}] + :signature [{:params [code] + :return Address}]}} + + + difference + {:doc {:description "Computes the difference of one or more sets. Nil is accepted and treated like an empty set." + :errors {:CAST "If any of the arguments is neither a set nor nil."} + :examples [{:code "(difference #{1 2} #{2 3})"}] + :signature [{:params [set & more] + :return Set}]}} + + + disj + {:doc {:description "Removes the specified key(s) from a set. Nil is treated as an empty Set." + :errors {:CAST "If the first argument is not a set."} + :examples [{:code "(disj #{1 2 3} 1)"}] + :signature [{:params [coll key]}]}} + + + dissoc + {:doc {:description "Removes entries with the specified key(s) from a map or blob map." + :errors {:CAST "If the first argument is not a map."} + :examples [{:code "(dissoc {1 2 3 4} 3)"}] + :signature [{:params [coll & keys] + :return Map}]}} + + do + {:doc {:description "Executes multiple expressions sequentially, and returns the value of the final expression." + :examples [{:code "(do (count [1 2 3]) :done)"}] + :signature [{:params [& expressions]}]} + :special? true} + + + double + {:doc {:description "Casts any numerical value to a `double`." + :errors {:CAST "If the argument is not castable to double."} + :examples [{:code "(double 3)"}] + :signature [{:params [x] + :return Double}]}} + + empty + {:doc {:description "Returns an empty collection of the same type as the argument. `(empty nil)` returns nil." + :errors {:CAST "If the argument is neither nil nor a data structure."} + :examples [{:code "(empty [1 2 3])"}] + :signature [{:params [coll] + :return DataStructure}]}} + + + empty? + {:doc {:description "Checks if the argument is an empty collection. Nil is considered empty. " + :errors {:CAST "If the argument is neither nil nor a data structure."} + :examples [{:code "(empty? [])"}] + :signature [{:params [coll] + :return Boolean}]}} + + + encoding + {:doc {:description ["Returns the byte encoding for a given value as a blob." + "The encoding is the unique canonical binary representation of a value. Encodings may change between Convex versions - it is unwise to rely on the exact representation."] + :examples [{:code "(encoding {1 2})"}] + :signature [{:params [value] + :return Blob}]}} + + + eval + {:doc {:description "Evaluates code in the current context, expanding and compiling the form if necessary (see `expand`,`compile`)." + :errors {:COMPILE "If a compiler error occurred evaluating the form." + :EXPAND "If an expander error occurred while expanding the form."} + :examples [{:code "(eval '(+ 1 2))"}] + :signature [{:params [form]}]}} + + eval-as + {:doc {:description "Like `eval` but evaluates code in the environment of the specifed account. The current account must have controller privileges to execute this operation (see `set-controller`)." + :examples [{:code "(eval-as #666 '(+ 1 2))"}] + :signature [{:params [address form]}]}} + + + exp + {:doc {:description "Returns `e` raised to the power of the given numerical argument." + :errors {:CAST "If the argument is not a number."} + :examples [{:code "(exp 1.0)"}] + :signature [{:params [x] + :return Double}]}} + + + expand + {:doc {:description ["Expands the given form." + "Uses the specified expander (including macros) as the primary expander if provided, the default `*initial-expander*` otherwise." + "If also provided, a continuation expander will be passed to the primary expander, otherwise the primary will be used as its own continuation." + "Expanders are an advanced feature."] + :examples [{:code "(expand '(if a :truthy :falsey))"}] + :signature [{:params [form]} + {:params [form expander]} + {:params [form expander cont]}]}} + + + callable? + {:doc {:description "Returns true the given function name (a symbol) is a callable function in actor. See `call`." + :errors {:CAST "If the actor argument is not an address."} + :examples [{:code "(callable? actor-address 'function-name)"}] + :signature [{:params [actor symbol]}]}} + + + fail + {:doc {:description ["Causes execution to fail at the current position." + "Error type defaults to `:ASSERT` if not specified, and cannot be nil. Typically a keyword, it can actually be any value." + "Error message defaults to nil if not specified. The message may be any value, but the use of short descriptive strings is recommended."] + :examples [{:code "(fail :ASSERT \"Assertion failed\")"}] + :signature [{:params []} + {:params [message]} + {:params [error-type message]}]}} + + + first + {:doc {:description "Returns the first element from a collection which must contain at least one element. Also see `empty?`" + :errors {:BOUNDS "If the collection is empty." + :CAST "If the first argument is not a countable collection."} + :examples [{:code "(first [1 2 3])"}] + :signature [{:params [coll]}]}} + + floor + {:doc {:description "Computes the mathematical floor (rounding down towards negative infinity) for a numerical argument. Uses double precision mathematics." + :examples [{:code "(floor 16.3)"}] + :signature [{:params [x] + :return Double}]}} + + + fn + {:doc {:description "Creates an anonymous function (closure) with the specified argument list and function body. Will close over variables in the current lexical scope." + :examples [{:code "(let [f (fn [x y] (* x y))] (f 10 7))"}] + :signature [{:params [args & body]}]} + :special? true} + + + fn? + {:doc {:description "Returns true if the argument is a function, false otherwise. Some values may sometimes operate as functions but are not functions themselves (eg. maps and vectors)." + :examples [{:code "(fn? count)"}] + :signature [{:params [x] + :return Boolean}]}} + + get + {:doc {:description ["Gets an element from a collection at the specified index value. Works on all collection types including maps, sets and sequences. Nil is treated as an empty collection." + "If the index is not present, returns `not-found` value (nil by default)."] + :examples [{:code "(get {:foo 10 :bar 15} :foo)"}] + :signature [{:params [coll key]} + {:params [coll key not-found]}]}} + + get-holding + {:doc {:description "Gets the holding value for a specified owner account address. Owner account must exist. Holding will be null by default. See `*holdings*`, `set-holding`." + :errors {:CAST "If the argument is not an address."} + :examples [{:code "(get-holding *caller*)"}] + :signature [{:params [owner]}]}} + + + get-in + {:doc {:description "Gets an element by successively looking up keys in a collection according to the logic of `get`. If any lookup does not find the appropriate key, will return `not-found` (nil by default)." + :errors {:CAST "If the first argument is not an associative collection."} + :examples [{:code "(get-in [[1 2] [3 4]] [1 1])"}] + :signature [{:params [coll keys]} + {:params [coll keys not-found]}]}} + + halt + {:doc {:description ["Completes execution in the current context with the specified result, or null if not provided. Does not roll back any state changes made." + "If the currently executing context is an actor, the result will be used as the return value from the actor call."] + :examples [{:code "(halt :we-are-finished-here)"}] + :signature [{:params []} + {:params [result]}]}} + + hash + {:doc {:description "Calculates the 32-byte SHA3-256 cryptographic hash of a `blob` or an `address` (which is a specialized type of `blob`). Returns a 32-byte `blob`." + :examples [{:code "(hash 0x1234)"} + {:code "(hash (encoding :foo))"}] + :signature [{:params [value] + :return Blob}]}} + + + hash-map + {:doc {:description "Constructs a map with the given keys and values. If a key is repeated, the last value will overwrite previous ones." + :errors {:ARITY "If the number of arguments is not even (key-value pairs)."} + :examples [{:code "(hash-map 1 2 3 4)"}] + :signature [{:params [& kvs]}]}} + + + hash-set + {:doc {:description "Constructs a set with the given values. If a value is repeated, it will be included only once in the set." + :examples [{:code "(hash-set 1 2 3)"}] + :signature [{:params [& values]}]}} + + + inc + {:doc {:description "Increments the given `long` by 1." + :errors {:CAST "If the actor argument is not a `long`."} + :examples [{:code "(inc 10)"}] + :signature [{:params [num] + :return Long}]}} + + + intersection + {:doc {:description "Computes the intersection of one or more sets. Nil is treated as an empty set." + :errors {:CAST "If any of the arguments is neither a set nor nil."} + :examples [{:code "(intersection #{1 2} #{2 3})"}] + :signature [{:params [set & more] + :return Set}]}} + + + into + {:doc {:description "Adds elements to a collection, in a collection-defined manner as with `conj`." + :errors {:ARGUMENT "If any of the elements is not a valid type for the given data structure." + :CAST "If either argument is not a data structure."} + :examples [{:code "(into {} [[1 2] [3 4]])"}] + :signature [{:params [coll elements] + :return DataStructure}]}} + + keys + {:doc {:description "Returns a vector of keys in the given map, in the map defined order. Also see `values`." + :errors {:CAST "If the argument is not a map."} + :examples [{:code "(keys {:foo 1 :bar 2})"}] + :signature [{:params [m] + :return Vector}]}} + + keyword + {:doc {:description "Coerces the argument to a keyword: a symbol, a keyword, or a string between 1 and 64 characters." + :errors {:ARGUMENT "If the keyword name is of illegal length." + :CAST "If the argument is not of a type castable to keyword."} + :examples [{:code "(keyword \"foo\")"}] + :signature [{:params [name] + :return Keyword}]}} + + + keyword? + {:doc {:description "Returns true if the argument is a keyword, false otherwise." + :examples [{:code "(keyword? :foo)"}] + :signature [{:params [x] + :return Boolean}]}} + + + last + {:doc {:description "Returns the last element of a data structure, in collection-defined order. Collection argument must be coercible to a sequential data structure." + :errors {:CAST "If the argument is not coercible to a sequential data structure (list or vector)."} + :examples [{:code "(last [1 2 3])"}] + :signature [{:params [coll]}]}} + + + let + {:doc {:description "Binds local variables according to symbol-expression pairs in a binding vectors, then execute following expressions in an implicit do block." + :examples [{:code "(let [x 10] (* x x))"}] + :signature [{:params [bindings & exps]}]} + :special? true} + + + list + {:doc {:description "Creates a list containing the given arguments as elements." + :examples [{:code "(list 1 2 3)"}] + :signature [{:params [& elements] + :return List}]}} + + + list? + {:doc {:description "Returns true if the argument is a list." + :examples [{:code "(list? :foo)"}] + :signature [{:params [x] + :return Boolean}]}} + + + log + {:doc {:description ["Outputs a sequence of values to the CVM log for the current account. Any valid CVM values can be logged. Returns a vector containing logged values." + "The CVM log is NOT stored on chain. It may be used by peers for external audits."] + :examples [{:code "(log :EVENT 123 [:some :data])"}] + :signature [{:params [& values] + :return Vector}]}} + + + long + {:doc {:description "Casts the given argument to a 64-bit signed integer." + :errors {:CAST "If the argument is not castable to `long`."} + :examples [{:code "(long 10)"}] + :signature [{:params [num]}]}} + + + long? + {:doc {:description "Returnes true if the argment is a `long` value, false otherwise." + :examples [{:code "(long? 1234)"}] + :signature [{:params [x] + :return Boolean}]}} + + + lookup + {:doc {:description "Looks up the value of a symbol in the current execution environment, or the account of the given address if specified." + :errors {:NOBODY "If the target account for lookup does not exist."} + :examples [{:code "(do (def a 13) (lookup a))"} + {:code "(lookup #8 count)"}] + :signature [{:params [sym]} + {:params [address sym]}]} + :special? true} + + + lookup-meta + {:doc {:description "Looks up metadata for a symbol in the current execution environment, or the account of the given address if specified. Returns nil if not found." + :examples [{:code "(lookup-meta 'count)"}] + :signature [{:params [name]} + {:params [address name]}]}} + + + loop + {:doc {:description ["Creates a loop body, binding one or more loop variables in a manner similar to `let`." + "Within the loop body, `recur` can be used to return to the start of the loop while re-binding the loop variables with new values. Does not consume stack."] + :examples [{:code "(loop [i 10 acc 1] (if (> i 1) (recur (dec i) (* acc i)) acc))"}] + :signature [{:params [bindings & body]}]} + :special? true} + + + map + {:doc {:description "Applies a function to each element of a data structure in sequence, and returns a vector of results. Additional collection may be provided to call a function with higher arity." + :examples [{:code "(map inc [1 2 3])"}] + :signature [{:params [f coll]} + {:params [f coll1 coll2 & more-colls ]}]}} + + + map? + {:doc {:description "Returns true if argument is a map, false otherwise." + :examples [{:code "(map? {1 2})"}] + :signature [{:params [coll] + :return Boolean}]}} + + merge + {:doc {:description "Merges zero or more maps (not blob maps), replacing existing values. Nil is considered as an empty map." + :examples [{:code "(merge {1 2 3 4} {3 5 7 9})"}] + :signature [{:params [& maps]}]}} + + + meta + {:doc {:description "Returns metadata for a `syntax` object. Returns nil if the argument is not a syntax object." + :examples [{:code "(meta (syntax 'foo {:bar 1}))"}] + :signature [{:params [syntax] + :return Map}]}} + + mod + {:doc {:description "Returns the integer modulus of a numerator divided by a divisor. The result will always be positive, in consistent with Euclidean Divsion." + :examples [{:code "(mod 13 5)"}] + :signature [{:params [num div]}]}} + + + nan? + {:doc {:description "Returns true if argment is `##NaN`. Returns false for any other value (including non-numerical arguments)." + :examples [{:code "(nan? ##NaN)"}] + :signature [{:params [x] + :return Boolean}]}} + + + name + {:doc {:description "Gets the string name of an object: a keyword, a symbol, or a string." + :errors {:CAST "If the argument is not castable to a String name."} + :examples [{:code "(name :foo)" + :return "\"foo\""}] + :signature [{:params [named-object] + :return String}]}} + + + next + {:doc {:description "Returns the elements of a sequential data structure after the first element, or nil if no more elements remain." + :errors {:CAST "If the argument is not a sequential data structure."} + :examples [{:code "(next [1 2 3])"}] + :signature [{:params [coll] + :return Sequence}]}} + + + nil? + {:doc {:description "Returns true if argument is nil, false otherwise." + :examples [{:code "(nil? nil)"}] + :signature [{:params [x] + :return Boolean}]}} + + + not + {:doc {:description "Inverts a truth value. Returns true on false or nil, returns false on any other value." + :examples [{:code "(not true)"} + {:code "(not nil)"}] + :signature [{:params [b] + :return Boolean}]}} + + + nth + {:doc {:description ["Gets the nth element of a countable data structure: a collection as defined in `collection?`, a blob, or a string." + "The index must be a valid long between 0 (inclusive) and the element count of the collection (exclusive)."] + :errors {:CAST "If the first argument is not countable data structure."} + :examples [{:code "(nth [1 2 3] 2)"}] + :signature [{:params [coll index]}]}} + + + number? + {:doc {:description "Returns true if the argument is a numeric value, false otherwise." + :examples [{:code "(number? 2.3)"}] + :signature [{:params [x] + :return Boolean}]}} + + + pow + {:doc {:description "Returns the first argument raised to the power of the second argument. Uses double precision maths." + :errors {:CAST "If the argument is not a Number."} + :examples [{:code "(pow 2 3)"}] + :signature [{:params [x y] + :return Double}]}} + + + quasiquote + {:doc {:description "Returns the quoted value of a form, without evaluating it. Like 'quote', but elements within the form may be unquoted via `unquote`." + :examples [{:code "(quasiquote foo)" + :return "foo"} + {:code "(quasiquote (:a :b (unquote (+ 2 3))))" + :return "(:a :b 5)"}] + :signature [{:params [form]}]} + :expander? true + :special? true} + + + query + {:doc {:description "Runs forms in query mode. When returning result, any state change will be rolled back as if nothing happened." + :examples [{:code "(query (def a 10) a)"} + {:code "(query (call unsafe-actor (do-something)))"}] + :signature [{:params [& forms]}]} + :special? true} + + + quot + {:doc {:description "Returns the quotient of a numerator divided by a divisor. Performs truncated division (ie. rounds towards zero)." + :examples [{:code "(quot 13 5)"}] + :signature [{:params [num div]}]}} + + + + quote + {:doc {:description "Returns the quoted value of a form, without evaluating it. For example, you can quote a symbol to get the symbol itself rather than the value in the environment that it refers to." + :examples [{:code "(quote foo)"} + {:code "(eval (quote (+ 1 2 3)))"}] + :signature [{:params [form]}]} + :expander? true + :special? true} + + + recur + {:doc {:description "Escapes from the currently executing code and recurs at the level of the next loop or function body." + :examples [{:code "(recur acc (dec i))"}] + :signature [{:params [x y]}]} + :special? true} + + + reduce + {:doc {:description ["Efficient and convenient looping mechanism." + "Reduces over a collection, calling `f` with an accumulator value and, successively each element of that collection (see example showing a summation)." + "Looping can be stopped at any moment by calling `reduced`. Otherwise, all elements in the collection are processed." + "If an initial accumulator value is not supplied, the initial value will be determined by calling the function on the first 0, 1 or 2 elements of the collection (however many are available)."] + :errors {:CAST "If the first argument is not a function, or the final argument is not a sequential collection."} + :examples [{:code "(reduce (fn [acc item] (+ acc item)) 0 [1 2 3 4 5])"}] + :signature [{:params [f coll]} + {:params [f init coll]}]}} + + + reduced + {:doc {:description "Returns immediately from the enclosing `reduce` function, providing the given value as the result of the whole `reduce` operation. This can be used to terminate early from a reduce operation, saving transaction costs." + :examples [{:code "(reduce (fn [acc x] (reduced :exit)) 1 [1 2 3 4 5])"}] + :signature [{:params [result]}]}} + + + rem + {:doc {:description "Returns the remainder of a numerator divided by a divisor, consistent with division performed by `quot`. The remainder will therefore have the same sign as the numerator." + :examples [{:code "(rem 13 5)"}] + :signature [{:params [num div]}]}} + + + return + {:doc {:description "Escapes from the currently executing code and returns the specified value from the current function. Expressions following `return` will not be executed." + :examples [{:code "(do (return :finished) 42)"}] + :signature [{:params [value]}]} + :special? true} + + + reverse + {:doc {:description "Reverses a sequential data structure. Lists are converted to vectors, and vice versa for efficieny reasons. Nil is treated as an empty vector." + :examples [{:code "(reverse [1 2 3])"}] + :signature [{:params [value] + :return Sequence}]} + :special? true} + + + rollback + {:doc {:description "Escapes from the currently executing smart contract. Rolls back any state changes, as if nothing happened during that transaction. Returns the given value." + :examples [{:code "(rollback :aborted)"}] + :signature [{:params [value]}]} + :special? true} + + + schedule* + {:doc {:description "Schedules a form for future execution under this account. Expands and compiles form now, but does not execute until the specified timestamp." + :examples [{:code "(schedule* (+ *timestamp* 1000) '(transfer my-friend 1000000))"}] + :signature [{:params [timestamp code]}]}} + + + second + {:doc {:description "Returns the second element of a countable collection." + :errors {:BOUNDS "If the argument does not have a second value" + :CAST "If the argument is not a countable collection."} + :examples [{:code "(second [1 2 3])"}] + :signature [{:params [coll]}]}} + + + set + {:doc {:description "Coerces any data structure to a set." + :errors {:CAST "If the argument is not a countable data structure."} + :examples [{:code "(set [1 2 3])"}] + :signature [{:params [coll] + :return Set}]}} + + + set! + {:doc {:description ["Sets a local binding identified by an unqualified symbol to the given value." + "This local change will be be visible until the scope leaves the current binding form (eg. `let` binding, function body or recur)." + "This is probably most useful for updating a local variable in imperative style. Returns the value assigned to the local binding if successful."] + :errors {:ARGUMENT "If the symbol is qualified."} + :examples [{:code "(let [a 10] (set! a 20) a)"}] + :signature [{:params [sym value]}]} + :special? true} + + + set-controller + {:doc {:description ["Sets the controller for the current account." + "Controller account is granted powerful access privileges including the ability to run `eval-as`. Setting to nil disable such control (default value)."] + :errors {:CAST "If the argument is neither a validaAddress nor nil" + :NOBODY "If the address does not refer to an existing account."} + :examples [{:code "(set-controller #9)"}] + :signature [{:params [addr]}]}} + + + set-holding + {:doc {:description "Sets the holding value for a specified owner account address. Owner account must exist. Returns the new holding value." + :errors {:CAST "If the first argument is not an address."} + :examples [{:code "(set-holding *caller* 1000)"}] + :signature [{:params [owner value]}]}} + + + set-key + {:doc {:description ["Sets the public key (32-byte blob) for this account. May set to nil to turn this account into an actor and disable future external transactions." + "WARNING: You may lose access to the account if you do not have access to the associated private key."] + :signature [{:params [new-key]}]}} + + + set-memory + {:doc {:description "Sets the free memory allowance for the current account address, in number of bytes. Increases in memory allowance may cost coin balance. Decreases in memory allowance may earn a coin refund." + :examples [{:code "(set-memory 10000)"}] + :signature [{:params [mem]}]}} + + + set-peer-data + {:doc {:description "Sets metadata on a given peer. Metadata must be a map. Peer must exist, with a public account key." + :errors {:CAST "If the first argument is not a valid peer Key, or the second argument is not a map."} + :examples [{:code "(set-peer-data peer-key {:url \"my-peer.com:4242\"})"}] + :signature [{:params [peer map]}]}} + + + set-peer-stake + {:doc {:description "Sets the peer stake. Stake must be a `long`. Peer must exist, with a public account key." + :errors {:CAST "If the first argument is not a valid peer key, or the second argument is not a `long`."} + :examples [{:code "(set-peer-data peer-key {:url \"my-peer.com:4242\"})"}] + :signature [{:params [peer stake]}]}} + + + set? + {:doc {:description "Returns true if the argument is a set, false otherwise." + :examples [{:code "(set? #{1 2 3})"}] + :signature [{:params [x] + :return Boolean}]}} + + + signum + {:doc {:description "Returns the signum of a numeric value, defined to be -1, 0 or 1. Results in a cast error if the argument is not a number (including `##NaN`)." + :examples [{:code "(signum -1)"}] + :signature [{:params [x] + :return Double}]}} + + + sqrt + { :doc {:description "Computes the square root of a numerical argument. Uses double precision mathematics. May return `##NaN` for negative values." + :examples [{:code "(sqrt 16.0)"}] + :signature [{:params [x] + :return Double}]}} + + + stake + {:doc {:description "Sets the stake on a given peer. Peer must exist, and funds must be available to set the stake to the specified level. Setting stake to zero removes the stake entirely." + :examples [{:code "(stake trusted-peer-account-key 7000000000)"}] + :signature [{:params [account-key amount]}]}} + + str + {:doc {:description "Coerces values into strings and concatenates them." + :examples [{:code "(str \"Hello \" name)"}] + :signature [{:params [& args] + :return String}]}} + + + str? + {:doc {:description "Returns true if the argument is a string, false otherwise." + :examples [{:code "(str? name)"}] + :signature [{:params [x] + :return Boolean}]}} + + + subset? + {:doc {:description "Returns true if `set-1` is a subset of `set-2u. Both arguments must be sets, nil being considered as an empty set." + :examples [{:code "(subset? #{1} #{1 2 3})"}] + :signature [{:params [set-1 set-2] + :return Boolean}]}} + + + symbol + {:doc {:description "Creates a symbol from keyword, a symbol, or a string between 1 and 64 character" + :examples [{:code "(symbol :foo)"}] + :signature [{:params [name] + :return Symbol}]}} + + + str? + {:doc {:description "Returns true if the argument is a string, false otherwise." + :examples [{:code "(str? \"foo\")"}] + :signature [{:params [x] + :return Boolean}]}} + + + symbol? + {:doc {:description "Returns true if the argument is a symbol, false otherwise." + :examples [{:code "(symbol? 'foo)"}] + :signature [{:params [x] + :return Boolean}]}} + + + syntax + {:doc {:description "Wraps a value as a syntax object, if it is not already one. If metadata is provided, merge the metadata into the resulting syntax object." + :examples [{:code "(syntax 'bar)"} + {:code "(syntax 'bar {:some :metadata})"}] + :signature [{:params [value] + :return Syntax} + {:params [value meta] + :return Syntax}]}} + + + syntax? + {:doc {:description "Returns true if the argument is a syntax object." + :examples [{:code "(syntax? form)"}] + :signature [{:params [x] + :return Boolean}]}} + + + tailcall* + {:doc {:description "Like `tailcall` but lower-level. Instead of a form, takes a function and then arguments separately." + :examples [{:code "(tailcall* + 1 2 3)"}] + :signature [{:params [f & args] }]}} + + + transfer + {:doc {:description "Transfers the specified amount of coins to the target `address`. Returns the amount transferred if successful." + :errors {:FUNDS "If there is insufficient balance in the sender's account." + :STATE "If the receiver is an actor that is unable to accept funds."} + :examples [{:code "(transfer #42 12345678)"}] + :signature [{:params [address amount] + :return Long}]}} + + + transfer-memory + {:doc {:description "Transfers the specified amount of memory allowance to the target `address`. Returns the amount transferred if successful." + :errors {:MEMORY "If there is insufficient balance in the sender's account."} + :examples [{:code "(transfer-memory my-friend-address 100)"}] + :signature [{:params [address amount] + :return Long}]}} + + + undef* + {:doc {:description "Undefines a symbol, removing the mapping from the current environment if it exists. Helper function for the 'undef' macro." + :examples [{:code "(undef* 'a)"}] + :signature [{:params [sym]}]}} + + + union + {:doc {:description "Computes the union of zero or more sets. Nil is treated as the empty set." + :examples [{:code "(union #{1 2} #{2 3})"}] + :signature [{:params [& sets] + :return Set}]}} + + unsyntax + {:doc {:description "Unwraps a value from a syntax object. If the argument is not a syntax object, returns it unchanged." + :examples [{:code "(unsyntax form)"}] + :signature [{:params [form]}]}} + + + values + {:doc {:description "Gets the values from a map. Also see `keys`." + :examples [{:code "(values {1 2 3 4})"}] + :signature [{:params [map] + :return Vector}]}} + + vec + {:doc {:description "Coerces the argument to a vector. Arguement must be coercible to a sequential data structure." + :examples [{:code "(vec #{1 2 3 4})"}] + :signature [{:params [coll] + :return Vector}]}} + + vector + {:doc {:description "Creates a vector with the given elements." + :examples [{:code "(vector 1 2 3)"}] + :signature [{:params [& elements] + :return Vector}]}} + + vector? + {:doc {:description "Returns true if the argument is a vector, false otherwise." + :examples [{:code "(vector? [1 2 3])"}] + :signature [{:params [x] + :return Boolean}]}} + + zero? + {:doc {:description "Returns true if the argument has the numeric value zero, false otherwise." + :examples [{:code "(zero? 0.1)"}] + :signature [{:params [x] + :return Boolean}]}} + +} diff --git a/convex-core/src/main/cvx/convex/fungible.cvx b/convex-core/src/main/cvx/convex/fungible.cvx new file mode 100644 index 000000000..7d98a565e --- /dev/null +++ b/convex-core/src/main/cvx/convex/fungible.cvx @@ -0,0 +1,380 @@ +'convex.fungible + + +(call *registry* + (register {:description ["Provides library functions for building and managing standard fungible assets." + "Quantity is expressed as a long representing the amount of an asset." + "The `build-token` function creates deployable code that follows the interface described in `convex.asset`."] + :name "Fungible token creation and management"})) + + +;;;;;;;;;; Building actors + + +(defn add-mint + + ^{:doc {:description ["Creates deployable code that, when added to actor code from `build-token`, allows priviledged accounts to mint and burn tokens." + "Configuration map contains:" + "- `:max-supply`, a long designating the maximum mintable supply (optional, defaults to 1000000000000000000, the allowed maximum)" + "- `:minter`, a single address or a Trust Monitor from `convex.trust` (mandatory)"] + :examples [{:code "(deploy [(build-token {}) (add-mint {:minter *address* :max-supply 1000000000})])"}] + :signature [{:params [config]}]}} + + [config] + + (let [max-supply (long (or (:max-supply config) + 1000000000000000000)) + minter (address (or (:minter config) + *address*))] + (assert (<= 0 + max-supply + 1000000000000000000)) + `(do + (import convex.trust :as trust) + + ;; Who is allowed to mint tokens? + ;; + (def minter + ~minter) + + ;; Maximum supply (limit after minting) + ;; + (def max-supply + ~max-supply) + + + (defn burn + + ^{:callable? true} + + [amount] + + (when-not (trust/trusted? minter + *caller*) + (fail :TRUST + "No rights to burn")) + (let [amount (long amount) + bal (balance *caller*)] + ;; Burn amount must be less than or equal to caller's balance. + ;; + (assert (<= 0 + amount + bal)) + (set-holding *caller* + (- bal + amount)) + (def supply (- supply + amount)))) + + + (defn mint + + ^{:callable? true} + + [amount] + + (when-not (trust/trusted? minter + *caller*) + (fail :TRUST + "No rights to mint")) + (let [amount (long amount) + new-supply (+ supply + amount) + bal (balance *caller*) + new-bal (+ bal + amount)] + ;; Mint amount. + ;; + (assert (<= 0 + new-bal + max-supply)) + ;; New supply must be in valid range. + ;; + (assert (<= 0 + new-supply + max-supply)) + (set-holding *caller* + new-bal) + (def supply + new-supply)))))) + + + +(defn build-token + + ^{:doc {:description ["Creates deployable code for a new fungible token which follows the interface described in `convex.asset`." + "An optional config map can be provided:" + "- `:initial-holder`, address which will hold the initial supply (defaults to `*address*`)" + "- `:supply`, supply created and attributed to `:initial-holder` (long, defaults to 1000000)"] + :examples [{:code "(deploy (build-token {:supply 1000000 :initial-holder *address*}))"}] + :signature [{:params [config]}]}} + + [config] + + (let [supply (or (:supply config) + 1000000) + initial-holder (or (:initial-holder config) + *address*)] + `(do + + (def supply + ~supply) + + (set-holding ~initial-holder + ~supply) + + ;; Map of holder-address -> {offeree-address -> positive long amount} + ;; Must enforce valid positive offers + ;; + (def offers {}) + + + ;; Functions of the interface described in the `convex.asset` library + + (defn accept + + ^{:callable? true} + + [sender quantity] + + (let [sender (address sender) + quantity (if quantity + (long quantity) + 0) + om (or (get offers + sender) + 0) + sendbal (or (get-holding sender) + 0) + offer (or (get om + *caller*) + 0)] + (cond + (< quantity + 0) + (fail "Can't accept a negative quantity of fungible tokens.") + + (< offer + quantity) + (fail "Offer is insufficient") + + (< sendbal + quantity) + (fail "Sender token balance is insufficient") + + (let [new-offer (- offer + quantity)] + (def offers + (assoc offers + sender + (if (> new-offer + 0) + (assoc om + *caller* + new-offer) + (dissoc om *caller*)))) + (set-holding sender + (- sendbal + quantity)) + (set-holding *caller* + (+ (or (get-holding *caller*) + 0) + quantity)))))) + + + (defn balance + + ^{:callable? true} + + [addr] + + (or (get-holding addr) + 0)) + + + ;; No restrictions on transfer by default. + ;; + (defn check-transfer + + ^{:callable? true} + + [_sender _receiver _quantity] + + nil) + + + (defn direct-transfer + + ^{:callable? true} + + [addr amount] + + (let [addr (address addr) + amount (if amount + (long amount) + 0) + bal (or (get-holding *caller*) + 0) + tbal (or (get-holding addr) + 0)] + ;; Amount must be in valid range. + ;; + (assert (<= 0 + amount + bal)) + ;; Need this check in case of self-transfers. + (when (= *caller* + addr) + (return amount)) + (set-holding *caller* + (- bal + amount)) + (set-holding addr + (+ tbal + amount)))) + + + (defn get-offer + + ^{:callable? true} + + [sender receiver] + + (or (get-in offers + [sender + receiver]) + 0)) + + + (defn offer + + ^{:callable? true} + + [receiver quantity] + + (let [receiver (address receiver) + quantity (if quantity + (long quantity) + 0) + om (get offers + *caller*)] + (if (<= quantity + 0) + (when (get om + receiver) + (def offers + (assoc offers + *caller* + (dissoc om + receiver)))) + (def offers + (assoc-in offers + [*caller* + receiver] + quantity))) + quantity)) + + + ;; TODO. Shouldn't also implement `owns?` + + + (defn quantity-add + + ^{:callable? true} + + [a b] + + (let [a (if a + (long a) + 0) + b (if b + (long b) + 0)] + (+ a b))) + + + (defn quantity-sub + + ^{:callable? true} + + [a b] + + (let [a (if a + (long a) + 0) + b (if b + (long b) + 0)] + (if (> a b) + (- a + b) + 0))) + + + (defn quantity-subset? + + ^{:callable? true} + + [a b] + + (<= (if a + (long a) + 0) + (if b + (long b) + 0)))))) + + +;;;;;;;;;; API for handling actors + + +(defn balance + + ^{:doc {:description "Gets the balance from a fungible token. Checks the balance for the specified holder, or the current *address* if not specified." + :examples [{:code "(balance my-token-address)"}] + :signature [{:params [token holder]}]}} + + [token holder] + + (call token + (balance holder))) + + + +(defn burn + + ^{:doc {:description "Burns an amount of tokens for the given token. User must have minting privileges. Amount must be non-negative and no greater than the caller's balance." + :examples [{:code "(mint my-token-address 1000)"}] + :signature [{:params [token amount]}]}} + + [token amount] + + (call token + (burn amount))) + + + +(defn mint + + ^{:doc {:description "Mints an amount of tokens for the given token. User must have minting privileges. Amount may be negative to burn fungible tokens." + :examples [{:code "(mint my-token-address 1000)"}] + :signature [{:params [token amount]}]}} + + [token amount] + + (call token + (mint amount))) + + + +(defn transfer + + ^{:doc {:description "Transfers balance of a fungible token." + :examples [{:code "(transfer my-token-address my-friend 100)"}] + :signature [{:params [token target amount]}]}} + + [token target amount] + + (call token + (direct-transfer target + amount))) diff --git a/convex-core/src/main/cvx/convex/play.cvx b/convex-core/src/main/cvx/convex/play.cvx new file mode 100644 index 000000000..9afc2e9b9 --- /dev/null +++ b/convex-core/src/main/cvx/convex/play.cvx @@ -0,0 +1,80 @@ +'convex.play + + +(call *registry* + (register {:description ["Provides a playable environment, akin to a text game, where transactions are actually interpreted by an actor." + "Based on `*lang*`." + "User can always send a transaction `stop` to stop the playable environment and resume normal transaction processing." + "Actor must define callable functions:" + "- `(command trx)`, takes a transaction and do any arbitrary operation, returns a displayable message or nil to stop" + "- `(start)`, for initializing the environment"] + :name "Playable environment library"})) + + +;; +;; +;; The intention of this library is to provide tools to safely delegate command execution to an actor +;; which can intrepret user input as a custom language. +;; +;; Safety measures: +;; - All actor commands executed in the actor's environment: cannot control or modify user account +;; - User can always type `stop` to stop, avoids getting locked +;; +;; + + +;;;;;;;;;; Private API + + + +(defn -stop + + ^{:private? true} + + ;; Private because unreachable: + ;; + ;; - User most likely cannot reach because transactions are interpreted by actor + ;; - Actor cannot reach because it would define `*lang*` in its environment, not user's + + [] + + (undef *lang*) + "Stop.") + + +;;;;;;;;;; Public API + + +(defn runner + + ^{:doc {:description ["Returns a function that can be set to `*lang*` and which calls the `command` function on `actor` with each transaction." + "User can always transact `stop` to remove it from `*lang*` and resume normal transaction processing."] + :examples [{:code "(runner #1234)"}] + :signature [{:params [actor]}]}} + + [actor] + + (fn [trx] + (if (= trx + 'stop) + (-stop) + (let [result (call actor + (command trx))] + (if (nil? result) + (-stop) + result))))) + + + +(defn start + + ^{:doc {:description "Prepares `*lang*` using `runner` and then calls `start` on `actor` to launch the playable environment." + :examples [{:code "(start #1234)"}] + :signature [{:params [actor]}]}} + + [actor] + + (def *lang* + (runner actor)) + (call actor + (start))) diff --git a/convex-core/src/main/cvx/convex/registry.cvx b/convex-core/src/main/cvx/convex/registry.cvx new file mode 100644 index 000000000..12fccf2a6 --- /dev/null +++ b/convex-core/src/main/cvx/convex/registry.cvx @@ -0,0 +1,140 @@ +'convex.registry + +(set-holding *address* + {:description ["Actor hosting a registry for resolving arbitrary symbols to addresses." + "Typically, actors and libraries are registered so that they can be retrieved and consumed using standard `import`." + "Each record in the registry has a controller that can update that record in any way." + "A controller is an address or, more speficially, a trust monitor as described in `convex.trust`." + "This actor also provides a standard way for adding metadata to an address."] + :name "Convex Name Service"}) + + +;; +;; +;; Deployed by default during network initialisation at a well-known address. +;; Initialization takes care of registering this registry alongside other actors and libraries. +;; +;; This make it accessible from early in network bootstrap as a way to register and locate Accounts. +;; +;; + + +;;;;;;;;;; Values + +(def cns-database + + ^{:private? true} + + ;; Map of `symbol` -> `[address-target address-controller]`. + + {}) + + + +(def trust + + ^{:private? true} + + ;; Address of the `convex.trust`, it is deployed right after this account, hence it is predictable. + + (address (inc (long *address*)))) + + + +;;;;;;;;;; Address metadata + + +(defn lookup + + ^{:callable? true + :doc {:description "Looks up registry metadata for a given address." + :examples [{:code "(call *registry* (lookup somebody)"}] + :signature [{:params [addr]}]}} + + [addr] + + (get-holding (address addr))) + + + +(defn register + + ^{:callable? true + :doc {:description "Registers metadata for the *caller* account. Metadata can be an arbitrary value, but by convention is a map with defined fields." + :examples [{:code "(call *registry* (register {:name \"My Name\"})"}] + :signature [{:params [metadata]}]}} + + [data] + + (set-holding *caller* + data)) + + + +(defn cns-control + + ^{:callable? true + :doc {:description "Updates a CNS name mapping to set a new controller. May only be peformed by a current controller." + :examples [{:code "(call *registry* (cns-control 'my.actor trust-monitor-address)"}] + :signature [{:params [name addr]}]}} + + [name addr] + + (let [record (get cns-database + name)] + (when (nil? record) + (fail :STATE + "CNS record does not exist")) + (when (not (trust/trusted? (second record) + *caller*)) + (fail :TRUST + "Caller is not trusted with transferring control for that CNS record")) + (def cns-database + (assoc cns-database + name + (assoc record + 1 + addr))))) + + + +(defn cns-resolve + + ^{:callable? true + :doc {:description "Resolves a name in the Convex Name Service." + :examples [{:code "(call *registry* (cns-resolve 'convex.registry)"}] + :signature [{:params [addr]}]}} + + [name] + + (assert (symbol? name)) + (when-let [record (get cns-database + name)] + (first record))) + + + +(defn cns-update + + ^{:callable? true + :doc {:description "Updates or adds a name mapping in the Convex Name Service. Only the owner of a CNS record may update the mapping for an existing name" + :examples [{:code "(call *registry* (cns-update 'my.actor addr)"}] + :signature [{:params [name addr]}]}} + + [name addr] + + (let [record (get cns-database + name)] + (when (and record + (not (trust/trusted? (second record) + *caller*))) + (fail :TRUST + "Caller is not trusted with updating the requested CNS record")) + (when-not (account addr) + (fail :NOBODY + "Can only use an existing account")) + (def cns-database + (assoc cns-database + name + [addr + *caller*])))) diff --git a/convex-core/src/main/cvx/convex/trust.cvx b/convex-core/src/main/cvx/convex/trust.cvx new file mode 100644 index 000000000..0e4345dd9 --- /dev/null +++ b/convex-core/src/main/cvx/convex/trust.cvx @@ -0,0 +1,260 @@ +'convex.trust + + +(call *registry* + (register {:description ["Based on the reference monitor security model." + "See comments about trusted monitors in `trusted?`." + "Provides the creation of blacklists, whitelists, and upgradable actors."] + :name "Trust monitor library"})) + + +;; +;; See: https://en.wikipedia.org/wiki/Reference_monitor +;; + + +;;;;;;;;;; Private + + +(def -self + + ^{:private? true} + + *address*) + + +;;;;;;;;;; Checking trust + + +(defn trusted? + + ^{:doc {:description ["Returns true if `subject` is trusted by `trust-monitor`, false otherwise." + "A trust monitor is an address, pointing to either:" + "- A user account that can only trust itself" + "- An actor implementing `(check-trusted? subject action object)` which returns true or false." + "`action` and `object` are arbitrary values specific to the trust monitor." + "In practice, `subject` is often an address, although this is specific to the trust monitor as well." + "See `build-blacklist` and `build-whitelist`."] + :examples [{:code "(trusted? my-blacklist *caller*)"}] + :signature [{:params [trust-monitor subject]} + {:params [trust-monitor subject action]} + {:params [trust-monitor subject action object]}]}} + + + ([trust-monitor subject action] + + (trusted? trust-monitor + subject + action + nil)) + + + ([trust-monitor subject] + + (trusted? trust-monitor + subject + nil + nil)) + + + ([trust-monitor subject action object] + + (if (actor? trust-monitor) + (query (call trust-monitor + (check-trusted? subject + action + object))) + (= (address trust-monitor) + subject)))) + + +;;;;;;;;;; Building black/white lists + + +(defn build-blacklist + + ^{:doc {:description ["Creates deployable code for a new blacklist, an actor acting as a trust monitor." + "An optional configuration map may be provided:" + "- `:blacklist`, collection of addresses forming the initial blacklist" + "- `:controller`, address that has the ability to modify the blacklist"] + :examples [{:code "(deploy (build-blacklist {:controller *address* :blacklist [my-foe-1 my-foe-2]}))"}] + :signature [{:params [config]}]}} + + [config] + + (let [blacklist (reduce (fn [w x] + (conj w + (address x))) + #{} + (or (:blacklist config) + [*address*])) + controller (address (or (:controller config) + *address*))] + `(do + (def trust + ~-self) + + (def blacklist + + ;; blacklist of addresses that are denied. + + ~blacklist) + + + (def controller + + ;; Controller address determines who can modify the blacklist. + + ~controller) + + + (defn check-trusted? + + ^{:callable? true} + + [subject action object] + + (not (contains-key? blacklist + (address subject)))) + + + (defn set-trusted + + ^{:callable? true} + + [subject allow?] + + (if (trust/trusted? controller + *caller*) + (def blacklist + ((if allow? + disj + conj) + blacklist + (address subject))) + (fail :TRUST + "No access to blacklist!")))))) + + + +(defn build-whitelist + + ^{:doc {:description ["Creates deployable code for a new whitelist, an actor acting as a trust monitor." + "An optional configuration map may be provided:" + "- `:controller`, address that has the ability to modify the whitelist" + "- `:whitelist`, collection of addresses forming the initial whitelist"] + :examples [{:code "(deploy (build-whitelist {:controller *address* :whitelist [*address*]}))"}] + :signature [{:params [config]}]}} + + [config] + + (let [whitelist (reduce (fn [w x] + (conj w + (address x))) + #{} + (or (:whitelist config) + [*address*])) + controller (or (:controller config) + *address*)] + `(do + (def trust + ~-self) + + + (def whitelist + + ;; A whitelist of addresses that are accepted. + + ~whitelist) + + + (def controller + + ;; Controller address determines who can modify the whitelist. + + ~(address controller)) + + + (defn check-trusted? + + ^{:callable? true} + + [subject action object] + + (contains-key? whitelist + (address subject))) + + + (defn set-trusted + + ^{:callable? true} + + [subject allow?] + + (if (trust/trusted? controller + *caller*) + (def whitelist + ((if allow? conj disj) + whitelist + (address subject))) + (fail :TRUST + "No access to whitelist!")))))) + + +;;;;;;;;;; Upgradable actors + + +(defn add-trusted-upgrade + + ;; TODO. Improve docstring, `:root` is a blacklist or a whitelist. + + ^{:doc {:description ["Creates deployable code for an upgradable actor where any arbitrary code can be executed." + "An optional configuration map may be provided:" + "- `:root`, address that can execute arbitrary code in the actor (defaults to `*address*`)" + "Meant to be used wisely."] + :examples [{:code "(deploy (add-trusted-upgrade {:root *address*}))"}] + :signature [{:params [config]}]}} + + [config] + + (let [root (or (:root config) + *address*)] + `(do + (def trust + ~-self) + + + (def upgradable-root + ~root) + + + (defn upgrade + + ^{:callable? true} + + [code] + + (if (trust/trusted? upgradable-root + *caller*) + (eval code) + (fail :TRUST + "No root access to upgrade capability!")))))) + + + +(defn remove-upgradability! + + ^{:doc {:description ["Removes upgradability from an actor, previously added using `add-trusted-upgrade`." + "Cannot be undone, meant to be used wisely after considering all implications."] + :examples [{:code "(remove-upgradability! upgradable-actor)"}] + :signature [{:params [config]}]}} + + [actor] + + (call actor + (upgrade + '(do + ;; Undefine things used for upgradability + (undef upgrade) + (undef upgradable-root)))) + nil) diff --git a/convex-core/src/main/cvx/convex/trusted-oracle.cvx b/convex-core/src/main/cvx/convex/trusted-oracle.cvx new file mode 100644 index 000000000..6a5e0ff49 --- /dev/null +++ b/convex-core/src/main/cvx/convex/trusted-oracle.cvx @@ -0,0 +1,141 @@ +'convex.trusted-oracle + + +(call *registry* + (register {:description ["API for simple oracle actors that depend on a trusted set of addresses who may provide results." + "Default actor used is `convex.trusted-oracle.actor`." + "At first, a key (any arbitrary value) is registered via `register`." + "When ready, a result for that key is provided via `provide` by a trusted account." + "Consumers can fetch data about keys using `data`, check if a result is provided using `finalized?`, and read results using `read`." + "Implementating a simple oracle actor requires defining callable function versions of:" + "- `(data key)`" + "- `(finalized? key)`" + "- `(read key)`" + "- `(register key result)`" + "- `(provide key result)`"] + :name "Trusted oracle API"})) + + +(import convex.trusted-oracle.actor :as default-actor) + + +;;;;;;;;;; API + + +(defn data + + ^{:doc {:description "Returns data registered for `key`." + :examples [{:code "(data :foo)"}] + :signature [{:params [key]} + {:params [actor key]}]}} + + + ([key] + + (data default-actor + key)) + + + ([actor key] + + (call actor + (data key)))) + + + +(defn finalized? + + ^{:callable? true + :doc {:description "Returns a boolean indicating if a results has been provided for `key`." + :examples [{:code "(finalized? :foo)"}] + :signature [{:params [key]} + {:params [actor key]}]}} + + + ([key] + + (finalized? default-actor + key)) + + + ([actor key] + + (call actor + (finalized? key)))) + + + +(defn read + + ^{:callable? true + :doc {:description "Returns the result for `key`." + :examples [{:code "(read :foo)"}] + :signature [{:params [key]} + {:params [actor key]}]}} + + + ([key] + + (read default-actor + key)) + + + ([actor key] + + (call actor + (read key)))) + + + +(defn register + + ^{:callable? true + :doc {:description ["Callable function for registering a new oracle key." + "Returns true if successful, false if key already exists." + "Data should be a map containg at least `:trust`, a set of addresses trusted for using `provide` on that key." + "Without `:trust`, a result cannot be delivered."] + :examples [{:code "(register :foo {:trust #{*address*}})"}] + :signature [{:params [key data]} + {:params [actor key data]}]}} + + + ([key data] + + (register default-actor + key + data)) + + + ([actor key data] + + (call actor + (register key + data)))) + + + +(defn provide + + ^{:callable? true + :doc {:description ["Provides a result for a key registered using `register`." + "Does not change anything if a resulted has already been provided for that key." + "Returns the result associated with that key."] + :errors {:STATE "When key does not exist" + :TRUST "When caller is untrusted"} + :examples [{:code "(provide :foo 42)"}] + :signature [{:params [key result]} + {:params [actor key result]}]}} + + + ([key result] + + (provide default-actor + key + result)) + + + ([actor key result] + + (call actor + (provide key + result)))) diff --git a/convex-core/src/main/cvx/convex/trusted-oracle/actor.cvx b/convex-core/src/main/cvx/convex/trusted-oracle/actor.cvx new file mode 100644 index 000000000..9f452bb44 --- /dev/null +++ b/convex-core/src/main/cvx/convex/trusted-oracle/actor.cvx @@ -0,0 +1,107 @@ +'convex.trusted-oracle.actor + + +(call *registry* + (register {:description ["Default actor used by `convex.trusted-oracle`." + "Go there for more information about this implementation."] + :name "Trusted oracle actor implementation"})) + + +;;;;;;;;;; Values + + +(def *list* + + ;; Map of `key` -> `arbitrary map describing an oracle`. + + {}) + +(def *results* + + ;; Map of `key` -> `result`. + + {}) + + +;;;;;;;;;; Callable functions + + +(defn data + + ^{:callable? true} + + [key] + + (*list* key)) + + + +(defn finalized? + + ^{:callable? true} + + [key] + + (contains-key? *results* + key)) + + + +(defn read + + ^{:callable? true} + + [key] + + (*results* key)) + + + +(defn register + + ^{:callable? true} + + [key data] + + (if (contains-key? *list* + key) + false + (do + (def *list* + (assoc *list* + key + data)) + true))) + + + +(defn provide + + ^{:callable? true} + + [key value] + + (cond + (not (*list* key)) + (fail :STATE + (str "Unknown oracle key: " + key)) + + (not (get-in *list* + [key + :trust + *caller*])) + (fail :TRUST + "Untrusted caller") + + (contains-key? *results* + key) + (*results* key) + + :else + (do + (def *results* + (assoc *results* + key + value)) + value))) diff --git a/convex-core/src/main/cvx/lab/convex/xform.cvx b/convex-core/src/main/cvx/lab/convex/xform.cvx new file mode 100644 index 000000000..9080753d7 --- /dev/null +++ b/convex-core/src/main/cvx/lab/convex/xform.cvx @@ -0,0 +1,175 @@ +;; +;; +;; Prototype for Clojure-like transducers +;; +;; https://clojure.org/reference/transducers +;; +;; + + + +(call *registry* + (cns-update 'convex.xform + *address*)) + + + +(defn filter + + [f] + + (fn [rf] + + (fn + ([] + (rf)) + + ([result] + (rf result)) + + ([acc x] + + (if (f x) + (rf acc + x) + acc))))) + + + +(defn map + + [f] + + (fn [rf] + + (fn + ([] + (rf)) + + ([result] + (rf result)) + + ([acc x] + (rf acc + (f x))) + + ([acc x & x+] + (rf acc + (apply f + x + x+)))))) + + + +(defn transduce + + + ([xform f coll] + + (transduce xform + f + (f) + coll)) + + + ([xform f init coll] + + (let [f-2 (xform f)] + (f-2 (reduce f-2 + init + coll))))) + + + +(defn first + + ([] + + nil) + + + ([result] + + result) + + + ([_acc x] + + (reduced x))) + + + +(defn first-n + + [n] + + (fn + ([] []) + + + ([result] + + result) + + + ([acc x] + + (let [acc-2 (conj acc + x)] + (if (>= (count acc-2) + n) + (reduced acc-2) + acc-2))))) + + + +(defn last + + ([] + + nil) + + + ([result] + + result) + + + ([_acc x] + + x)) + + + +(defn last-n + + ;; TODO. Fails because of: https://github.com/Convex-Dev/convex/issues/193 + + [n] + + (fn + ([] + + [0 + (loop [acc [] + i 0] + (if (< i + n) + (recur (conj acc + nil) + (inc i)) + acc))]) + + + ([[_pointer acc]] + + acc) + + + ([[pointer acc] x] + + [(inc pointer) + (assoc acc + (rem pointer + n) + x)]))) diff --git a/convex-core/src/main/cvx/lab/messenger.cvx b/convex-core/src/main/cvx/lab/messenger.cvx new file mode 100644 index 000000000..197e886a9 --- /dev/null +++ b/convex-core/src/main/cvx/lab/messenger.cvx @@ -0,0 +1,9 @@ + (defn accept + + ^{:callable? true + :doc {:description "Accepts a message and offered assets." + :examples [{:code "(call messenger (accept message-id))"}] + :signature [{:params [token holder]}]}} + [message-id] + + ) diff --git a/convex-core/src/main/cvx/lab/prediction-market.cvx b/convex-core/src/main/cvx/lab/prediction-market.cvx new file mode 100644 index 000000000..389ced014 --- /dev/null +++ b/convex-core/src/main/cvx/lab/prediction-market.cvx @@ -0,0 +1,102 @@ +(defn build-prediction-market [oracle oracle-key outcomes] + `(do + ;; store oracle address and key in environment + (def oracle (address ~oracle)) + (def oracle-key ~oracle-key) + (def outcomes ~outcomes) + + + ;; Stakes are a map of outcome value to map of ( address ->stake) + (def stakes (into {} (map (fn [x] [x {}]) ~outcomes))) + + ;; Total stake for each outcome + (def totals (into {} (map (fn [x] [x 0]) ~outcomes))) + + ;; NOTE: total current bonded value is the balance of this contract + + ;; Bonding curve function for a given map of stakes + (defn bond + ^{:callable? true} + [stks] + (let [sxx (reduce (fn [acc [k v]] (let [x (double v)] (+ acc (* x x)))) 0.0 stks)] + (sqrt sxx))) + + ;; Adjust stake on an outcome + ;; must be called with a sufficient offer to fund any increase + ;; returns positive cost of increased stake, or negative refund of reduced stake + (defn stake + ^{:callable? true} + [outcome new-stake] + (assert (contains-key? stakes outcome) (>= new-stake 0)) + + (let [old-stake (or (get (stakes outcome) *caller*) 0) + _ (if (== old-stake new-stake) (return 0)) + nos (assoc (stakes outcome) *caller* new-stake) + new-stakes (assoc stakes outcome nos) + new-totals (assoc totals outcome (+ (totals outcome) (- new-stake old-stake))) + new-bond (long (bond new-totals)) + dval (- new-bond *balance*)] + + ;; get or refund funds. Will error if not enough provided for stake increase? + (cond + (== dval 0) nil ;; nothing to do.... can this happen? + (> dval 0) (accept dval) + (< dval 0) (transfer *caller* (- dval))) + + (def totals new-totals) + (def stakes new-stakes) + dval)) + + ;; Get the effective price for an outcome. May be NaN if no balance at all. + (defn price + ^{:callable? true} + [outcome] + (when-not (contains-key? totals outcome) (return il)) + (let [tstk (double (totals outcome)) + bal (double *balance*)] + (/ (* tstk tstk) (* bal bal)))) + + ;; Get the collection of possible outcomes + (defn get-outcomes + ^{:callable? true} + [data] + outcomes) + + ;; final outcome + (def final-outcome nil) + (def final-flag false) + + ;; Check if contract is finalised + (defn finalized? + ^{:callable? true} + [] + (when final-flag (return true)) + (cond (call oracle (finalized? oracle-key)) :OK (return false)) + + (let [result (call oracle (read oracle-key))] + (def final-outcome result) + (def final-flag true) + true)) + + ;; call to refund all stakes, not for external use + ;; called in event of a unanticipated outsome + (def refund [] + ;; TODO + ) + + + ;; call to claim payout + ;; returns amount paid out, or null if not yet finalised + (defn payout + ^{:callable? true} + [] + (if (finalized?) :OK (return nil)) + (when-not (contains-key? totals final-outcome) (return (refund))) + (let [total (double (totals final-outcome)) ;; total stake on winning outcome + fo-stakes (stakes final-outcome {}) + stk (double (or (fo-stakes *caller*) 0.0)) ;; caller's stake on final outcome + quota (long (* *balance* (/ stk total)))] + (def stakes (assoc stakes final-outcome (dissoc fo-stakes *caller*))) + (transfer *caller* quota) + quota)) + )) diff --git a/convex-core/src/main/cvx/lab/secured-loan.con b/convex-core/src/main/cvx/lab/secured-loan.con new file mode 100644 index 000000000..e5d2f6f7a --- /dev/null +++ b/convex-core/src/main/cvx/lab/secured-loan.con @@ -0,0 +1,18 @@ +;; Smart contract implementing a secured loan +;; +;; Concept: +;; 1. Issuer creates secured-loan contract in :initial state +;; 2. Issuer places collateral assets (tokens, etc.) in account under control of secured-loan contract +;; 3. Issuer switches state to :offered +;; 4. Lender may provide the loan, in which case state changes to :accepted +;; 5. Once accepted, issuer may withdraw borrowed funds but cannot touch collateral +;; 6. Issuer must ensure principal plus interest is repaid before the maturity date +;; 7. At maturity, either: +;; a) Loan is repaid, and issuer regains control of collateral assets +;; b) Loan defaults, and lender gains control of collateral assets + +(do + + + + ) \ No newline at end of file diff --git a/convex-core/src/main/cvx/torus/currencies.cvx b/convex-core/src/main/cvx/torus/currencies.cvx new file mode 100644 index 000000000..6562d0d34 --- /dev/null +++ b/convex-core/src/main/cvx/torus/currencies.cvx @@ -0,0 +1,12 @@ +[ +["USD" "US Dollar" "US National Currency" "$" 1000000000 2 1.00] +["JPY" "Japanese Yen" "Japanese National Currency" "¥" 1000000000 0 0.0092] +["EUR" "Euro" "European Union Currency" "€" 1000000000 2 1.18819] +["GBP" "Pound Sterling" "UK National Currency" "£" 1000000000 2 1.38700] +["THB" "Thai Baht" "Thai National Currency" "฿" 1000000000 2 0.03244] +["VND" "Vietnamese Dong" "Vietnamese National Currency" "₫" 1000000000 2 0.00004] +["MYR" "Malaysian Ringgit" "Malaysian National Currency" "RM" 1000000000 2 0.24260] +["CHF" "Swiss Franc" "Swiss National Currency" "Fr." 1000000000 2 1.07269] +["HKD" "Hong Kong Dollar" "Hong Kong Currency" "HK$" 1000000000 2 0.12879] +["SGD" "Singapore Dollar" "Singapore National Currency" "S$" 1000000000 2 0.751] +] \ No newline at end of file diff --git a/convex-core/src/main/cvx/torus/exchange.cvx b/convex-core/src/main/cvx/torus/exchange.cvx new file mode 100644 index 000000000..26d4b20d0 --- /dev/null +++ b/convex-core/src/main/cvx/torus/exchange.cvx @@ -0,0 +1,632 @@ +'torus.exchange + +(call *registry* + (register {:description ["Torus establishes automated market makers for fungible assets (see `convex.fungible`)." + "It creates singleton CVX/Token trading pairs for each fungible asset."] + :name "Torus exchange"})) + + +;; TODO. Add docstrings to library functions. + + +;;;;;;;;;; Imports + + +(import convex.asset :as asset) +(import convex.fungible :as fungible) +(import convex.trust :as trust) + + +;;;;;;;;;; Values + + +(def markets + + ^{:private? true} + + ;; Blob-map of `token address` -> `market actor address`. + + (blob-map)) + + +;;;;;;;;;; API - Creating markets + + +(defn build-market + + ^{:doc {:description "Creates deployable code for a new Torus token market." + :examples [{:code "(deploy (build-market {:token token-address}))"}] + :signature [{:params [token torus-addr]}]}} + + ;; Deployable code is `[fungible-token-code market-code]`. + + [token torus] + + [(fungible/build-token {:supply 0}) + `(do + (import convex.asset :as asset) + (import convex.core :as core) + (import convex.fungible :as fungible) + + + (def token ~token) + (def torus ~torus) + (def token-balance + 0) + + + (defn add-liquidity + + ^{:callable? true} + + [amount] + + (let [;; Amount of tokens deposited. + amount (long amount) + ;; Price of token in CVX (double), nil if no current liquidity. + price (price) + initial-cvx-balance *balance* + ;; Amount of CVX required (all if initial deposit). + cvx (core/accept (if price + (long (* price + amount)) + *offer*)) + ;; Ensures tokens are transferred from caller to market actor. + _ (asset/accept *caller* + [token + amount]) + ;; Compute new total balances for actor/ + new-token-balance (+ token-balance + amount) + ;; Compute number of new shares for depositor = increase in liquidity (%) * current total shares. + ;; If no current liquidity just initialise with the geometric mean of amounts deposited. + delta (if (> supply + 0) + (let [;; Initial size of liquidity pool (geometric mean). + liquidity (sqrt (* (double initial-cvx-balance) + token-balance)) + new-liquidity (sqrt (* (double *balance*) + new-token-balance))] + (long (* (- new-liquidity + liquidity) + (/ supply + liquidity)))) + (long (sqrt (* (double amount) + cvx))))] + ;; Perform updates to reflect new holdings of liquidity pool shares and total token balance (all longs) + (set-holding *caller* + (+ delta + (or (get-holding *caller*) + 0))) + (def supply (+ supply + delta)) + (def token-balance + new-token-balance) + delta)) + + + (defn buy-cvx + + ^{:callable? true} + + [amount] + + (let [amount (long amount) + required-tokens (or (buy-cvx-quote amount) + (fail :FUNDS + "Pool cannot supply this amount of CVX"))] + (asset/accept *caller* + [token + required-tokens]) + (def token-balance + (+ token-balance + required-tokens)) + ;; Must be done last. + ;; + (core/transfer *caller* + amount) + required-tokens)) + + + (defn buy-cvx-quote + + ^{:callable? true} + + [amount] + + ;; Security: check pool can provide. + ;; + (when-not (< 0 + amount + *balance*) + (return nil)) + (let [;; Computes pool and fees/ + cvx-balance *balance* + pool (* (double token-balance) + cvx-balance) + rate (calc-rate)] + ;; Computes required payment in tokens. + (long (ceil (* (+ 1.0 + rate) + (- (/ pool + (- cvx-balance + amount)) + token-balance)))))) + + + (defn buy-tokens + + ^{:callable? true} + + [amount] + + (let [amount (long amount) + required-cvx (or (buy-tokens-quote amount) + (fail "Pool cannot supply this amount of tokens"))] + (core/accept required-cvx) + (def token-balance + (- token-balance + amount)) + ;; Must be done last. + ;; + (fungible/transfer token + *caller* + amount) + required-cvx)) + + + (defn buy-tokens-quote + + ^{:callable? true} + + [amount] + + ;; Security: check pool can provide. + ;; + (when-not (< 0 + amount + token-balance) + (return nil)) + (let [;; Computes pool and fees. + cvx-balance *balance* + pool (* (double token-balance) + cvx-balance) + rate (calc-rate)] + ;; Computes required payment in CVX. + (long (ceil (* (+ 1.0 + rate) + (- (/ pool + (- token-balance + amount)) + cvx-balance)))))) + + + (defn calc-rate + + ;; TODO. Have variable rate set by torus and/or trade velocity. + ;; Maybe BASE_FEE / 1 + (THROUGHPUT / LIQUIDITY) ? + + [] + + 0.001) + + + (defn price + + ^{:callable? true} + + ;; Price is CVX amount per token, or nil if there are no tokens in liquidity pool. + + [] + + (when (> token-balance + 0) + (/ (double *balance*) + token-balance))) + + + (defn sell-cvx + + ^{:callable? true} + + [amount] + + (let [amount (long amount) + gained-tokens (or (sell-cvx-quote amount) + (fail "Cannot sell this amount into pool"))] + (core/accept amount) + (def token-balance + (- token-balance + gained-tokens)) + ;; Must be done last. + ;; + (asset/transfer *caller* + [token + gained-tokens]) + gained-tokens)) + + + (defn sell-cvx-quote + + ^{:callable? true} + + [amount] + + ;; Security: check amount is positive. + ;; + (when (< amount + 0) + (return nil)) + (let [;; Computes pool and fees. + cvx-balance *balance* + pool (* (double token-balance) + cvx-balance) + rate (calc-rate) + new-cvx-balance (+ cvx-balance + amount)] + ;; Computes gained Tokens coins from sale. + (long (/ (- token-balance + (/ pool + new-cvx-balance)) + (+ 1.0 + rate))))) + + + (defn sell-tokens + + ^{:callable? true} + + [amount] + + (let [amount (long amount) + gained-cvx (or (sell-tokens-quote amount) + (fail "Cannot sell this amount into pool"))] + (asset/accept *caller* + [token + amount]) + (def token-balance + (+ token-balance + amount)) + ;; Must be done last. + ;; + (core/transfer *caller* + gained-cvx) + gained-cvx)) + + + (defn sell-tokens-quote + + ^{:callable? true} + + [amount] + + ;; Security: check amount is positive. + ;; + (when-not (< 0 + amount) + (return nil)) + (let [;; Computes pool and fees. + cvx-balance *balance* + pool (* (double token-balance) + cvx-balance) + rate (calc-rate) + new-token-balance (+ token-balance + amount)] + ;; Computes gained Convex coins from sale. + (long (/ (- cvx-balance + (/ pool + new-token-balance)) + (+ 1.0 + rate))))) + + + (defn withdraw-liquidity + + ^{:callable? true} + + [shares] + + (let [;; Amount of shares to withdraw. + shares (long shares) + ;; Shares of holder. + own-holding (or (get-holding *caller*) + 0) + _ (assert (<= 0 + shares + own-holding)) + proportion (if (> supply + 0) + (/ (double shares) + supply) + 0.0) + coin-refund (long (* proportion + *balance*)) + token-refund (long (* proportion + token-balance))] + ;; SECURITY: + ;; 1. Update balances then transfer coins first. Risk of re-entrancy attack if transfers are made while + ;; this actor is in an inconsistent state so we MUST do accounting first. + (def token-balance + (- token-balance + token-refund)) + (set-holding *caller* + (- own-holding + shares)) + (def supply + (- supply + shares)) + ;; 2. Transfer back coins. Be aware caller might do *anything* in transfer callbacks! + (transfer *caller* + coin-refund) + ;; 3. Finally transfer asset. We've accounted this already, so safe + ;; TODO. Decide which of these is best + ;;(asset/transfer *caller* [token token-refund] :withdraw) + (fungible/transfer token + *caller* + token-refund) + shares)))]) + + + +(defn create-market + + ^{:callable? true + :doc {:description "Gets or creates the canonical market for a token." + :examples [{:code "(deploy (build-market {:token token-address}))"}] + :signature [{:params [config]}]}} + + [token] + + (when-not (= *address* + ~*address*) + (return (call ~*address* + (create-market token)))) + (assert (address? token)) + (let [existing-market (get markets + token)] + (or existing-market + (let [market (deploy (build-market token + *address*))] + (def markets + (assoc markets + token + market)) + market)))) + + + +(defn get-market + + ^{:doc {:description "Gets the canonical market for a token. Returns nil if the market does not exist." + :examples [{:code "(deploy-once (build-market {:token token-address}))"}] + :signature [{:params [token]}]}} + + [token] + + (get markets + token)) + + +;;;;;;;;;; API - Handling markets + + +(defn add-liquidity + + ^{:doc {:description nil + :signature [{:params [token token-amount cvx-amount]}]}} + + [token token-amount cvx-amount] + + (let [market (create-market token)] + (asset/offer market + [token + token-amount]) + (call market + (long cvx-amount) + (add-liquidity token-amount)))) + + + +(defn buy + + ^{:doc {:description nil + :signature [{:params [of-token amount with-token]}]}} + + [of-token amount with-token] + + (let [market (or (get-market of-token) + (fail (str "Torus: market does not exist for token: " + of-token))) + cvx-amount (or (call market + (buy-tokens-quote amount)) + (fail :LIQUIDITY + "No liquidity available to buy token")) + sold (buy-cvx with-token + cvx-amount)] + (buy-tokens of-token + amount) + sold)) + + + +(defn buy-cvx + + ^{:doc {:description nil + :signature [{:params [token amount]}]}} + + [token amount] + + (let [market (or (get-market token) + (fail :LIQUIDITY + (str "Torus: market does not exist for token: " + token)))] + ;; Note we can offer all tokens, market will accept what it needs to complete order. + (asset/offer market + [token + (fungible/balance token + *address*)]) + (call market + *balance* + (buy-cvx amount)))) + + + +(defn buy-tokens + + ^{:doc {:description nil + :signature [{:params [token amount]}]}} + + [token amount] + + (let [market (or (get-market token) + (fail :LIQUIDITY + (str "Torus: market does not exist for token: " + token)))] + ;; Note we can offer all CVX + (call market + *balance* + (buy-tokens amount)))) + + + +(defn buy-quote + + ^{:doc {:description nil + :signature [{:params [of-token amount]} + {:params [of-token amount with-token]}]}} + + + ([of-token amount] + + (when-let [market (get-market of-token)] + (call market (buy-tokens-quote amount)))) + + + ([of-token amount with-token] + + (when-let [market (get-market with-token)] + (when-let [cvx-amount (buy-quote of-token amount)] + (call market + (buy-cvx-quote cvx-amount)))))) + + + +(defn price + + ^{:doc {:description "Gets the current price for a token, in CVX or an optional given currency. Returns nil if a market with liquidity does not exist." + :examples [{:code "(price USD)"} + {:code "(price GBP USD)"}] + :signature [{:params [token]} + {:params [token currency]}]}} + + + ([token] + + (when-let [market (get-market token)] + (call market + (price)))) + + + ([token currency] + + (let [market.token (or (get-market token) + (return nil)) + price.cvx (or (call market.token + (price)) + (return nil)) + market.currency (or (get-market currency) + (return nil)) + price.currency (or (call market.currency + (price)) + (return nil))] + (/ price.cvx + price.currency)))) + + + +(defn sell + + ^{:doc {:description nil + :signature [{:params [of-token amount with-token]}]}} + + [of-token amount with-token] + + (let [cvx-amount (sell-tokens of-token + amount)] + (sell-cvx with-token + cvx-amount))) + + + +(defn sell-cvx + + ^{:doc {:description nil + :signature [{:params [token amount]}]}} + + [token amount] + + (let [market (or (get-market token) + (fail :LIQUIDITY + (str "Torus: market does not exist for token: " + token)))] + ;; Offer the amount of CVX being sold. + (call market + amount + (sell-cvx amount)))) + + + +(defn sell-quote + + ^{:doc {:description nil + :signature [{:params [of-token amount]} + {:params [of-token amount with-token]}]}} + + ([of-token amount] + + (when-let [market (get-market of-token)] + (call market + (sell-tokens-quote amount)))) + + + ([of-token amount with-token] + + (when-let [market (get-market with-token)] + (when-let [cvx-amount (sell-quote of-token + amount)] + (call market + (sell-cvx-quote cvx-amount)))))) + + + +(defn sell-tokens + + ^{:doc {:description nil + :signature [{:params [token amount]}]}} + + [token amount] + + (let [market (or (get-market token) + (fail :LIQUIDITY + (str "Torus: market does not exist for token: " + token)))] + ;; Offer the amount of tokens being sold. + (asset/offer market + [token + amount]) + (call market + (sell-tokens amount)))) + + + +(defn withdraw-liquidity + + ^{:doc {:description nil + :signature [{:params [token shares]}]}} + + [token shares] + + (let [market (or (get-market token) + (fail "No market exists to withdraw liquidity"))] + (call market + (withdraw-liquidity shares)))) diff --git a/convex-core/src/main/java/convex/core/Belief.java b/convex-core/src/main/java/convex/core/Belief.java new file mode 100644 index 000000000..ee53ff80f --- /dev/null +++ b/convex-core/src/main/java/convex/core/Belief.java @@ -0,0 +1,723 @@ +package convex.core; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.function.Function; + +import convex.core.crypto.AKeyPair; +import convex.core.data.ABlob; +import convex.core.data.ACell; +import convex.core.data.AMap; +import convex.core.data.ARecord; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.BlobMap; +import convex.core.data.BlobMaps; +import convex.core.data.Format; +import convex.core.data.Hash; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.MapEntry; +import convex.core.data.PeerStatus; +import convex.core.data.SignedData; +import convex.core.data.Tag; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.BadSignatureException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.impl.RecordFormat; +import convex.core.util.Counters; +import convex.core.util.Utils; + +/** + * Class representing a Peer's view of the overall network consensus state. + * + * Belief is immutable, and is designed to be independent of any particular Peer + * so that it can be efficiently merged towards consensus. + * + * Belief can be merged with other Beliefs from the perspective of a Peer. This + * property is fundamental to the Convex consensus algorithm. + * + * "Sorry to be a wet blanket. Writing a description for this thing for general + * audiences is bloody hard. There's nothing to relate it to." – Satoshi + * Nakamoto + */ +public class Belief extends ARecord { + private static final RecordFormat BELIEF_KEYS = RecordFormat.of(Keywords.ORDERS, Keywords.TIMESTAMP); + + /** + * The latest view of signed Orders held by other Peers + */ + private final BlobMap> orders; + + /** + * The timestamp at which this belief was created + */ + private final long timestamp; + + // private final long timeStamp; + + private Belief(BlobMap> orders, long timestamp) { + super(BELIEF_KEYS); + this.orders = orders; + this.timestamp = timestamp; + } + + @Override + public ACell get(ACell k) { + if (Keywords.ORDERS.equals(k)) return orders; + if (Keywords.TIMESTAMP.equals(k)) return CVMLong.create(timestamp); + return null; + } + + @SuppressWarnings("unchecked") + @Override + protected Belief updateAll(ACell[] newVals) { + BlobMap> newOrders = (BlobMap>) newVals[0]; + long newTimestamp = ((CVMLong) newVals[1]).longValue(); + if ((this.orders == newOrders)&&(this.timestamp==newTimestamp)) { + return this; + } + return new Belief(newOrders, newTimestamp); + } + + /** + * Gets an empty Belief + * @return Empty Belief + */ + public static Belief initial() { + return create(BlobMaps.empty()); + } + + /** + * Create a Belief with a single order signed by the given key pair, using initial timestamp. + * @param kp Peer Key pair with which to sign the order. + * @param order Order of blocks that the Peer is proposing + * @return new Belief representing the isolated belief of a single Peer. + */ + public static Belief create(AKeyPair kp, Order order) { + BlobMap> orders=BlobMap.of(kp.getAccountKey(),kp.signData(order)); + return create(orders); + } + + + private static Belief create(BlobMap> orders, long timestamp) { + return new Belief(orders, timestamp); + } + + private static Belief create(BlobMap> orders) { + return create(orders, Constants.INITIAL_TIMESTAMP); + } + + /** + * Create a Belief with a single empty order. USeful for Peer startup. + * + * @param kp Keypair for Peer + * @return New Belief + */ + public static Belief createSingleOrder(AKeyPair kp) { + AccountKey address = kp.getAccountKey(); + SignedData order = kp.signData(Order.create()); + return create(BlobMap.of(address, order)); + } + + /** + * The Belief merge function + * + * @param mc MergeContext for Belief Merge + * @param beliefs An array of Beliefs. May contain nulls, which will be ignored. + * @return The updated merged belief, or the same Belief if there is no change. + * @throws BadSignatureException In case of a bad signature + * @throws InvalidDataException In case of invalid data + */ + public Belief merge(MergeContext mc, Belief... beliefs) throws BadSignatureException, InvalidDataException { + Belief newBelief = mergeOnce(mc, beliefs); + + // May repeat belief update until stable, this handles the case when the Peer's + // own voting stake is sufficient to change proposed / actual consensus + // if we updated the Belief, then do a quick update again. + // this may be needed to stabilise state in the case that this peer's update + // changes the consensus + if (this != newBelief) { + newBelief = newBelief.mergeOnce(mc); + } + return newBelief; + } + + /** + * Merges Beliefs one time. This may need to be repeated. + * + * @param mc MergeContext for Belief Merge + * @param beliefs An array of Beliefs. May contain nulls, which will be ignored. + * @return The updated merged belief, or the same Belief if there is no change. + * @throws BadSignatureException In case of a bad signature + * @throws InvalidDataException In case of invalid data + */ + Belief mergeOnce(MergeContext mc, Belief... beliefs) throws BadSignatureException, InvalidDataException { + + Counters.beliefMerge++; + + // accumulate combined list of latest chains for all peers + final BlobMap> accOrders = accumulateOrders(mc, beliefs); + + // vote for new proposed chain + final BlobMap> resultOrders = vote(mc, accOrders); + if (resultOrders == null) return this; + + // update my belief with the resulting Orders + long newTimestamp = mc.getTimeStamp(); + if ((orders == resultOrders) && (timestamp == newTimestamp)) return this; + final Belief result = new Belief(resultOrders, newTimestamp); + + return result; + } + + private BlobMap> accumulateOrders(MergeContext mc, + Belief[] beliefs) { + // Initialise result with existing Orders from this Belief + BlobMap> result = this.orders; + + // assemble the latest list of orders from all peers + for (Belief belief : beliefs) { + if (belief == null) continue; // ignore null beliefs, might happen if invalidated + if (belief.equals(this)) continue; // ignore an identical belief. Nothing to update. + BlobMap> bOrders = belief.orders; + + long bcount=bOrders.count(); + for (long i=0; i> be=bOrders.entryAt(i); + ABlob key=be.getKey(); + + // Skip merging own Key. We should always have our own latest Order + if(key.equalsBytes(mc.getAccountKey())) continue; + + SignedData a=result.get(key); + if (a == null) {result=result.assocEntry(be); continue;} + SignedData b=be.getValue(); + if (b == null) continue; + + // Check signature + if (!b.checkSignature()) { + // TODO: Better handling than just ignoring, e.g. slashing? + continue; + }; + + if (a.equals(b)) continue; // PERF: fast path for no changes + + Order ac = a.getValue(); + Order bc = b.getValue(); + + // TODO: penalise inconsistency? + // TODO: check for forks / inconsistent values? + // TODO: check logic? + + // prefer advanced consensus first! + if (bc.getConsensusPoint() > ac.getConsensusPoint()) {result=result.assocEntry(be); continue;}; + + // prefer longer orders, must be later? + if (bc.getBlockCount() > ac.getBlockCount()) {result=result.assocEntry(be); continue;}; + + // prefer advanced proposals + if (bc.getProposalPoint() > ac.getProposalPoint()) {result=result.assocEntry(be); continue;}; + + // keep current view (more stable?) + } + } + return result; + } + + /** + * Conducts a stake-weighted vote across a map of consistent chains, in the + * given merge context + * + * @param accOrders Accumulated map for latest Orders received from all Peer Beliefs + * @param mc Merge context + * @param filteredChains + * @return + * @throws BadSignatureException @ + */ + private BlobMap> vote(final MergeContext mc, final BlobMap> accOrders) + throws BadSignatureException { + AccountKey myAddress = mc.getAccountKey(); + + // get current Order for this peer. + final Order myOrder = getMyOrder(mc); + assert (myOrder != null); // we should always have a Order! + + // get the Consensus state from this Peer's current perspective + // this is needed for peer weights: we only trust peers who have stake in the + // current consensus! + State votingState = mc.getConsensusState(); + + // filter chains for compatibility with current chain for inclusion in Initial Voting Set + // TODO: figure out what to do with new blocks filtered out? + final BlobMap> filteredOrders = accOrders.filterValues(signedOrder -> { + try { + Order otherOrder = signedOrder.getValue(); + return myOrder.checkConsistent(otherOrder); + } catch (Exception e) { + throw Utils.sneakyThrow(e); + } + }); + + // Current Consensus Point + long consensusPoint = myOrder.getConsensusPoint(); + + // Compute stake for all peers in consensus state + AMap peers = votingState.getPeers(); + HashMap weightedStakes = votingState.computeStakes(); + double totalStake = weightedStakes.get(null); + + // Extract unique proposed chains from provided map, computing vote for each. + // compute the total weighted vote at the same time in accumulator + // Peers with no stake should be ignored (might be old peers etc.) + HashMap stakedOrders = new HashMap<>(peers.size()); + double consideredStake = prepareStakedOrders(filteredOrders, weightedStakes, stakedOrders); + + // Get the winning chain for this peer, including new blocks encountered + AVector winningBlocks = computeWinningOrder(stakedOrders, consensusPoint, consideredStake); + if (winningBlocks == null) return null; // if no voting stake on any chain + + // winning chain should have same consensus as my initial chain + Order winningOrder = myOrder.updateBlocks(winningBlocks); + + final double P_THRESHOLD = totalStake * Constants.PROPOSAL_THRESHOLD; + final Order proposedOrder = updateProposal(winningOrder, stakedOrders, P_THRESHOLD); + + assert (proposedOrder != null); + + final double C_THRESHOLD = totalStake * Constants.CONSENSUS_THRESHOLD; + final Order consensusOrder = updateConsensus(proposedOrder, stakedOrders, C_THRESHOLD); + + BlobMap> resultOrders = filteredOrders; + if (!consensusOrder.equals(myOrder)) { + // Only sign and update Order if it has changed + final SignedData signedOrder = mc.sign(consensusOrder); + resultOrders = resultOrders.assoc(myAddress, signedOrder); + } + return resultOrders; + } + + /** + * Updates the consensus point for the winning Order, given an overall map of + * staked orders and consensus threshold. + */ + private Order updateConsensus(Order proposedOrder, HashMap stakedOrders, double THRESHOLD) { + AVector proposedBlocks = proposedOrder.getBlocks(); + ArrayList agreedChains = Utils.sortListBy(new Function() { + @Override + public Long apply(Order c) { + // scoring function scores by level of proposed agreement with proposed chain + // in order to sort by length of matched proposals + long blockMatch = proposedBlocks.commonPrefixLength(c.getBlocks()); + + long minProposal = Math.min(proposedOrder.getProposalPoint(), c.getProposalPoint()); + + long match = Math.min(blockMatch, minProposal); + if (match <= proposedOrder.getConsensusPoint()) return null; // skip if no progress vs existing + // consensus + return -match; + } + }, stakedOrders.keySet()); + int numAgreed = agreedChains.size(); + // assert(proposedChain.equals(agreedChains.get(0))); + double accumulatedStake = 0.0; + int i = 0; + for (; i < numAgreed; i++) { + Order c = agreedChains.get(i); + Double chainStake = stakedOrders.get(c); + accumulatedStake += chainStake; + if (accumulatedStake > THRESHOLD) break; + } + + if (i < numAgreed) { + // we have a consensus! + Order lastAgreed = agreedChains.get(i); + long prefixMatch = proposedOrder.getBlocks().commonPrefixLength(lastAgreed.getBlocks()); + long proposalMatch = Math.min(proposedOrder.getProposalPoint(), lastAgreed.getProposalPoint()); + long newConsensusPoint = Math.min(prefixMatch, proposalMatch); + if (newConsensusPoint < proposedOrder.getConsensusPoint()) { + throw new Error("Consensus going backwards! prefix=" + prefixMatch + " propsalmatch=" + proposalMatch); + } + return proposedOrder.withConsenusPoint(newConsensusPoint); + } else { + return proposedOrder; + } + } + + /** + * Updates the proposal point for the winning Order, given an overall map of + * staked Orders and consensus threshold. + */ + private Order updateProposal(Order winningOrder, HashMap stakedOrders, double THRESHOLD) { + AVector winningBlocks = winningOrder.getBlocks(); + + // sort all chains according to extent of agreement with winning chain + ArrayList agreedOrders = sortByAgreement(stakedOrders, winningBlocks); + int numAgreed = agreedOrders.size(); + + // accumulate stake to see how many agreed chains are required to meet proposal + // threshold + double accumulatedStake = 0.0; + int i = 0; + for (; i < numAgreed; i++) { + Order c = agreedOrders.get(i); + double orderStake = stakedOrders.get(c); + accumulatedStake += orderStake; + if (accumulatedStake > THRESHOLD) break; + } + + if (i < numAgreed) { + // we have a proposed consensus + Order lastAgreed = agreedOrders.get(i); + AVector lastBlocks = lastAgreed.getBlocks(); + long newProposalPoint = winningBlocks.commonPrefixLength(lastBlocks); + return winningOrder.withProposalPoint(newProposalPoint); + } else { + return winningOrder; + } + } + + /** + * Sorts a set of Orders according to level of agreement with a given vector of + * Blocks. Orders with longest common prefix length are placed first. + * + * @param stakedOrders Map with Orders as keys + * @param winningBlocks Vector of blocks to seek agreement with + * @return List of Orders in agreement order + */ + private ArrayList sortByAgreement(HashMap stakedOrders, AVector winningBlocks) { + return Utils.sortListBy(new Function() { + @Override + public Long apply(Order c) { + long match = winningBlocks.commonPrefixLength(c.getBlocks()); + return -match; // sort highest matches first + } + }, stakedOrders.keySet()); + } + + /** + * Gets an ordered list of new blocks from a collection of Chains. Ordering is a + * partial order based on when a block is first observed. This is an important + * heuristic (thou to avoid re-ordering new blocks from the same peer. + */ + private static ArrayList collectNewBlocks(Collection> orders, long consensusPoint) { + // We want to preserve order, remove duplicates + HashSet newBlocks = new HashSet<>(); + ArrayList newBlocksOrdered = new ArrayList<>(); + for (AVector blks : orders) { + if (blks.count()<=consensusPoint) continue; + Iterator it = blks.listIterator(consensusPoint); + while (it.hasNext()) { + Block b = it.next(); + if (!newBlocks.contains(b)) { + newBlocks.add(b); + newBlocksOrdered.add(b); + } + } + } + return newBlocksOrdered; + } + + /** + * Compute the new winning Order for this Peer, including any new blocks + * encountered + * + * @param stakedOrders Amount of stake on each distinct Order + * @param consensusPoint Current consensus point + * @param initialTotalStake Total stake under consideration + * @return Vector of Blocks in wiing Order + */ + public static AVector computeWinningOrder(HashMap stakedOrders, long consensusPoint, + double initialTotalStake) { + assert (!stakedOrders.isEmpty()); + // Get the Voting Set. Will be updated each round to winners of previous round. + HashMap, Double> votingSet = combineToBlocks(stakedOrders); + + // Accumulate new blocks. + ArrayList newBlocksOrdered = collectNewBlocks(votingSet.keySet(), consensusPoint); + + double totalStake = initialTotalStake; + long point = consensusPoint; + + findWinner: + while (votingSet.size() > 1) { + // Accumulate candidate winning Blocks for this round, indexed by next Block + HashMap, Double>> blockVotes = new HashMap<>(); + + for (Map.Entry, Double> me : votingSet.entrySet()) { + AVector blocks = me.getKey(); + long cCount = blocks.count(); + + if (cCount <= point) continue; // skip Ordering with insufficient blocks: cannot win this round + + Block b = blocks.get(point); + + // update hashmap of Orders voting for each block (i.e. agreed on current Block) + HashMap, Double> agreedOrders = blockVotes.get(b); + if (agreedOrders == null) { + agreedOrders = new HashMap<>(); + blockVotes.put(b, agreedOrders); + } + Double stake = me.getValue(); + agreedOrders.put(blocks, stake); + if (stake >= totalStake * 0.5) { + // have a winner for sure, no point continuing so populate final Voting set and break + votingSet.clear(); + votingSet.put(blocks, stake); + break findWinner; + } + } + + if (blockVotes.size() == 0) { + // we have multiple chains, but no more blocks - so they should be all equal + // we can break loop and continue with an arbitrary choice + break findWinner; + } + + Map.Entry, Double>> winningResult = null; + double winningVote = Double.NEGATIVE_INFINITY; + for (Map.Entry, Double>> me : blockVotes.entrySet()) { + HashMap, Double> agreedChains = me.getValue(); + double blockVote = computeVote(agreedChains); + if (blockVote > winningVote) { + winningVote = blockVote; + winningResult = me; + } + } + + if (winningResult==null) throw new Error("This shouldn't happen!"); + votingSet = winningResult.getValue(); // Update Orderings to be included in next round + totalStake = winningVote; // Total Stake among winning Orderings + + // advance to next block position for next round + point++; + } + + if (votingSet.size() == 0) { + // no vote for any Order. Might happen if the peer doesn't have any stake + // and doesn't have any Orders from other peers with stake? + return null; + } + AVector winningBlocks = votingSet.keySet().iterator().next(); + + // add new blocks back to winning chain if not already included + AVector fullWinningBlocks = appendNewBlocks(winningBlocks, newBlocksOrdered, consensusPoint); + + return fullWinningBlocks; + } + + private static final AVector appendNewBlocks(AVector blocks, ArrayList newBlocksOrdered, + long consensusPoint) { + HashSet newBlocks = new HashSet<>(); + newBlocks.addAll(newBlocksOrdered); + + // exclude new blocks already in the base Order + // TODO: what about blocks already in consensus? + Iterator it = blocks.listIterator(Math.min(blocks.count(), consensusPoint)); + while (it.hasNext()) { + newBlocks.remove(it.next()); + } + newBlocksOrdered.removeIf(b -> !newBlocks.contains(b)); + + // sort new blocks by timestamp and append to winning Order + // must be a stable sort to maintain order from equal timestamps + newBlocksOrdered.sort(Block.TIMESTAMP_COMPARATOR); + + AVector fullBlocks = blocks.appendAll(newBlocksOrdered); + return fullBlocks; + } + + /** + * Combine stakes from multiple orders to a single stake for each distinct Block ordering. + * + * @param stakedOrders + * @return Map of AVector to total stake + */ + private static HashMap, Double> combineToBlocks(HashMap stakedOrders) { + HashMap, Double> result = new HashMap<>(); + for (Map.Entry e : stakedOrders.entrySet()) { + Order c = e.getKey(); + Double stake = e.getValue(); + AVector blocks = c.getBlocks(); + Double acc = result.get(blocks); + if (acc == null) { + result.put(blocks, stake); + } else { + result.put(blocks, acc + stake); + } + } + return result; + } + + /** + * Computes the total vote for all entries in a HashMap + * + * @param The type of values used as keys in the HashMap + * @param m A map of values to votes + * @return The total voting stake + */ + public static double computeVote(HashMap m) { + double result = 0.0; + for (Map.Entry me : m.entrySet()) { + result += me.getValue(); + } + return result; + } + + /** + * Compute the total stake for every distinct Order seen. Stores results in + * a map of Orders to staked value. + * + * @param peerOrders A map of peer addresses to signed proposed Orders + * @param peerStakes A map of peers addresses to weighted stakes for each peer + * @param dest Destination hashmap to store the stakes for each Order + * @return The total stake of all chains among peers under consideration + */ + public static double prepareStakedOrders(AMap> peerOrders, + HashMap peerStakes, HashMap dest) { + return peerOrders.reduceValues((acc, signedOrder) -> { + try { + // Get the Order for this peer + Order order = signedOrder.getValue(); + AccountKey cAddress = signedOrder.getAccountKey(); + Double cStake = peerStakes.get(cAddress); + if ((cStake == null) || (cStake == 0.0)) return acc; + Double stake = dest.get(order); + if (stake == null) { + dest.put(order, cStake); // new Order to consider + } else { + dest.put(order, stake + cStake); // add stake to existing Order + } + return acc + cStake; + } catch (Exception e) { + throw Utils.sneakyThrow(e); + } + }, 0.0); + } + + /** + * Gets the Order for the current peer specified by a MergeContext in this + * Belief + * + * @param mc + * @return Order for current Peer, or null if not found + * @throws BadSignatureException + */ + private Order getMyOrder(MergeContext mc) throws BadSignatureException { + AccountKey myAddress = mc.getAccountKey(); + SignedData signed = (SignedData) orders.get(myAddress); + if (signed == null) return null; + assert (signed.getAccountKey().equals(myAddress)); + return signed.getValue(); + } + + /** + * Updates this Belief with a new set of Chains for each peer address + * + * @param newOrders New map of peer keys to Orders + * @return The updated belief, or the same Belief if no change. + */ + public Belief withOrders(BlobMap> newOrders) { + if (newOrders == orders) return this; + return Belief.create(newOrders); + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=getTag(); + return encodeRaw(bs,pos); + } + + @Override + public int estimatedEncodingSize() { + return 1+orders.estimatedEncodingSize()+12; + } + + /** + * Read a Belief from a ByteBuffer. Assumes tag already read. + * @param bb ByteBuffer to read from + * @return Belief instance + * @throws BadFormatException If encoding is invalid + */ + public static Belief read(ByteBuffer bb) throws BadFormatException { + BlobMap> chains = Format.read(bb); + if (chains == null) throw new BadFormatException("Null orders in Belief"); + CVMLong timestamp = Format.read(bb); + if (timestamp == null) throw new BadFormatException("Null timestamp"); + return new Belief(chains, timestamp.longValue()); + } + + @Override + public byte getTag() { + return Tag.BELIEF; + } + + /** + * Gets the current Order for a given Address within this Belief. + * + * @param address Address of peer + * @return The chain for the peer within this Belief, or null if noy found. + */ + public Order getOrder(AccountKey address) { + SignedData sc = orders.get(address); + if (sc == null) return null; + return sc.getValue(); + } + + /** + * Get the map of orders for this Belief + * @return Orders map + */ + public BlobMap> getOrders() { + return orders; + } + + @Override + public void validateCell() throws InvalidDataException { + if (orders == null) throw new InvalidDataException("Null orders", this); + orders.validateCell(); + } + + /** + * Returns the timestamp of this Belief. A Belief should have a new timestamp if + * and only if the Peer incorporates new information. + * @return Timestamp of belief + */ + public long getTimestamp() { + return timestamp; + } + + @Override + public boolean equals(AMap a) { + if (this == a) return true; // important optimisation for e.g. hashmap equality + if (a == null) return false; + if (a.getTag()!=getTag()) return false; + Belief as=(Belief)a; + return equals(as); + } + + /** + * Tests if this Belief is equal to another + * @param a Belief to compare with + * @return true if equal, false otherwise + */ + public boolean equals(Belief a) { + if (a == null) return false; + Hash h=this.cachedHash(); + if (h!=null) { + Hash ha=a.cachedHash(); + if (ha!=null) return Utils.equals(h, ha); + } + + if (timestamp!=a.timestamp) return false; + if (!(Utils.equals(orders, a.orders))) return false; + return true; + } + +} diff --git a/convex-core/src/main/java/convex/core/Block.java b/convex-core/src/main/java/convex/core/Block.java new file mode 100644 index 000000000..148a8d626 --- /dev/null +++ b/convex-core/src/main/java/convex/core/Block.java @@ -0,0 +1,243 @@ +package convex.core; + +import java.nio.ByteBuffer; +import java.util.Comparator; +import java.util.List; + +import convex.core.data.ACell; +import convex.core.data.AMap; +import convex.core.data.ARecord; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.Format; +import convex.core.data.Hash; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.SignedData; +import convex.core.data.Tag; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.RT; +import convex.core.lang.impl.RecordFormat; +import convex.core.transactions.ATransaction; +import convex.core.util.Utils; + +/** + * A block contains an ordered collection of signed transactions that may be applied + * collectively as part of a state update. + * + * Blocks represent the units of novelty in the consensus system: a future state is + * 100% deterministic given the previous state and the Block to be applied. + * + * "Man, the living creature, the creating individual, is always more important + * than any established style or system." - Bruce Lee + * + */ +public final class Block extends ARecord { + private final long timestamp; + private final AVector> transactions; + private final AccountKey peerKey; + + private static final Keyword[] BLOCK_KEYS = new Keyword[] { Keywords.TIMESTAMP, Keywords.TRANSACTIONS, Keywords.PEER }; + private static final RecordFormat FORMAT = RecordFormat.of(BLOCK_KEYS); + + /** + * Comparator to sort blocks by timestamp + */ + static final Comparator TIMESTAMP_COMPARATOR = new Comparator<>() { + @Override + public int compare(Block a, Block b) { + int sig = Long.compare(a.getTimeStamp(), b.getTimeStamp()); + return sig; + } + }; + + private Block(long timestamp, AVector> transactions, AccountKey peer) { + super(FORMAT); + this.timestamp = timestamp; + this.transactions = transactions; + this.peerKey=peer; + + if (peerKey==null) throw new Error("Trying to construct block with null peer key"); + } + + @Override + public ACell get(ACell k) { + if (Keywords.TIMESTAMP.equals(k)) return CVMLong.create(timestamp); + if (Keywords.TRANSACTIONS.equals(k)) return transactions; + if (Keywords.PEER.equals(k)) return peerKey; + return null; + } + + @SuppressWarnings("unchecked") + @Override + protected Block updateAll(ACell[] newVals) { + long newTimestamp = RT.ensureLong(newVals[0]).longValue(); + AVector> newTransactions = (AVector>) newVals[1]; + AccountKey newPeer = (AccountKey) newVals[2]; + if ((this.transactions == newTransactions) && (this.timestamp == newTimestamp) && (peerKey==newPeer)) { + return this; + } + return new Block(newTimestamp, newTransactions,newPeer); + } + + /** + * Gets the timestamp of this block + * + * @return Timestamp, as a long value + */ + public long getTimeStamp() { + return timestamp; + } + + /** + * Gets the Peer for this block + * + * @return Address of Peer publishing this block + */ + public AccountKey getPeer() { + return peerKey; + } + + /** + * Creates a block with the given timestamp and transactions + * + * @param timestamp Timestamp for the newly created Block. + * @param transactions A java.util.List instance containing the required transactions + * @param peerKey Peer Key of Peer producing Block + * @return A new Block containing the specified signed transactions + */ + public static Block create(long timestamp, List> transactions, AccountKey peerKey) { + return new Block(timestamp, Vectors.create(transactions),peerKey); + } + + /** + * Creates a block with the given transactions. + * + * @param timestamp Timestamp of block creation, according to Peer + * @param peerKey Public key of Peer producing Block + * @param transactions Vector of transactions to include in Block + * + * @return A new Block containing the specified signed transactions + */ + public static Block create(long timestamp, AccountKey peerKey, AVector> transactions) { + return new Block(timestamp, transactions,peerKey); + } + + /** + * Creates a block with the given transactions. + * + * @param timestamp Timestamp of block creation, according to Peer + * @param peerKey Public key of Peer producing Block + * @param transactions Array of transactions to include in Block + * @return New Block + */ + @SafeVarargs + public static Block of(long timestamp, AccountKey peerKey, SignedData... transactions) { + return new Block(timestamp, Vectors.of((Object[])transactions),peerKey); + } + + /** + * Gets the length of this block in number of transactions + * + * @return Number of transactions on this block + */ + public int length() { + return Utils.checkedInt(transactions.count()); + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=getTag(); + // generic record writeRaw, handles all fields in declared order + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = Utils.writeLong(bs,pos, timestamp); + pos = transactions.encode(bs,pos); + pos = peerKey.writeToBuffer(bs, pos); + return pos; + } + + @Override + public int estimatedEncodingSize() { + return 10+transactions.estimatedEncodingSize()+AccountKey.LENGTH; + } + + /** + * Reads a Block from the given bytebuffer, assuming tag is already read + * + * @param bb ByteBuffer containing Block representation + * @return A Block + * @throws BadFormatException if a Block could noy be read. + */ + public static Block read(ByteBuffer bb) throws BadFormatException { + long timestamp = Format.readLong(bb); + try { + AVector> transactions = Format.read(bb); + if (transactions==null) throw new BadFormatException("Null transactions"); + + AccountKey peer=AccountKey.readRaw(bb); + if (peer==null) throw new BadFormatException("Bad peer key in Block"); + return Block.create(timestamp, peer,transactions); + } catch (ClassCastException e) { + throw new BadFormatException("Error reading Block format", e); + } + } + + /** + * Get the vector of transactions in this Block + * @return Vector of transactions + */ + public AVector> getTransactions() { + return transactions; + } + + @Override + public boolean isCanonical() { + if (!transactions.isCanonical()) return false; + return true; + } + + @Override + public byte getTag() { + return Tag.BLOCK; + } + + @Override + public void validateCell() throws InvalidDataException { + transactions.validateCell(); + } + + @Override + public boolean equals(AMap a) { + if (!(a instanceof Block)) return false; + return equals((Block)a); + } + + /** + * Tests if this Block is equal to another + * @param a PeerStatus to compare with + * @return true if equal, false otherwise + */ + public boolean equals(Block a) { + if (a == null) return false; + if (timestamp!=a.timestamp) return false; + + Hash h=this.cachedHash(); + if (h!=null) { + Hash ha=a.cachedHash(); + if (ha!=null) return Utils.equals(h, ha); + } + + if (!(Utils.equals(peerKey, a.peerKey))) return false; + + if (!(Utils.equals(transactions, a.transactions))) return false; + return true; + } + +} diff --git a/convex-core/src/main/java/convex/core/BlockResult.java b/convex-core/src/main/java/convex/core/BlockResult.java new file mode 100644 index 000000000..0ec9b9320 --- /dev/null +++ b/convex-core/src/main/java/convex/core/BlockResult.java @@ -0,0 +1,211 @@ +package convex.core; + +import java.nio.ByteBuffer; + +import convex.core.data.ACell; +import convex.core.data.AMap; +import convex.core.data.ARecord; +import convex.core.data.AVector; +import convex.core.data.Format; +import convex.core.data.Hash; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.Tag; +import convex.core.data.Vectors; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.impl.RecordFormat; +import convex.core.util.Utils; + +/** + * Class representing the result of applying a Block to a State. + * + * Each transaction in the block has a corresponding result entry, which may + * either be a valid result or an error. + * + */ +public class BlockResult extends ARecord { + private State state; + private AVector results; + + private static final Keyword[] BLOCKRESULT_KEYS = new Keyword[] { Keywords.STATE, + Keywords.RESULTS}; + + private static final RecordFormat FORMAT = RecordFormat.of(BLOCKRESULT_KEYS); + + + private BlockResult(State state, AVector results) { + super(FORMAT); + this.state = state; + this.results = results; + } + + /** + * Create a BlockResult + * @param state Resulting State + * @param results Results of transactions in Block + * @return BlockResult instance + */ + public static BlockResult create(State state, Result[] results) { + int n=results.length; + Object[] rs=new Object[n]; + for (int i=0; i results) { + return new BlockResult(state, results); + } + + /** + * Get the State resulting from this Block. + * @return State after Block is executed + */ + public State getState() { + return state; + } + + /** + * Gets the Results of all transactions in the Block + * @return Vector of Results + */ + public AVector getResults() { + return results; + } + + /** + * Checks if a result at a specific position is an error + * @param i Index of result in block + * @return True if result at index i is an error, false otherwise. + */ + public boolean isError(long i) { + return getResult(i).isError(); + } + + /** + * Gets a specific Result + * @param i Index of Result + * @return Result at specified index for the current Block + */ + public Result getResult(long i) { + return results.get(i); + } + + /** + * Gets the error code for a given transaction + * @param i Index of Result + * @return Error code, or null if the transaction succeeded. + */ + public Object getErrorCode(long i) { + Result result=results.get(i); + return result.getErrorCode(); + } + + @Override + public ACell get(ACell key) { + if (Keywords.STATE.equals(key)) return state; + if (Keywords.RESULTS.equals(key)) return results; + return null; + } + + @Override + public byte getTag() { + return Tag.BLOCK_RESULT; + } + + @SuppressWarnings("unchecked") + @Override + protected BlockResult updateAll(ACell[] newVals) { + State newState=(State)newVals[0]; + AVector newResults=(AVector)newVals[1]; + return create(newState,newResults); + } + + @Override + public void validateCell() throws InvalidDataException { + // TODO Auto-generated method stub + + } + + @Override + public void validate() throws InvalidDataException { + super.validate(); + results.validate(); + state.validate(); + + long n=results.count(); + for (long i=0; i newResults=Format.read(bb); + if (newResults==null) throw new BadFormatException("Null results"); + return create(newState,newResults); + } + + @Override + public boolean equals(AMap a) { + if (this == a) return true; // important optimisation for e.g. hashmap equality + if (a == null) return false; + if (a.getTag()!=getTag()) return false; + BlockResult as=(BlockResult)a; + return equals(as); + } + + /** + * Tests if this BlockResult is equal to another + * @param a BlockResult to compare with + * @return true if equal, false otherwise + */ + public boolean equals(BlockResult a) { + if (a == null) return false; + Hash h=this.cachedHash(); + if (h!=null) { + Hash ha=a.cachedHash(); + if (ha!=null) return Utils.equals(h, ha); + } + + if (!(Utils.equals(results, a.results))) return false; + if (!(Utils.equals(state, a.state))) return false; + return true; + } + +} diff --git a/convex-core/src/main/java/convex/core/Coin.java b/convex-core/src/main/java/convex/core/Coin.java new file mode 100644 index 000000000..df3b85ea1 --- /dev/null +++ b/convex-core/src/main/java/convex/core/Coin.java @@ -0,0 +1,43 @@ +package convex.core; + +/** + * Static Constants for Coin sizes and total supply + */ +public class Coin { + /** + * Copper coin, the lowest (indivisible) denomination. + */ + public static final long COPPER=1L; + + /** + * Copper coin, a denomination for small change/ Equal to 1000 Copper + */ + public static final long BRONZE=1000*COPPER; + + /** + * Silver Coin, a denomination for small payments. Equal to 1000 Bronze + */ + public static final long SILVER=1000*BRONZE; + + /** + * A denomination suitable for medium/large payments. Equal to 1000 Silver, and divisible into one billion copper coins. + * + * Intended as the primary "human scale" quanity of Convex Coins in regular usage. + */ + public static final long GOLD=1000*SILVER; + + /** + * A large denomination. 1000 Gold. + */ + public static final long DIAMOND=1000*GOLD; + + /** + * A massively valuable amount of Convex Coins. One million Gold. + */ + public static final long EMERALD=1000*DIAMOND; + + /** + * The total Convex Coin maximum supply limit. One billion Gold Coins + */ + public static final long SUPPLY=1000*EMERALD; +} diff --git a/convex-core/src/main/java/convex/core/Constants.java b/convex-core/src/main/java/convex/core/Constants.java new file mode 100644 index 000000000..b0de4c348 --- /dev/null +++ b/convex-core/src/main/java/convex/core/Constants.java @@ -0,0 +1,195 @@ +package convex.core; + +import java.time.Instant; + +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.Format; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMBool; + +/** + * Static class for global configuration constants that affect protocol + * behaviour + */ +public class Constants { + + /** + * Limit of scheduled transactions run in a single Block + */ + public static final long MAX_SCHEDULED_TRANSACTIONS_PER_BLOCK = 100; + + /** + * Threshold of stake required to propose consensus + */ + public static final double PROPOSAL_THRESHOLD = 0.50; + + /** + * Threshold of stake required to confirm consensus + */ + public static final double CONSENSUS_THRESHOLD = 0.67; + + /** + * Initial timestamp for new States + */ + public static final long INITIAL_TIMESTAMP = Instant.parse("2020-02-02T00:20:20.0202Z").toEpochMilli(); + + /** + * Juice price in the initial Genesis State + */ + public static final long INITIAL_JUICE_PRICE = 2L; + + /** + * Initial memory Pool of 1gb + */ + public static final long INITIAL_MEMORY_POOL = 1000000000L; + + /** + * Initial memory price per byte + */ + public static final long INITIAL_MEMORY_PRICE = 10L; + + /** + * Max juice allowable for execution of a single transaction. + */ + public static final long MAX_TRANSACTION_JUICE = 1000000; + + /** + * Constant to set deletion of Etch temporary files on exit. Probably should be true, unless you want to dubug temp files. + */ + public static final boolean ETCH_DELETE_TEMP_ON_EXIT = true; + + /** + * Sequence number used for any new account + */ + public static final long INITIAL_SEQUENCE = 0; + + /** + * Size in bytes of constant overhead applied per non-embedded Cell in memory accounting + */ + public static final long MEMORY_OVERHEAD = 64; + + /** + * Default timeout in milliseconds for client transactions + */ + public static final long DEFAULT_CLIENT_TIMEOUT = 6000; + + /** + * Allowance for initial user / peer accounts + */ + public static final long INITIAL_ACCOUNT_ALLOWANCE = 10000000; + + /** + * Maximum supply of Convex Coins set at protocol level + */ + public static final long MAX_SUPPLY = Coin.SUPPLY; + + /** + * Maximum CVM execution depth + */ + public static final int MAX_DEPTH = 256; + + /** + * Initial global values for a new State + */ + public static final AVector INITIAL_GLOBALS = Vectors.of( + Constants.INITIAL_TIMESTAMP, 0L, Constants.INITIAL_JUICE_PRICE); + + /** + * Maximum length of a symbolic name in characters (keywords and symbols) + * + * Note: Chosen so that small qualified symbolic values are always embedded + */ + public static final int MAX_NAME_LENGTH = 64; + + /** + * Value used to indicate inclusion of a key in a Set. Must be a singleton instance + */ + public static final CVMBool SET_INCLUDED = CVMBool.TRUE; + + /** + * Value used to indicate exclusion of a key from a Set. Must be a singleton instance + */ + public static final CVMBool SET_EXCLUDED = CVMBool.FALSE; + + /** + * Length for public keys + */ + public static final int KEY_LENGTH = 32; + + /** + * Length for Hash values + */ + public static final int HASH_LENGTH = 32; + + /** + * Default number of outgoing connections for a Peer + */ + public static final Integer DEFAULT_OUTGOING_CONNECTION_COUNT = 20; + + /** + * Number of milliseconds average time to drop low-staked Peers + */ + public static final double PEER_CONNECTION_DROP_TIME = 20000; + + /** + * Minimum stake for a PEer to be considered by other Peers in consensus + */ + public static final long MINIMUM_EFFECTIVE_STAKE = Coin.GOLD*1; + + /** + * Default size for client receive buffers. + */ + public static final int RECEIVE_BUFFER_SIZE = Format.LIMIT_ENCODING_LENGTH*10+20; + + /** + * Default size for client receive buffers. + */ + public static final int SEND_BUFFER_SIZE = Format.LIMIT_ENCODING_LENGTH*10+20; + + + /** + * Size of default server socket receive buffer + */ + public static final int SOCKET_SERVER_BUFFER_SIZE = 16*65536; + + /** + * Size of default server socket buffers for a peer connection + */ + public static final int SOCKET_PEER_BUFFER_SIZE = 16*65536; + + /** + * Size of default client socket receive buffer + */ + public static final int SOCKET_RECEIVE_BUFFER_SIZE = 65536; + + /** + * Size of default client socket send buffer + */ + public static final int SOCKET_SEND_BUFFER_SIZE = 65536; + + /** + * Delay before rebroadcasting Belief if not in consensus + */ + public static final long REBROADCAST_DELAY = 200; + + /** + * Delay before a Peer produces another Block + */ + public static final long MIN_BLOCK_TIME = 100; + + /** + * Timeout for syncing with an existing Peer + */ + public static final long PEER_SYNC_TIMEOUT = 60000; + + /** + * Number of fields in a Peer STATUS message + */ + public static final long STATUS_COUNT = 5; + + /** + * Default port for Convex Peers + */ + public static final int DEFAULT_PEER_PORT = 18888; +} diff --git a/convex-core/src/main/java/convex/core/ErrorCodes.java b/convex-core/src/main/java/convex/core/ErrorCodes.java new file mode 100644 index 000000000..c197e9caa --- /dev/null +++ b/convex-core/src/main/java/convex/core/ErrorCodes.java @@ -0,0 +1,190 @@ +package convex.core; + +import convex.core.data.Keyword; + +/** + * Standard codes used for CVM Exceptional Conditions. + * + * An Exceptional Condition may include a Message, which is kept outside CVM state but may be user to return + * information to the relevant client. + */ +public class ErrorCodes { + /** + * Error code for a bad sequence. This Error Condition is generated by the CVM only during transaction + * preparation if the Sequence Number for the new transaction is wrong for the given Account (it must be one + * greater than the Sequence Number of the last transaction executed which is stored in the Account). + * + * This Error code may be returned by Peers before publishing a transaction to the network, if the + * Sequence Number cannot possibly be correct (i.e. less than the current Sequence Number). + * + * The message is expected to be the current sequence number: Clients may use this to automatically correct + * and re-submit transactions with the correct sequence, although this is unreliable if multiple clients are + * sending transactions for the same Account. + */ + public static final Keyword SEQUENCE = Keyword.create("SEQUENCE"); + + /** + * Error code for when the specified account does not have enough available funds to perform an operation + */ + public static final Keyword FUNDS = Keyword.create("FUNDS"); + + /** + * Error code for when a transaction runs out of available juice + */ + public static final Keyword JUICE = Keyword.create("JUICE"); + + /** + * Error code for when a transaction exceeds execution depth limits. Typically, this indicates + * infinite recursion. + */ + public static final Keyword DEPTH = Keyword.create("DEPTH"); + + /** + * Error code for situations where a transaction is unable to complete due to insufficient + * Memory Allowance. + * + * This Error Condition is only be generated by the CVM during the failure of transaction completion. + * Within transactions, memory usage may exceed allowances as long as there is enough juice to + * pay for the temporary allocations. + */ + public static final Keyword MEMORY = Keyword.create("MEMORY"); + + /** + * Error code when attempting to perform an action using a non-existent Account + */ + public static final Keyword NOBODY = Keyword.create("NOBODY"); + + /** + * Error code when function or expander application has an inappropriate number of arguments. + * Arity is checked first: it takes precedence over CAST and ARGUMENT errors. + */ + public static final Keyword ARITY = Keyword.create("ARITY"); + + /** + * Error code when an undeclared symbol is accessed + */ + public static final Keyword UNDECLARED = Keyword.create("UNDECLARED"); + + /** + * Error code when the type of some argument cannot be cast to a suitable type for + * some requested operation. ARITY errors take predecence over CAST errors if both + * are applicable. + */ + public static final Keyword CAST = Keyword.create("CAST"); + + /** + * Error code for when indexed access is attempted that is out of bounds for some sequential object. + * + */ + public static final Keyword BOUNDS = Keyword.create("BOUNDS"); + + /** + * Error code for when an argument is of the correct type, but is not an allowable value. + */ + public static final Keyword ARGUMENT = Keyword.create("ARGUMENT"); + + /** + * Error code for a request that would normally be valid, but failed because some aspect of + * actor / system state was wrong. Typically indicates that some preparatory step was omitted, + * appropriate pre-conditions were not checked, or an operation was attempted at an inappropriate time + */ + public static final Keyword STATE = Keyword.create("STATE"); + + /** + * Error code caused by compilation failure with an invalid AST. Should only occur during + * compile phase of on-chain Compiler + */ + public static final Keyword COMPILE = Keyword.create("COMPILE"); + + /** + * Error code caused by failure to successfully expand an AST node. Should only occur during + * expand phase of on-chain Compiler + */ + public static final Keyword EXPAND = Keyword.create("EXPAND"); + + /** + * Error code indicating that an asserted condition was not met. This usually indicates invalid + * input that failed a precondition check. The message should be used to give meaningful feedback to + * the User. + */ + public static final Keyword ASSERT = Keyword.create("ASSERT"); + + /** + * Error code indicating that an a trust condition was violated. This usually means a USer or Actor + * attempted to perform an unauthorised operation. + */ + public static final Keyword TRUST = Keyword.create("TRUST"); + + /** + * ErrorCode for an unexpected Error. Likely fatal. + */ + public static final Keyword UNEXPECTED = Keyword.create("UNEXPECTED"); + + /** + * Error code for unhandled exceptions + */ + public static final Keyword EXCEPTION = Keyword.create("EXCEPTION"); + + // Error codes for non-error values + + /** + * Exceptional Condition indicating a halt operation was executed. + * + * This will halt the currently executing transaction context and return to the caller. + */ + public static final Keyword HALT = Keyword.create("HALT"); + + /** + * Exceptional Condition indicating a recur operation was executed + * + * This will return execution to the surrounding loop or function binding, which will be + * re-executed with new bindings provided to the recur operation. + */ + public static final Keyword RECUR = Keyword.create("RECUR"); + + /** + * Exceptional Condition indicating a tailcall operation has been executed + * + * This will return execution to the surrounding loop or function binding, which will be + * re-executed with new bindings provided to the recur operation. + */ + public static final Keyword TAILCALL = Keyword.create("TAILCALL"); + + /** + * Exceptional Condition indicating a return operation was executed + * + * This will return execution to the caller of surrounding function binding, with whatever + * value is passed to the return operation as a result. + */ + public static final Keyword RETURN = Keyword.create("RETURN"); + + /** + * Exceptional condition indicated a 'reduced' result. + */ + public static final Keyword REDUCED = Keyword.create("REDUCED"); + + /** + * Exceptional Condition indicating a halt operation was executed. + * + * This will terminate the currently executing transaction context, roll back any state changes + * and return to the caller with whatever value is passed as the rollback result. + */ + public static final Keyword ROLLBACK = Keyword.create("ROLLBACK"); + + /** + * Exceptional Condition indicating a bad signature on a transaction. + */ + public static final Keyword SIGNATURE = Keyword.create("SIGNATURE"); + + /** + * Exceptional Condition indicating something is not yet implemented + */ + public static final Keyword TODO = Keyword.create("TODO"); + + /** + * ErrorCode for a FATAL Error. Should trigger Peer shutdown. + */ + public static final Keyword FATAL = Keyword.create("FATAL"); + + +} diff --git a/convex-core/src/main/java/convex/core/MergeContext.java b/convex-core/src/main/java/convex/core/MergeContext.java new file mode 100644 index 000000000..79ed1e0ee --- /dev/null +++ b/convex-core/src/main/java/convex/core/MergeContext.java @@ -0,0 +1,87 @@ +package convex.core; + +import convex.core.crypto.AKeyPair; +import convex.core.data.ACell; +import convex.core.data.AccountKey; +import convex.core.data.SignedData; + +/** + * Class representing the context to be used for a Belief merge/update function. This + * context must be created by a Peer to perform a valid Belief merge. It can be safely + * discarded after use. + * + * SECURITY: contains a hot key pair! We need this to sign new belief updates + * including any chains we want to communicate. Don't allow this to leak + * anywhere! + * + */ +public class MergeContext { + + private final AccountKey publicKey; + private final State state; + private final AKeyPair keyPair; + private final long timestamp; + + private MergeContext(AKeyPair peerKeyPair, long mergeTimestamp, State consensusState) { + this.state = consensusState; + this.publicKey = peerKeyPair.getAccountKey(); + this.keyPair = peerKeyPair; + this.timestamp = mergeTimestamp; + } + + /** + * Create a MergeContext + * @param kp Keypair + * @param timestamp Timestamp + * @param s Consensus State + * @return New MergeContext instance + */ + public static MergeContext create(AKeyPair kp, long timestamp, State s) { + return new MergeContext(kp, timestamp, s); + } + + /** + * Get the address of the current Peer (the one performing the merge) + * + * @return The Address of the peer. + */ + public AccountKey getAccountKey() { + return publicKey; + } + + /** + * Sign a value using the keypair for this MergeContext + * @param Type of value + * @param value Value to sign + * @return Signed value + */ + public SignedData sign(T value) { + return SignedData.create(keyPair, value); + } + + /** + * Gets the timestamp of this merge + * @return Timestamp + */ + public long getTimeStamp() { + return timestamp; + } + + /** + * Updates the timestamp of this MergeContext + * @param newTimestamp New timestamp + * @return Updated MergeContext + */ + public MergeContext withTimestamp(long newTimestamp) { + return new MergeContext(keyPair, newTimestamp, state); + } + + /** + * Gets the Consensus State for this merge + * @return Consensus State + */ + public State getConsensusState() { + return state; + } + +} diff --git a/convex-core/src/main/java/convex/core/Order.java b/convex-core/src/main/java/convex/core/Order.java new file mode 100644 index 000000000..2b7cd60fa --- /dev/null +++ b/convex-core/src/main/java/convex/core/Order.java @@ -0,0 +1,309 @@ +package convex.core; + +import java.nio.ByteBuffer; + +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.Format; +import convex.core.data.IRefFunction; +import convex.core.data.Ref; +import convex.core.data.Tag; +import convex.core.data.Vectors; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; + +/** + * Class representing an Ordering of transactions, along with the consensus position. + * + * An Ordering contains: + *
    + *
  • The Vector of known verified Blocks announced by the Peer
  • + *
  • The proposed consensus point (point at which the peer believes there is sufficient + * alignment for consensus)
  • + *
  • The current consensus point (point at which the + * peer has observed sufficient consistent consensus proposals)
  • + *
+ * + * An Ordering is immutable. + * + */ +public class Order extends ACell { + private final AVector blocks; + + private final long proposalPoint; + private final long consensusPoint; + + private Order(AVector blocks, long proposalPoint, long consensusPoint) { + this.blocks = blocks; + this.consensusPoint = consensusPoint; + this.proposalPoint = proposalPoint; + } + + /** + * Create an Order + * @param blocks Blocks in ORder + * @param proposalPoint Proposal Point + * @param consensusPoint Conesnsus Point + * @return New Order instance + */ + private static Order create(AVector blocks, long proposalPoint, long consensusPoint) { + return new Order(blocks, proposalPoint, consensusPoint); + } + + /** + * Create an empty Order + + * @return New Order instance + */ + public static Order create() { + return create(Vectors.empty(), 0, 0); + } + + private byte getRecordTag() { + return Tag.ORDER; + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=getRecordTag(); + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = blocks.encode(bs,pos); + pos = Format.writeVLCLong(bs,pos, proposalPoint); + pos = Format.writeVLCLong(bs,pos, consensusPoint); + return pos; + } + + @Override + public int estimatedEncodingSize() { + return blocks.estimatedEncodingSize()+30; // blocks plus enough size for points + } + + /** + * Decode an Order from a ByteBuffer + * @param bb ByteBuffer to read from + * @return Order instance + * @throws BadFormatException If encoding format is invalid + */ + public static Order read(ByteBuffer bb) throws BadFormatException { + AVector blocks = Format.read(bb); + if (blocks==null) { + throw new BadFormatException("Null blocks in Order!"); + } + long bcount=blocks.count(); + + long pp = Format.readVLCLong(bb); + long cp = Format.readVLCLong(bb); + + if ((cp < 0) || (cp > bcount)) { + throw new BadFormatException("Consensus point outside current block range: " + cp); + } + if (ppbcount) { + throw new BadFormatException("Proposal point outside block range: " + pp); + } + return new Order(blocks, pp, cp); + } + + + + @Override + public boolean isCanonical() { + // Always canonical? + return true; + } + + @Override public final boolean isCVMValue() { + // Orders exist outside CVM only + return false; + } + + @Override + public void print(StringBuilder sb) { + sb.append("{"); + sb.append(":prop " + getProposalPoint() + ","); + sb.append(":cons " + getConsensusPoint() + ","); + sb.append(":hash " + getHash() + ","); + sb.append(":blocks "); + blocks.print(sb); + sb.append("}\n"); + } + + /** + * Checks if another Order is consistent with this Order. + * + * Order is defined as consistent iff: + *
    + *
  • Blocks are equal up to the Consensus + * Point of this Order + *
  • + *
+ * + * @param bc Order to compare with + * @return True if chains are consistent, false otherwise. + */ + public boolean checkConsistent(Order bc) { + long commonPrefix = blocks.commonPrefixLength(bc.blocks); + return commonPrefix >= consensusPoint; + } + + /** + * Gets the Consensus Point of this Order + * @return Consensus Point + */ + public long getConsensusPoint() { + return consensusPoint; + } + + /** + * Gets the Proposal Point of this Order + * @return Proposal Point + */ + public long getProposalPoint() { + return proposalPoint; + } + + /** + * Gets the Blocks in this Order + * @return Vector of Blocks + */ + public AVector getBlocks() { + return blocks; + } + + /** + * Get a specific Block in this Order + * @param i Index of Block + * @return Block at specified index. + */ + public Block getBlock(long i) { + return blocks.get(i); + } + + /** + * Propose a new block of transactions in this Order + * + * @param block Block to append + * @return The updated chain + */ + public Order propose(Block block) { + AVector newBlocks = blocks.append(block); + return create(newBlocks, proposalPoint, consensusPoint); + } + + /** + * Updates blocks in this Order. Returns the same Order if the blocks are identical. + * @param newBlocks New blocks to use + * @return Updated Order, or the same order if unchanged + */ + public Order withBlocks(AVector newBlocks) { + if (blocks == newBlocks) return this; + return create(newBlocks, proposalPoint, consensusPoint); + } + + /** + * Updates this Order with a new proposal position. It is an error to set the + * proposal point before the consensus point, or beyond the last block. + * + * @param newProposalPoint New Proposal Point in Order + * @return Updated Order + */ + public Order withProposalPoint(long newProposalPoint) { + if (this.proposalPoint == newProposalPoint) return this; + if (newProposalPoint < consensusPoint) { + throw new IllegalArgumentException( + "Trying to move proposed consensus before confirmed consensus?! " + newProposalPoint); + } + if (newProposalPoint > blocks.count()) throw new IndexOutOfBoundsException("Block index: " + newProposalPoint); + return new Order(blocks, newProposalPoint, consensusPoint); + } + + /** + * Updates this Order with a new consensus position. + * + * Proposal point will be set to the max of the consensus point and the current + * proposal point + * + * @param newConsensusPoint New consensus point + * @return Updated chain, or this Chain instance if no change. + */ + public Order withConsenusPoint(long newConsensusPoint) { + if (this.consensusPoint == newConsensusPoint) return this; + if (newConsensusPoint > blocks.count()) + throw new IndexOutOfBoundsException("Block index: " + newConsensusPoint); + long newProposalPoint = Math.max(proposalPoint, newConsensusPoint); + return create(blocks, newProposalPoint, newConsensusPoint); + } + + /** + * Get the number of Blocks in this Order + * @return Number of Blocks + */ + public long getBlockCount() { + return blocks.count(); + } + + /** + * Clears the consensus and proposal point + * @return Updated order with zeroed consensus positions + */ + public Order withoutConsenus() { + return create(blocks, 0, 0); + } + + /** + * Update this chain with a new list of blocks + * + * @param newBlocks New vector of blocks to use in this Chain + * @return The updated Order + */ + public Order updateBlocks(AVector newBlocks) { + if (blocks == newBlocks) return this; + long prefix = blocks.commonPrefixLength(newBlocks); + long newProposalPoint = Math.min(prefix, proposalPoint); + long newConsensusPoint = Math.min(consensusPoint, newProposalPoint); + return create(newBlocks, newProposalPoint, newConsensusPoint); + } + + @Override + public void validate() throws InvalidDataException { + super.validate(); + blocks.validate(); + } + + @Override + public void validateCell() throws InvalidDataException { + + } + + @Override + public int getRefCount() { + return blocks.getRefCount(); + } + + @Override + public Ref getRef(int i) { + return blocks.getRef(i); + } + + @Override + public Order updateRefs(IRefFunction func) { + AVector newBlocks = blocks.updateRefs(func); + return this.withBlocks(newBlocks); + } + + @Override + public byte getTag() { + return Tag.ORDER; + } + + @Override + public ACell toCanonical() { + return this; + } +} diff --git a/convex-core/src/main/java/convex/core/Peer.java b/convex-core/src/main/java/convex/core/Peer.java new file mode 100644 index 000000000..67a1c4b05 --- /dev/null +++ b/convex-core/src/main/java/convex/core/Peer.java @@ -0,0 +1,564 @@ +package convex.core; + +import java.io.IOException; +import java.util.function.Consumer; + +import convex.core.crypto.AKeyPair; +import convex.core.data.ACell; +import convex.core.data.AMap; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.Address; +import convex.core.data.BlobMap; +import convex.core.data.Hash; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.Maps; +import convex.core.data.PeerStatus; +import convex.core.data.Ref; +import convex.core.data.SignedData; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadSignatureException; +import convex.core.exceptions.InvalidDataException; +import convex.core.init.Init; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.store.AStore; +import convex.core.store.Stores; +import convex.core.transactions.ATransaction; +import convex.core.util.Utils; + +/** + *

+ * Immutable class representing the encapsulated state of a Peer + *

+ * + * SECURITY: + *
    + *
  • Needs to contain the Peer's unlocked private key for online signing.
  • + *
  • Manages Peer state transitions given external events. Must do so + * correctly.
  • + *
+ * + *

+ * Must have at least one state, the initial state. New states will be added as + * consensus updates happen. + *

+ * + * + * "Don't worry about what anybody else is going to do. The best way to predict + * the future is to invent it." - Alan Kay + */ +public class Peer { + /** This Peer's key */ + private final AccountKey peerKey; + + /** This Peer's key pair + * + * Make transient to mark that this should never be Persisted by accident + * + */ + private transient final AKeyPair keyPair; + + /** The latest merged belief */ + private final SignedData belief; + + /** + * The latest observed timestamp. This is increased by the Server polling the + * local clock. + */ + private final long timestamp; + + /** + * Vector of states + */ + private final AVector states; + + /** + * Vector of results + */ + private final AVector blockResults; + + private Peer(AKeyPair kp, SignedData belief, AVector states, AVector results, + long timeStamp) { + this.keyPair = kp; + this.peerKey = kp.getAccountKey(); + this.belief = belief; + this.states = states; + this.blockResults = results; + this.timestamp = timeStamp; + } + + /** + * Constructs a Peer instance from persisted PEer Data + * @param keyPair Key Pair for Peer + * @param peerData Peer data map + * @return NEw Peer instance + */ + @SuppressWarnings("unchecked") + public static Peer fromData(AKeyPair keyPair,AMap peerData) { + SignedData belief=(SignedData) peerData.get(Keywords.BELIEF); + AVector results=(AVector) peerData.get(Keywords.RESULTS); + AVector states=(AVector) peerData.get(Keywords.STATES); + long timestamp=belief.getValue().getTimestamp(); + return new Peer(keyPair,belief,states,results,timestamp); + } + + /** + * Gets the Peer Datat map for this Peer + * @return Peer data + */ + public AMap toData() { + return Maps.of( + Keywords.BELIEF,belief, + Keywords.RESULTS,blockResults, + Keywords.STATES,states + ); + } + + /** + * Creates a Peer + * @param peerKP Key Pair + * @param initialState Genesis State + * @return New Peer instance + */ + public static Peer create(AKeyPair peerKP, State initialState) { + Belief belief = Belief.createSingleOrder(peerKP); + SignedData sb = peerKP.signData(belief); + AVector states=Vectors.of(initialState); + + // Ensure initial belief and states are persisted in current store + ACell.createPersisted(sb); + ACell.createPersisted(states); + + // Check belief persistence + Ref> sbr=Ref.forHash(sb.getHash()); + if (sbr==null) { + throw new Error("Belief not correctly persisted! "+sb.getHash()); + } + + return new Peer(peerKP, sb, states, Vectors.empty(), initialState.getTimeStamp().longValue()); + } + + /** + * Create a Peer instance from a remotely acquired Belief + * @param peerKP Peer KeyPair + * @param initialState Initial genesis State of the Network + * @param remoteBelief Remote belief to sync with + * @return New Peer instance + */ + public static Peer create(AKeyPair peerKP, State initialState, Belief remoteBelief) { + Peer peer=create(peerKP,initialState); + try { + peer=peer.mergeBeliefs(remoteBelief); + return peer; + } catch (Throwable e) { + throw Utils.sneakyThrow(e); + } + } + + /** + * Restores a Peer from the Etch database specified in Config + * @param store Store to restore from + * @param keyPair Key Pair to use for restored Peer + * @return Peer instance, or null if root hash was not found + * @throws IOException If store reading failed + */ + public static Peer restorePeer(AStore store,AKeyPair keyPair) throws IOException { + AMap peerData=getPeerData(store); + if (peerData==null) return null; + Peer peer=Peer.fromData(keyPair,peerData); + return peer; + } + + /** + * Gets Peer Data from a Store. + * + * @param store Store to retrieve Peer Datat from + * @return Peer data map, or null if not available + * @throws IOException If a store IO error occurs + */ + public static AMap getPeerData(AStore store) throws IOException { + AStore tempStore=Stores.current(); + try { + Stores.setCurrent(store); + Hash root = store.getRootHash(); + Ref ref=store.refForHash(root); + if (ref==null) return null; // not found case + if (ref.getStatus() peerData=(AMap) ref.getValue(); + return peerData; + } finally { + Stores.setCurrent(tempStore); + } + } + + /** + * Creates a new Peer instance at server startup using the provided + * configuration. Current store must be set to store for server. + * + * @param keyPair Key pair for genesis peer + * @param genesisState Genesis state, or null to generate fresh state + * @return A new Peer instance + */ + public static Peer createGenesisPeer(AKeyPair keyPair, State genesisState) { + if (keyPair == null) throw new IllegalArgumentException("Peer initialisation requires a keypair"); + + if (genesisState == null) { + genesisState=Init.createState(Utils.listOf(keyPair.getAccountKey())); + genesisState=genesisState.withTimestamp(Utils.getCurrentTimestamp()); + } + + return create(keyPair, genesisState); + } + + /** + * Gets a MergeContext for this Peer + * @return MergeContext + */ + public MergeContext getMergeContext() { + return MergeContext.create(keyPair, timestamp, getConsensusState()); + } + + /** + * Updates the timestamp to the specified time, going forwards only + * + * @param newTimestamp New Peer timestamp + * @return This peer upated with the given timestamp + */ + public Peer updateTimestamp(long newTimestamp) { + if (newTimestamp < timestamp) return this; + return new Peer(keyPair, belief, states, blockResults, timestamp); + } + + /** + * Compiles and executes a query on the current consensus state of this Peer. + * + * @param Type of result + * @param form Form to compile and execute. + * @param address Address to use for query execution + * @return The Context containing the query results. Will be NOBODY error if address / account does not exist + */ + @SuppressWarnings("unchecked") + public Context executeQuery(ACell form, Address address) { + State state=getConsensusState(); + + if (address==null) { + return Context.createFake(state).withError(ErrorCodes.NOBODY,"Null Address provided for query"); + } + + Context ctx= Context.createFake(state, address); + + if (state.getAccount(address)==null) { + return ctx.withError(ErrorCodes.NOBODY,"Account does not exist for query: "+address); + } + + Context> ectx = ctx.expandCompile(form); + if (ectx.isExceptional()) { + return (Context) ectx; + } + + AOp op = ectx.getResult(); + Context rctx = ctx.run(op); + return rctx; + } + + /** + * Estimates the coin cost of a executing a given transaction by performing a "dry run". + * + * This will be exact if no intermediate transactions affect the state, and if no time-dependent functionality is used. + * + * @param trans Transaction to test + * @return Estimated cost + */ + public long estimateCost(ATransaction trans) { + Address address=trans.getAddress(); + State state=getConsensusState(); + Context ctx=executeDryRun(trans); + return state.getBalance(address)-ctx.getState().getBalance(address); + } + + /** + * Executes a "dry run" transaction on the current consensus state of this Peer. + * + * @param Type of Result + * @param transaction Transaction to execute + * @return The Context containing the transaction results. + */ + public Context executeDryRun(ATransaction transaction) { + Context ctx=getConsensusState().applyTransaction(transaction); + return ctx; + } + + /** + * Executes a query in this Peer's current Consensus State, using a default address + * @param Type of query result + * @param form Form to execute as a Query + * @return Context after executing query + */ + public Context executeQuery(ACell form) { + return executeQuery(form,Init.getGenesisAddress()); + } + + /** + * Gets the timestamp of this Peer + * @return Timestamp + */ + public long getTimeStamp() { + return timestamp; + } + + /** + * Gets the Peer Key of this Peer. + * @return Peer Key of Peer. + */ + public AccountKey getPeerKey() { + return peerKey; + } + + /** + * Gets the controller Address for this Peer + * @return Address of Peer controller Account, or null if does not exist + */ + public Address getController() { + PeerStatus ps= getConsensusState().getPeer(peerKey); + if (ps==null) return null; + return ps.getController(); + } + + /** + * Gets the Peer Key of this Peer. + * @return Address of Peer. + */ + public AKeyPair getKeyPair() { + return keyPair; + } + + /** + * Get the current Belief of this Peer + * @return Belief + */ + public Belief getBelief() { + return belief.getValue(); + } + + /** + * Get the signed Belief of this Peer + * @return Signed Belief + */ + public SignedData getSignedBelief() { + return belief; + } + + /** + * Signs a value with the keypair of this Peer + * @param Type of value to sign + * @param value Value to sign + * @return Signed data value + */ + public SignedData sign(T value) { + return SignedData.create(keyPair, value); + } + + /** + * Gets the current consensus state for this chain + * + * @return Consensus state for this chain (initial state if no block consensus) + */ + public State getConsensusState() { + return states.get(states.count() - 1); + } + + /** + * Merges a set of new Beliefs into this Peer's belief. Beliefs may be null, in + * which case they are ignored. + * + * @param beliefs An array of Beliefs. May contain nulls, which will be ignored. + * @return Updated Peer after Belief Merge + * @throws InvalidDataException if + * @throws BadSignatureException IF a Signature validation fails + * + */ + public Peer mergeBeliefs(Belief... beliefs) throws BadSignatureException, InvalidDataException { + Belief belief = getBelief(); + MergeContext mc = MergeContext.create(keyPair, timestamp, getConsensusState()); + Belief newBelief = belief.merge(mc, beliefs); + + long ocp=getConsensusPoint(); + Order newOrder=newBelief.getOrder(peerKey); + if (ocp>newBelief.getOrder(peerKey).getConsensusPoint()) { + // This probably shouldn't happen, but just in case..... + System.err.println("Receding consensus? Old CP="+ocp +", New CP="+newOrder.getConsensusPoint()); + @SuppressWarnings("unused") + Belief newBelief2 = belief.merge(mc, beliefs); + + } + + return updateConsensus(newBelief); + } + + /** + * Update this Peer with Consensus State for an updated Belief + * + * @param newBelief + * @return + * @throws BadSignatureException + */ + private Peer updateConsensus(Belief newBelief) { + if (belief.getValue() == newBelief) return this; + Order myOrder = newBelief.getOrder(peerKey); // this peer's chain from new belief + long consensusPoint = myOrder.getConsensusPoint(); + long stateIndex = states.count() - 1; // index of last state + AVector blocks = myOrder.getBlocks(); + + // need to advance states + AVector newStates = this.states; + AVector newResults = this.blockResults; + while (stateIndex < consensusPoint) { // add states until last state is at consensus point + State s = newStates.get(stateIndex); + Block block = blocks.get(stateIndex); + BlockResult br = s.applyBlock(block); + newStates = newStates.append(br.getState()); + newResults = newResults.append(br); + stateIndex++; + } + SignedData sb = keyPair.signData(newBelief); + return new Peer(keyPair, sb, newStates, newResults, timestamp); + } + + /** + * Persist the state of the Peer to the current store. We ensure states and results are also persisted + * @param noveltyHandler Novelty handler for Belief + * @return Updates Peer + */ + public Peer persistState(Consumer> noveltyHandler) { + // Peer Belief must be announced using novelty handler + SignedData sb=this.belief; + sb.announce(noveltyHandler); + + // Persist states + AVector newStates = this.states; + newStates=ACell.createPersisted(newStates).getValue(); + + // Persist results + AVector newResults = this.blockResults; + newResults=ACell.createPersisted(newResults).getValue(); + + return new Peer(this.keyPair, sb, newStates, newResults, this.timestamp); + } + + /** + * Gets the vector of States maintained by this Peer, starting from the + * Genesis state (index 0). + * + * @return Vector of states + */ + public AVector getStates() { + return states; + } + + /** + * Gets the result of a specific transaction + * @param blockIndex Index of Block in Order + * @param txIndex Index of transaction in block + * @return Result from transaction + */ + public Result getResult(long blockIndex, long txIndex) { + return blockResults.get(blockIndex).getResult(txIndex); + } + + /** + * Gets the BlockResult of a specific block index + * @param i Index of Block + * @return BlockResult + */ + public BlockResult getBlockResult(long i) { + return blockResults.get(i); + } + + /** + * Propose a new Block. Adds the block to the current proposed chain for this + * Peer. + * + * @param block Block to publish + * @return Peer after proposing new Block in Peer's own Order + */ + public Peer proposeBlock(Block block) { + Belief b = getBelief(); + BlobMap> orders = b.getOrders(); + + Order myOrder = b.getOrder(peerKey); + if (myOrder==null) myOrder=Order.create(); + + Order newChain = myOrder.propose(block); + SignedData newSignedChain = sign(newChain); + BlobMap> newChains = orders.assoc(peerKey, newSignedChain); + Belief newBelief=b.withOrders(newChains); + return updateConsensus(newBelief); + } + + /** + * Gets the Consensus Point for this Peer + * @return Consensus Point value + */ + public long getConsensusPoint() { + Order order=getPeerOrder(); + if (order==null) return 0; + return order.getConsensusPoint(); + } + + /** + * Gets the current Order for this Peer + * + * @return The Order for this peer in its current Belief. Will return null if the Peer is not a peer in the current consensus state + * + */ + public Order getPeerOrder() { + return getBelief().getOrder(peerKey); + } + + /** + * Gets the current chain this Peer sees for a given peer address + * + * @param peerKey Peer Key + * @return The current Order for the specified peer + */ + public Order getOrder(AccountKey peerKey) { + return getBelief().getOrder(peerKey); + } + + /** + * Returns State as-of timestamp. + * + * Timestamp doesn't need to be an exact match; a leftmost State will be returned - unless timestamp is too old. + * + * @param timestamp Timestamp in milliseconds. + * @return State or null. + */ + public State asOf(CVMLong timestamp) { + return Utils.stateAsOf(states, timestamp); + } + + /** + * Construct a vector of States starting at specified timestamp, and with a given interval in milliseconds. + * + * @param timestamp Timestamp in milliseconds. + * @param interval Interval in milliseconds. + * @param count Number of times to query. + * @return Vector of States. + */ + public AVector asOfRange(CVMLong timestamp, long interval, int count) { + return Utils.statesAsOfRange(states, timestamp, interval, count); + } + + /** + * Get the Network ID for this PEer + * @return Network ID + */ + public Hash getNetworkID() { + return getStates().get(0).getHash(); + } +} diff --git a/convex-core/src/main/java/convex/core/Result.java b/convex-core/src/main/java/convex/core/Result.java new file mode 100644 index 000000000..0d6408a4d --- /dev/null +++ b/convex-core/src/main/java/convex/core/Result.java @@ -0,0 +1,199 @@ +package convex.core; + +import java.nio.ByteBuffer; + +import convex.core.data.ACell; +import convex.core.data.ARecord; +import convex.core.data.ARecordGeneric; +import convex.core.data.AString; +import convex.core.data.AVector; +import convex.core.data.Keywords; +import convex.core.data.Tag; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.Context; +import convex.core.lang.impl.AExceptional; +import convex.core.lang.impl.ErrorValue; +import convex.core.lang.impl.RecordFormat; + +/** + * Class representing the result of a Query or Transaction. + * + * A Result is typically used to communicate the outcome of a Query or a Transaction from a Peer to a Client. + * + * + */ +public final class Result extends ARecordGeneric { + + private static final RecordFormat RESULT_FORMAT=RecordFormat.of(Keywords.ID,Keywords.RESULT,Keywords.ERROR_CODE,Keywords.TRACE); + + private Result(AVector values) { + super(RESULT_FORMAT, values); + } + + private static Result create(AVector values) { + return new Result(values); + } + + /** + * Create a Result + * @param id ID of Result message + * @param value Result Value + * @param errorCode Error Code (may be null for success) + * @param trace Error Trace + * @return Result instance + */ + public static Result create(CVMLong id, ACell value, ACell errorCode, ACell trace) { + return create(Vectors.of(id,value,errorCode,trace)); + } + + /** + * Create a Result + * @param id ID of Result message + * @param value Result Value + * @param errorCode Error Code (may be null for success) + * @return Result instance + */ + public static Result create(CVMLong id, ACell value, ACell errorCode) { + return create(id,value,errorCode,null); + } + + /** + * Create a Result + * @param id ID of Result message + * @param value Result Value + * @return Result instance + */ + public static Result create(CVMLong id, ACell value) { + return create(id,value,null,null); + } + + /** + * Returns the message ID for this result. Message ID is an arbitrary ID assigned by a client requesting a transaction. + * + * @return ID from this result + */ + public ACell getID() { + return values.get(0); + } + + /** + * Returns the value for this result. The value is the result of transaction execution (may be an error message if the transaction failed) + * + * @param Type of Value + * @return ID from this result + */ + @SuppressWarnings("unchecked") + public T getValue() { + return (T)values.get(1); + } + + /** + * Returns the stack trace for this result. May be null + * + * @return ID from this result + */ + @SuppressWarnings("unchecked") + public AVector getTrace() { + return (AVector) values.get(3); + } + + /** + * Returns the Error Code from this Result. Normally this should be a Keyword. + * + * Will be null if no error occurred. + * + * @return ID from this result + */ + public ACell getErrorCode() { + return values.get(2); + } + + @Override + public AVector values() { + return values; + } + + @Override + protected ARecord withValues(AVector newValues) { + if (values==newValues) return this; + return new Result(newValues); + } + + @Override + public void validateCell() throws InvalidDataException { + super.validateCell(); + Object id=values.get(0); + if ((id!=null)&&!(id instanceof CVMLong)) { + throw new InvalidDataException("Result ID must be a CVM long value",this); + } + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.RESULT; + pos=values.encodeRaw(bs,pos); + return pos; + } + + /** + * Reads a Result from a ByteBuffer encoding. Assumes tag byte already read. + * + * @param bb ByteBuffer to read from + * @return The Result read + * @throws BadFormatException If a Result could not be read + */ + public static Result read(ByteBuffer bb) throws BadFormatException { + AVector v=Vectors.read(bb); + if (v.size()!=RESULT_FORMAT.count()) throw new BadFormatException("Invalid number of fields for Result!"); + + return create(v); + } + + /** + * Tests is the Result represents an Error + * @return True if error, false otherwise + */ + public boolean isError() { + return getErrorCode()!=null; + } + + /** + * Constructs a Result from a Context + * @param id Id for Result + * @param ctx Context + * @return New Result instance + */ + public static Result fromContext(CVMLong id,Context ctx) { + Object result=ctx.getValue(); + ACell errorCode=null; + ACell trace=null; + if (result instanceof AExceptional) { + AExceptional ex=(AExceptional)result; + result=ex.getMessage(); + errorCode=ex.getCode(); + if (ex instanceof ErrorValue) { + trace=Vectors.create(((ErrorValue)ex).getTrace()); + } + } + return create(id,(ACell)result,errorCode,trace); + } + + /** + * Updates result with a given message ID. Used to tag Results for return to Clients + * @param id New Result message ID + * @return Updated Result + */ + public Result withID(ACell id) { + return create(values.assoc(0, id)); + } + + @Override + public byte getTag() { + return Tag.RESULT; + } + + +} diff --git a/convex-core/src/main/java/convex/core/State.java b/convex-core/src/main/java/convex/core/State.java new file mode 100644 index 000000000..b6344d7e6 --- /dev/null +++ b/convex-core/src/main/java/convex/core/State.java @@ -0,0 +1,808 @@ +package convex.core; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import convex.core.data.ABlob; +import convex.core.data.ACell; +import convex.core.data.AMap; +import convex.core.data.ARecord; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.core.data.BlobMap; +import convex.core.data.BlobMaps; +import convex.core.data.Format; +import convex.core.data.Hash; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.LongBlob; +import convex.core.data.MapEntry; +import convex.core.data.PeerStatus; +import convex.core.data.Ref; +import convex.core.data.SignedData; +import convex.core.data.Strings; +import convex.core.data.Symbol; +import convex.core.data.Tag; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.BadSignatureException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.RT; +import convex.core.lang.Symbols; +import convex.core.lang.impl.RecordFormat; +import convex.core.transactions.ATransaction; +import convex.core.util.Counters; +import convex.core.util.Utils; + +/** + * Class representing the immutable state of the CVM + * + * State transitions are represented by blocks of transactions, according to the logic: s[n+1] = s[n].applyBlock(b[n]) + * + * State contains the following elements - Map of AccountStatus for every + * Address - Map of PeerStatus for every Peer Address - Global values - Schedule + * data structure + * + * "State. You're doing it wrong" - Rich Hickey + * + */ +public class State extends ARecord { + private static final Keyword[] STATE_KEYS = new Keyword[] { Keywords.ACCOUNTS, Keywords.PEERS, + Keywords.GLOBALS, Keywords.SCHEDULE }; + + private static final RecordFormat FORMAT = RecordFormat.of(STATE_KEYS); + + /** + * Symbols for Globals + */ + static final AVector GLOBAL_SYMBOLS=Vectors.of(Symbols.TIMESTAMP, Symbols.FEES, Symbols.JUICE_PRICE); + + // Indexes for globals in Globals Vector + static final int GLOBAL_TIMESTAMP=0; + static final int GLOBAL_FEES=1; + static final int GLOBAL_JUICE_PRICE=2; + + /** + * An empty State + */ + public static final State EMPTY = create(Vectors.empty(), BlobMaps.empty(), Constants.INITIAL_GLOBALS, + BlobMaps.empty()); + + private static final Logger log = LoggerFactory.getLogger(State.class.getName()); + + + // Note: we are embedding these directly in the State cell. + // TODO: check we aren't at risk of hitting max encoding size limits + + private final AVector accounts; + private final BlobMap peers; + private final AVector globals; + private final BlobMap> schedule; + + private State(AVector accounts, BlobMap peers, + AVector globals, BlobMap> schedule) { + super(FORMAT); + this.accounts = accounts; + this.peers = peers; + this.globals = globals; + this.schedule = schedule; + } + + @Override + public ACell get(ACell k) { + if (Keywords.ACCOUNTS.equals(k)) return accounts; + if (Keywords.PEERS.equals(k)) return peers; + if (Keywords.GLOBALS.equals(k)) return globals; + if (Keywords.SCHEDULE.equals(k)) return schedule; + return null; + } + + @Override + public int getRefCount() { + int rc=accounts.getRefCount(); + rc+=peers.getRefCount(); + rc+=globals.getRefCount(); + rc+=schedule.getRefCount(); + return rc; + } + + public Ref getRef(int i) { + if (i<0) throw new IndexOutOfBoundsException(i); + + { + int c=accounts.getRefCount(); + if (i accounts = (AVector) newVals[0]; + BlobMap peers = (BlobMap) newVals[1]; + AVector globals = (AVector) newVals[2]; + BlobMap> schedule = (BlobMap>) newVals[3]; + if ((this.accounts == accounts) && (this.peers == peers) && (this.globals == globals) + && (this.schedule == schedule)) { + return this; + } + return new State(accounts, peers, globals, schedule); + } + + /** + * Create a State + * @param accounts Accounts + * @param peers Peers + * @param globals Globals + * @param schedule Schedule (may be null) + * @return New State instance + */ + public static State create(AVector accounts, BlobMap peers, + AVector globals, BlobMap> schedule) { + return new State(accounts, peers, globals, schedule); + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=getTag(); + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = accounts.encode(bs,pos); + pos = peers.encode(bs,pos); + pos = globals.encode(bs,pos); + pos = schedule.encode(bs,pos); + return pos; + } + + @Override + public long getEncodingLength() { + long length=1; + length+=accounts.getEncodingLength(); + length+=peers.getEncodingLength(); + length+=globals.getEncodingLength(); + length+=schedule.getEncodingLength(); + return length; + } + + @Override + public int estimatedEncodingSize() { + int est=1; + est+=accounts.estimatedEncodingSize(); + est+=peers.estimatedEncodingSize(); + est+=globals.estimatedEncodingSize(); + est+=schedule.estimatedEncodingSize(); + return est; + } + + /** + * Reads a State from a ByteBuffer encoding. Assumes tag byte already read. + * + * @param bb ByteBuffer to decode from + * @return The State decoded + * @throws BadFormatException If a State could not be read + */ + public static State read(ByteBuffer bb) throws BadFormatException { + try { + AVector accounts = Format.read(bb); + BlobMap peers = Format.read(bb); + AVector globals = Format.read(bb); + BlobMap> schedule = Format.read(bb); + return create(accounts, peers, globals, schedule); + } catch (ClassCastException ex) { + throw new BadFormatException("Can't read state", ex); + } + } + + /** + * Get all Accounts in this State + * @return Vector of Accounts + */ + public AVector getAccounts() { + return accounts; + } + + /** + * Gets the map of Peers for this State + * + * @return A map of addresses to PeerStatus records + */ + public BlobMap getPeers() { + return peers; + } + + /** + * Gets the balance of a specific address, or null if the Address does not exist + * @param address Address to check + * @return Long balance, or null if Account does not exist + */ + public Long getBalance(Address address) { + AccountStatus acc = getAccount(address); + if (acc == null) return null; + return acc.getBalance(); + } + + public State withBalance(Address address, long newBalance) { + AccountStatus acc = getAccount(address); + if (acc == null) { + throw new Error("No account for " + address); + } else { + acc = acc.withBalance(newBalance); + } + return putAccount(address, acc); + } + + /** + * Block level state transition function + * + * Updates the state by applying a given block of transactions + * + * @param block Block to Apply + * @return The BlockResult from applying the given Block to this State + */ + public BlockResult applyBlock(Block block) { + Counters.applyBlock++; + State state = prepareBlock(block); + return state.applyTransactions(block); + } + + /** + * Apply state updates consistent with time advancing to a given timestamp + * @param b + * @return + */ + private State prepareBlock(Block b) { + State state = this; + AVector glbs = state.globals; + long ts=((CVMLong)glbs.get(0)).longValue(); + long bts = b.getTimeStamp(); + if (bts > ts) { + AVector newGlbs=glbs.assoc(0,CVMLong.create(bts)); + state = state.withGlobals(newGlbs); + } + + state = state.applyScheduledTransactions(b); + + return state; + } + + @SuppressWarnings("unchecked") + private State applyScheduledTransactions(Block b) { + long tcount = 0; + BlobMap> sched = this.schedule; + CVMLong timestamp = this.getTimeStamp(); + + // ArrayList to accumulate the transactions to apply. Null until we need it + ArrayList al = null; + + // walk schedule entries to determine how many there are + // and remove from the current schedule + // we can optimise bulk removal later + while (tcount < Constants.MAX_SCHEDULED_TRANSACTIONS_PER_BLOCK) { + if (sched.isEmpty()) break; + MapEntry> me = sched.entryAt(0); + ABlob key = me.getKey(); + long time = key.longValue(); + if (time > timestamp.longValue()) break; // exit if we are still in the future + AVector trans = me.getValue(); + long numScheduled = trans.count(); // number scheduled at this schedule timestamp + long take = Math.min(numScheduled, Constants.MAX_SCHEDULED_TRANSACTIONS_PER_BLOCK - tcount); + + // add scheduled transactions to arraylist + if (al == null) al = new ArrayList<>(); + for (long i = 0; i < take; i++) { + al.add(trans.get(i)); + } + // remove schedule entries taken. Delete key if no more entries remaining + trans = trans.subVector(take, numScheduled - take); + if (trans.isEmpty()) sched = sched.dissoc(key); + } + if (al==null) return this; // nothing to do if no transactions to execute + + // update state with amended schedule + State state = this.withSchedule(sched); + + // now apply the transactions! + int n = al.size(); + log.debug("Applying {} scheduled transactions",n); + for (int i = 0; i < n; i++) { + AVector st = (AVector) al.get(i); + Address origin = (Address) st.get(0); + AOp op = (AOp) st.get(1); + Context ctx; + try { + // TODO juice refund? + ctx = Context.createInitial(state, origin, Constants.MAX_TRANSACTION_JUICE); + ctx = ctx.run(op); + if (ctx.isExceptional()) { + // TODO: what to do here? probably ignore + // we maybe need to think about reporting scheduled results? + log.trace("Scheduled transaction error: {}", ctx.getExceptional()); + } else { + state = ctx.getState(); + log.trace("Scheduled transaction succeeded"); + } + } catch (Exception e) { + log.trace("Scheduled transaction failed: {}",e); + e.printStackTrace(); + } + + } + + return state; + } + + private State withSchedule(BlobMap> newSchedule) { + if (schedule == newSchedule) return this; + return new State(accounts, peers, globals, newSchedule); + } + + private State withGlobals(AVector newGlobals) { + if (newGlobals == globals) return this; + return new State(accounts, peers, newGlobals, schedule); + } + + private BlockResult applyTransactions(Block block) { + State state = this; + int blockLength = block.length(); + Result[] results = new Result[blockLength]; + + AVector> transactions = block.getTransactions(); + for (int i = 0; i < blockLength; i++) { + // SECURITY: catch-all exception handler. + try { + // extract the signed transaction from the block + SignedData signed = transactions.get(i); + + // execute the transaction using the *latest* state (not necessarily "this") + Context ctx = state.applyTransaction(signed); + + // record results and state update + results[i] = Result.fromContext(CVMLong.create(i),ctx); + state = ctx.getState(); + } catch (Throwable t) { + String msg= "Unexpected fatal exception applying transaction: "+t.toString(); + results[i] = Result.create(CVMLong.create(i), Strings.create(msg),ErrorCodes.UNEXPECTED); + t.printStackTrace(); + log.error(msg); + } + } + + // TODO: changes for complete block? + return BlockResult.create(state, results); + } + + + /** + * Applies a signed transaction to the State. + * + * SECURITY: Checks digital signature and correctness of account key + * + * @return Context containing the updated chain State (may be exceptional) + */ + private Context applyTransaction(SignedData signedTransaction) throws BadSignatureException { + // Extract transaction, performs signature check + ATransaction t=signedTransaction.getValue(); + Address addr=t.getAddress(); + AccountStatus as = getAccount(addr); + if (as==null) { + return Context.createFake(this).withError(ErrorCodes.NOBODY,"Transaction for non-existent Account: "+addr); + } else { + AccountKey key=as.getAccountKey(); + if (key==null) return Context.createFake(this).withError(ErrorCodes.NOBODY,"Transaction for account that is an Actor: "+addr); + if (!Utils.equals(key, signedTransaction.getAccountKey())) { + return Context.createFake(this).withError(ErrorCodes.SIGNATURE,"Signature not valid for Account: "+addr+" expected public key: "+key); + } + } + + Context ctx=applyTransaction(t); + return ctx; + } + + /** + * Applies a transaction to the State. + * + * There are three phases in application of a transaction: + *
    + *
  1. Preparation for accounting, with {@link #prepareTransaction(Address, ATransaction) prepareTransaction}
  2. + *
  3. Functional application of the transaction with ATransaction.apply(....)
  4. + *
  5. Completion of accounting, with completeTransaction
  6. + *
+ * + * SECURITY: Assumes digital signature already checked. + * + * @param Type of transaction result + * @param t Transaction to apply + * @return Context containing the updated chain State (may be exceptional) + */ + public Context applyTransaction(ATransaction t) { + Address origin = t.getAddress(); + + try { + // Create prepared context (juice subtracted, sequence updated, transaction entry checks) + Context ctx = prepareTransaction(origin,t); + if (ctx.isExceptional()) { + // We hit some error while preparing transaction. Return context with no state change, + // i.e. before executing the transaction + return ctx; + } + + final long totalJuice = ctx.getJuice(); + + State preparedState=ctx.getState(); + + + // apply transaction. This may result in an error! + ctx = t.apply(ctx); + + // complete transaction + // NOTE: completeTransaction handles error cases as well + ctx = ctx.completeTransaction(preparedState, totalJuice); + + return ctx; + } catch (Throwable ex) { + // SECURITY: This should never happen! + // But catching right now to prevent CVM overall crash + StringWriter s=new StringWriter(); + ex.printStackTrace(new PrintWriter(s)); + String message=s.toString(); + Context fCtx=Context.createInitial(this, origin, 0); + fCtx=fCtx.withError(ErrorCodes.FATAL, message); + return fCtx; + } + } + + @SuppressWarnings("unchecked") + private Context prepareTransaction(Address origin,ATransaction t) { + // Pre-transaction state updates (persisted even if transaction fails) + AccountStatus account = getAccount(origin); + if (account == null) { + return (Context) Context.createFake(this).withError(ErrorCodes.NOBODY); + } + + // Update sequence number for target account + long sequence=t.getSequence(); + AccountStatus newAccount = account.updateSequence(sequence); + if (newAccount == null) { + return Context.createFake(this,origin).withError(ErrorCodes.SEQUENCE, "Received = "+sequence+" & Expected = "+(account.getSequence()+1)); + } + State preparedState = this.putAccount(origin, newAccount); + + // Create context with juice subtracted + Long maxJuice=t.getMaxJuice(); + long juiceLimit=Math.min(Constants.MAX_TRANSACTION_JUICE,(maxJuice==null)?account.getBalance():maxJuice); + Context ctx = Context.createInitial(preparedState, origin, juiceLimit); + return ctx; + } + + @Override + public boolean isCanonical() { + return true; + } + + /** + * Computes the weighted stake for each peer. Adds a single entry for the null + * key, containing the total stake + * + * @return Map of Stakes + */ + public HashMap computeStakes() { + HashMap hm = new HashMap<>(peers.size()); + Double totalStake = peers.reduceEntries((acc, e) -> { + double stake = (double) (e.getValue().getTotalStake()); + + // TODO: potential performance bottleneck from hashing? + hm.put(RT.ensureAccountKey(e.getKey()), stake); + return stake + acc; + }, 0.0); + hm.put(null, totalStake); + return hm; + } + + /** + * Updates the Accounts in this State + * @param newAccounts New Accounts vector + * @return Updated State + */ + public State withAccounts(AVector newAccounts) { + if (newAccounts == accounts) return this; + return create(newAccounts, peers,globals, schedule); + } + + /** + * Returns this state after updating the given account + * + * @param address Address of Account to update + * @param accountStatus New Account Status + * @return Updates State, or this state if Account was unchanged + */ + public State putAccount(Address address, AccountStatus accountStatus) { + long ix=address.longValue(); + long n=accounts.count(); + if (ix>n) { + throw new IndexOutOfBoundsException("Trying to add an account beyond accounts array at position: "+ix); + } + + AVector newAccounts; + if (ix==n) { + // adding a new account in next position + newAccounts=accounts.conj(accountStatus); + } else { + newAccounts = accounts.assoc(ix, accountStatus); + } + + return withAccounts(newAccounts); + } + + /** + * Gets the AccountStatus for a given account, or null if not found. + * + * @param target Address to look up. Must not be null + * @return The AccountStatus for the given account, or null. + */ + public AccountStatus getAccount(Address target) { + long ix=target.longValue(); + if ((ix<0)||(ix>=accounts.count())) return null; + return accounts.get(ix); + } + + /** + * Gets the environment for a given account, or null if not found. + * + * @param addr Address of account to obtain + * @return The environment of the given account, or null if not found. + */ + public AMap getEnvironment(Address addr) { + AccountStatus as = getAccount(addr); + if (as == null) return null; + return as.getEnvironment(); + } + + /** + * Updates the Peers in this State + * @param newPeers New Peer Map + * @return Updated State + */ + public State withPeers(BlobMap newPeers) { + if (peers == newPeers) return this; + return create(accounts, newPeers, globals, schedule); + } + + @Override + public byte getTag() { + return Tag.STATE; + } + + /** + * Deploys a new Actor in the current state. + * + * Returns the updated state. The actor will be the last Account. + * + * @return The updated state with the Actor deployed. + */ + public State tryAddActor() { + AccountStatus as = AccountStatus.createActor(); + AVector newAccounts = accounts.conj(as); + return withAccounts(newAccounts); + } + + /** + * Compute the total funds existing within this state. + * + * Should be constant! 1,000,000,000,000,000,000 in full deployment mode + * + * @return The total value of all funds + */ + public long computeTotalFunds() { + long total = accounts.reduce((Long acc,AccountStatus as) -> acc + as.getBalance(), (Long)0L); + total += peers.reduceValues((Long acc, PeerStatus ps) -> acc + ps.getTotalStake(), 0L); + total += getGlobalFees().longValue(); + return total; + } + + @Override + public void validate() throws InvalidDataException { + super.validate(); + } + + @Override + public void validateCell() throws InvalidDataException { + accounts.validateCell(); + peers.validateCell(); + globals.validateCell(); + schedule.validateCell(); + } + + /** + * Gets the current global timestamp from this state. + * + * @return The timestamp from this state. + */ + public CVMLong getTimeStamp() { + return (CVMLong) globals.get(GLOBAL_TIMESTAMP); + } + + /** + * Gets the current Juice price + * + * @return Juice Price + */ + public CVMLong getJuicePrice() { + return (CVMLong) globals.get(GLOBAL_JUICE_PRICE); + } + + /** + * Schedules an operation with the given timestamp and Op in this state + * + * @param time Timestamp at which to execute the scheduled op + * + * @param address AccountAddress to schedule op for + * @param op Op to execute in schedule + * @return The updated State + */ + public State scheduleOp(long time, Address address, AOp op) { + AVector v = Vectors.of(address, op); + + LongBlob key = LongBlob.create(time); + AVector list = schedule.get(key); + if (list == null) { + list = Vectors.of(v); + } else { + list = list.append(v); + } + BlobMap> newSchedule = schedule.assoc(key, list); + + return this.withSchedule(newSchedule); + } + + /** + * Gets the current schedule data structure for this state + * + * @return The schedule data structure. + */ + public BlobMap> getSchedule() { + return schedule; + } + + /** + * Gets the Global Fees accumulated in the State + * @return Global Fees + */ + public CVMLong getGlobalFees() { + return (CVMLong) globals.get(GLOBAL_FEES); + } + + /** + * Update Global Fees + * @param newFees New Fees + * @return Updated State + */ + public State withGlobalFees(CVMLong newFees) { + return withGlobals(globals.assoc(GLOBAL_FEES,newFees)); + } + + + /** + * Gets the PeerStatus record for the given Address, or null if it does not + * exist + * + * @param peerAddress Address of Peer to check + * @return PeerStatus + */ + public PeerStatus getPeer(AccountKey peerAddress) { + return getPeers().get(peerAddress); + } + + /** + * Updates the specified peer status + * + * @param peerKey Peer Key + * @param updatedPeer New Peer Status + * @return Updated state + */ + public State withPeer(AccountKey peerKey, PeerStatus updatedPeer) { + return withPeers(peers.assoc(peerKey, updatedPeer)); + } + + /** + * Gets the next available address for allocation, i.e. the lowest Address + * that does not yet exist in this State. + * + * @return Next address available + */ + public Address nextAddress() { + return Address.create(accounts.count()); + } + + /** + * Look up an Address from CNS + * @param name CNS name String + * @return Address from CNS, or null if not found + */ + public Address lookupCNS(String name) { + Context ctx=Context.createFake(this); + return (Address) ctx.lookupCNS(name).getResult(); + } + + /** + * Gets globals. + * + * @return Vector of global values + */ + public AVector getGlobals() { + return globals; + } + + /** + * Updates the State with a new timestamp + * @param timestamp New timestamp + * @return Updated State + */ + public State withTimestamp(long timestamp) { + return withGlobals(globals.assoc(GLOBAL_TIMESTAMP, CVMLong.create(timestamp))); + } + + @Override + public boolean equals(AMap a) { + if (this == a) return true; // important optimisation for e.g. hashmap equality + if (a == null) return false; + if (a.getTag()!=getTag()) return false; + State as=(State)a; + return equals(as); + } + + /** + * Tests if this State is equal to another + * @param a State to compare with + * @return true if equal, false otherwise + */ + public boolean equals(State a) { + if (a == null) return false; + Hash h=this.cachedHash(); + if (h!=null) { + Hash ha=a.cachedHash(); + if (ha!=null) return Utils.equals(h, ha); + } + + if (!(Utils.equals(accounts, a.accounts))) return false; + if (!(Utils.equals(globals, a.globals))) return false; + if (!(Utils.equals(peers, a.peers))) return false; + if (!(Utils.equals(schedule, a.schedule))) return false; + return true; + } + +} diff --git a/convex-core/src/main/java/convex/core/crypto/AKeyPair.java b/convex-core/src/main/java/convex/core/crypto/AKeyPair.java new file mode 100644 index 000000000..8e0777c5a --- /dev/null +++ b/convex-core/src/main/java/convex/core/crypto/AKeyPair.java @@ -0,0 +1,117 @@ +package convex.core.crypto; + +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; + +import convex.core.data.ACell; +import convex.core.data.AccountKey; +import convex.core.data.Blob; +import convex.core.data.Hash; +import convex.core.data.SignedData; + +/** + * Abstract base class for key pairs in Convex. + * + * Intended as a lightweight container for underlying crypto primitives. + */ +public abstract class AKeyPair { + + /** + * Gets the Account Public Key of this KeyPair + * @return AccountKey for this KeyPair + */ + public abstract AccountKey getAccountKey(); + + /** + * Gets the Private key encoded as a Blob + * @return Blob Private key data encoding + */ + public abstract Blob getEncodedPrivateKey(); + + /** + * Signs a value with this key pair + * @param Type of Value + * @param value Value to sign. Can be any valid CVM value. + * @return Signed Data Object + */ + public abstract SignedData signData(R value); + + @Override + public abstract boolean equals(Object a); + + /** + * Signs a hash value with this key pair, producing a signature of the appropriate type. + * @param hash Hash of value to sign + * @return A Signature compatible with the key pair. + */ + public abstract ASignature sign(Hash hash); + + /** + * Create a deterministic key pair with the given seed. + * + * SECURITY: Never use this for valuable keys or real assets: intended for deterministic testing only. + * @param seed Any long value. The same seed will produce the same key pair. + * @return New key pair + */ + public static AKeyPair createSeeded(long seed) { + return Ed25519KeyPair.createSeeded(seed); + } + + /** + * Create a key pair with the given Address and encoded private key + * + * @param publicKey Public Key + * @param encodedPrivateKey Encoded private key + * @return New key pair + */ + public static AKeyPair create(AccountKey publicKey, Blob encodedPrivateKey) { + return Ed25519KeyPair.create(publicKey,encodedPrivateKey); + } + + static { + Providers.init(); + } + + /** + * Generates a new, secure random key pair. Uses a Java SecureRandom instance. + * + * @return New Key Pair instance. + */ + public static AKeyPair generate() { + return Ed25519KeyPair.generate(); + } + + /** + * Creates a key pair using specific key material. + * + * @param keyMaterial Bytes to use as key + * @return New key pair + */ + public static AKeyPair create(byte[] keyMaterial) { + return Ed25519KeyPair.create(keyMaterial); + } + + /** + * Gets the JCA PrivateKey + * @return Private Key + */ + public abstract PrivateKey getPrivate(); + + /** + * Gets the JCA PublicKey + * @return Public Key + */ + public abstract PublicKey getPublic(); + + @Override + public String toString() { + return getAccountKey()+":"+getEncodedPrivateKey(); + } + + /** + * Gets the JCA representation of this Key Pair + * @return JCA KepPair + */ + public abstract KeyPair getJCAKeyPair(); +} diff --git a/convex-core/src/main/java/convex/core/crypto/ASignature.java b/convex-core/src/main/java/convex/core/crypto/ASignature.java new file mode 100644 index 000000000..6247c727b --- /dev/null +++ b/convex-core/src/main/java/convex/core/crypto/ASignature.java @@ -0,0 +1,90 @@ +package convex.core.crypto; + +import java.nio.ByteBuffer; + +import convex.core.data.ABlob; +import convex.core.data.ACell; +import convex.core.data.AccountKey; +import convex.core.data.Blob; +import convex.core.data.Tag; +import convex.core.exceptions.BadFormatException; +import convex.core.util.Utils; + +/** + * Class representing a cryptographic signature + */ +public abstract class ASignature extends ACell { + + /** + * Checks if the signature is valid for a given message hash + * @param message Message to verify + * @param publicKey Public key of signer + * @return True if signature is valid, false otherwise + */ + public abstract boolean verify(ABlob message, AccountKey publicKey); + + /** + * Reads a Signature from the given ByteBuffer. Assumes tag byte already read. + * + * Uses Ed25519 + * + * @param bb ByteBuffer to read from + * @return Signature instance + * @throws BadFormatException If encoding is invalid + */ + public static ASignature read(ByteBuffer bb) throws BadFormatException { + return Ed25519Signature.read(bb); + } + + /** + * Gets the content of this Signature as a hex string + * @return Hex String representation of Signature + */ + public abstract String toHexString(); + + /** + * Construct a Signature from a hex string + * + * Uses Ed25519 + * + * @param hex Hex String to read from + * @return Signature instance + */ + public static ASignature fromHex(String hex) { + byte[] bs=Utils.hexToBytes(hex); + return Ed25519Signature.wrap(bs); + } + + /** + * Construct a Signature from a Blob + * + * Uses Ed25519 + * + * @param sigData Blob of data representing raw signature + * @return Signature instance + */ + public static ASignature fromBlob(Blob sigData) { + byte[] bs=sigData.getBytes(); + return Ed25519Signature.wrap(bs); + } + + @Override + public boolean isEmbedded() { + return true; + } + + @Override + public byte getTag() { + return Tag.SIGNATURE; + } + + /** + * Gets a Blob containing the raw bytes of this digital signature + * + * @return Blob containing signature bytes + */ + protected abstract ABlob getSignatureBlob(); + + + +} diff --git a/convex-core/src/main/java/convex/core/crypto/Ed25519KeyPair.java b/convex-core/src/main/java/convex/core/crypto/Ed25519KeyPair.java new file mode 100644 index 000000000..111f042f0 --- /dev/null +++ b/convex-core/src/main/java/convex/core/crypto/Ed25519KeyPair.java @@ -0,0 +1,336 @@ +package convex.core.crypto; + +import java.io.IOException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.edec.EdECObjectIdentifiers; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters; +import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; + +import convex.core.data.ACell; +import convex.core.data.AccountKey; +import convex.core.data.Blob; +import convex.core.data.Hash; +import convex.core.data.SignedData; +import convex.core.util.Utils; + +/** + * Class representing an Ed25519 Key Pair + */ +public class Ed25519KeyPair extends AKeyPair { + private static final int SECRET_LENGTH=64; + private static final int SEED_LENGTH=32; + + private final AccountKey publicKey; + private KeyPair keyPair=null; + private final Blob seed; + + private final byte[] secretKeyBytes; + + private static final String ED25519 = "Ed25519"; + + private Ed25519KeyPair(AccountKey pk, Blob seed, byte[] skBytes) { + this.publicKey=pk; + this.seed=seed; + this.secretKeyBytes=skBytes; + } + + public static Ed25519KeyPair create(Blob seed) { + if (seed.count() != SEED_LENGTH) throw new IllegalArgumentException("256 bit private key material expected as seed!"); + + byte[] secretKeyBytes=new byte[SECRET_LENGTH]; + byte[] pkBytes=new byte[AccountKey.LENGTH]; + Providers.SODIUM_SIGN.cryptoSignSeedKeypair(pkBytes, secretKeyBytes, seed.getBytes()); + AccountKey publicKey=AccountKey.wrap(pkBytes); + return new Ed25519KeyPair(publicKey,seed,secretKeyBytes); + } + + /** + * Generates a new, secure random key pair. Uses a Java SecureRandom instance. + * + * @return New Key Pair instance. + */ + public static Ed25519KeyPair generate() { + return generate(new SecureRandom()); + } + + /** + * Create a KeyPair from a JCA KeyPair + * @param keyPair JCA KeyPair + * @return AKeyPair instance + */ + protected static Ed25519KeyPair create(KeyPair keyPair) { + Blob seed=extractSeed(keyPair.getPrivate()); + return create(seed); + } + + private static Blob extractSeed(PrivateKey private1) { + byte[] data=private1.getEncoded(); + int n=data.length; + Blob seed=Blob.wrap(data,n-SEED_LENGTH,SEED_LENGTH); + return seed; + } + + /** + * Creates an Ed25519 Key Pair with the specified keys + * @param publicKey Public key + * @param privateKey Private key + * @return Key Pair instance + */ + public static Ed25519KeyPair create(PublicKey publicKey, PrivateKey privateKey) { + KeyPair keyPair=new KeyPair(publicKey,privateKey); + return create(keyPair); + } + + /** + * Create a key pair given a public AccountKey and a encoded Blob + * @param accountKey Public Key + * @param encodedPrivateKey Encoded PKCS8 Private key + * @return AKeyPair instance + */ + public static Ed25519KeyPair create(AccountKey accountKey, Blob encodedPrivateKey) { + PublicKey publicKey= publicKeyFromBytes(accountKey.getBytes()); + PrivateKey privateKey=privateKeyFromBlob(encodedPrivateKey); + return create(publicKey,privateKey); + } + + public Blob getSeed() { + return seed; + } + + /** + * Generates a secure random key pair + * @param random A secure random instance + * @return New key pair + */ + public static Ed25519KeyPair generate(SecureRandom random) { + Blob seed=Blob.createRandom(random, 32); + return create(seed); + } + + /** + * Create a deterministic key pair with a specified seed. + * + * SECURITY: Use for testing purpose only + * @param seed See to use for generation + * @return Key Pair instance + */ + public static Ed25519KeyPair createSeeded(long seed) { + SecureRandom r = new InsecureRandom(seed); + Blob seedBlob=Blob.createRandom(r, 32); + return create(seedBlob); + } + + /** + * Create a SignKeyPair from given private key material. Public key is generated + * automatically from the private key + * + * @param keyMaterial An array of 32 bytes of random material to use for private key + * @return A new key pair using the given private key + */ + public static Ed25519KeyPair create(byte[] keyMaterial) { + return create(Blob.create(keyMaterial)); + } + + /** + * Create a KeyPair from given private key. Public key is generated + * automatically from the private key + * + * @param privateKey An PrivateKey item for private key + * @return A new key pair using the given private key + */ + public static Ed25519KeyPair create(PrivateKey privateKey) { + Ed25519PrivateKeyParameters privateKeyParam = new Ed25519PrivateKeyParameters(privateKey.getEncoded(), 16); + Ed25519PublicKeyParameters publicKeyParam = privateKeyParam.generatePublicKey(); + PublicKey generatedPublicKey = publicKeyFromBytes(publicKeyParam.getEncoded()); + // PrivateKey generatedPrivateKey = privateFromBytes(privateKeyParam.getEncoded()); + return create(generatedPublicKey, privateKey); + } + + /** + * Extracts an Address from an Ed25519 public key + * @param publicKey Public key + * @return + */ + static AccountKey extractAccountKey(PublicKey publicKey) { + byte[] bytes=publicKey.getEncoded(); + int n=bytes.length; + // take the bytes at the end of the encoding + return AccountKey.wrap(bytes,n-AccountKey.LENGTH); + } + + /** + * Gets a Ed25519 Private Key from a 32-byte array. + * @param privKey + * @return + */ + static PrivateKey privateFromBytes(byte[] privKey) { + try { + KeyFactory keyFactory = KeyFactory.getInstance(ED25519); + PrivateKeyInfo privKeyInfo = new PrivateKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), new DEROctetString(privKey)); + + var pkcs8KeySpec = new PKCS8EncodedKeySpec(privKeyInfo.getEncoded()); + + PrivateKey result = keyFactory.generatePrivate(pkcs8KeySpec); + return result; + } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) { + throw Utils.sneakyThrow(e); + } + } + + @Override + public Blob getEncodedPrivateKey() { + return extractPrivateKey(getPrivate()); + } + + /** + * Extracts an Blob containing the private key data from an Ed25519 private key + * + * SECURITY: Be careful with this Blob! + * + * @param publicKey Public key + * @return + */ + static Blob extractPrivateKey(PrivateKey privateKey) { + byte[] bytes=privateKey.getEncoded(); + return Blob.wrap(bytes); + } + + /** + * Gets a byte array representation of the public key + * @return Bytes of public key + */ + public byte[] getPublicKeyBytes() { + return getAccountKey().getBytes(); + } + + private static PrivateKey privateKeyFromBlob(Blob encodedKey) { + try { + KeyFactory keyFactory = KeyFactory.getInstance(ED25519); + PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(encodedKey.getBytes()); + PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec); + return privateKey; + } catch (Exception e) { + throw Utils.sneakyThrow(e); + } + } + + /** + * Creates a private key using the given raw bytes. + * @param key 32 bytes private key data + * @return Ed25519 Private Key instance + */ + public static PrivateKey privateKeyFromBytes(byte[] key) { + try { + KeyFactory keyFactory = KeyFactory.getInstance(ED25519); + PrivateKeyInfo privKeyInfo = new PrivateKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), + new DEROctetString(key)); + PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(privKeyInfo.getEncoded()); + PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec); + return privateKey; + } catch (Exception e) { + throw new Error(e); + } + } + + static PublicKey publicKeyFromBytes(byte[] key) { + try { + KeyFactory keyFactory = KeyFactory.getInstance(ED25519); + SubjectPublicKeyInfo pubKeyInfo = new SubjectPublicKeyInfo( + new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), key); + X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pubKeyInfo.getEncoded()); + PublicKey publicKey = keyFactory.generatePublic(x509KeySpec); + return publicKey; + } catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) { + throw new Error(e); + } + } + + @Override + public PublicKey getPublic() { + return getJCAKeyPair().getPublic(); + } + + @Override + public KeyPair getJCAKeyPair() { + if (keyPair==null) { + PublicKey pub=publicKeyFromBytes(publicKey.getBytes()); + PrivateKey priv=privateKeyFromBytes(seed.getBytes()); + keyPair=new KeyPair(pub,priv); + } + return keyPair; + } + + @Override + public PrivateKey getPrivate() { + return getJCAKeyPair().getPrivate(); + } + + @Override + public AccountKey getAccountKey() { + return publicKey; + } + + + @Override + public SignedData signData(R value) { + return SignedData.create(this, value); + } + + @Override + public ASignature sign(Hash hash) { + byte[] signature=new byte[Ed25519Signature.SIGNATURE_LENGTH]; + if (Providers.SODIUM_SIGN.cryptoSignDetached( + signature, + hash.getBytes(), + Hash.LENGTH, + getSecretKeyBytes())) {; + return Ed25519Signature.wrap(signature); + } else { + throw new Error("Signing failed!"); + } + +// try { +// Signature signer = Signature.getInstance(ED25519); +// signer.initSign(getPrivate()); +// signer.update(hash.getInternalArray(), hash.getInternalOffset(), Hash.LENGTH); +// byte[] signature = signer.sign(); +// return Ed25519Signature.wrap(signature); +// } catch (GeneralSecurityException e) { +// throw new Error(e); +// } + } + + /** + * Secret key bytes for LazySodium + * @return Private key byte array + */ + byte[] getSecretKeyBytes() { + return secretKeyBytes; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Ed25519KeyPair)) return false; + return equals((Ed25519KeyPair) o); + } + + boolean equals(Ed25519KeyPair other) { + if (!this.seed.equals(other.seed)) return false; + if (!this.publicKey.equals(other.publicKey)) return false; + return true; + } + +} diff --git a/convex-core/src/main/java/convex/core/crypto/Ed25519Signature.java b/convex-core/src/main/java/convex/core/crypto/Ed25519Signature.java new file mode 100644 index 000000000..a39234d34 --- /dev/null +++ b/convex-core/src/main/java/convex/core/crypto/Ed25519Signature.java @@ -0,0 +1,141 @@ +package convex.core.crypto; + +import java.nio.ByteBuffer; + +import convex.core.data.ABlob; +import convex.core.data.ACell; +import convex.core.data.AccountKey; +import convex.core.data.Blob; +import convex.core.data.Tag; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.util.Utils; + +/** + * Immutable dtata value class representing an Ed25519 digital signature. + */ +public class Ed25519Signature extends ASignature { + + /** + * Length in bytes of an Ed25519 signature + */ + public static final int SIGNATURE_LENGTH = 64; + + /** + * A Signature containing zerod bytes (not valid) + */ + public static final ASignature ZERO = wrap(new byte[SIGNATURE_LENGTH]); + + private final byte[] signatureBytes; + + private Ed25519Signature(byte[] signature) { + this.signatureBytes=signature; + } + + /** + * Creates a Signature instance with specific bytes + * @param signature Bytes for signature + * @return Signature instance + */ + public static Ed25519Signature wrap(byte[] signature) { + if (signature.length!=SIGNATURE_LENGTH) throw new IllegalArgumentException("Bsd signature length for ED25519"); + return new Ed25519Signature(signature); + } + + @Override + public boolean isCanonical() { + return true; + } + + @Override + public ACell toCanonical() { + return this; + } + + @Override public final boolean isCVMValue() { + return false; + } + + /** + * Read a signature from a ByteBuffer. Assumes tag already read. + * @param bb ByteBuffer to read from + * @return Signature instance + * @throws BadFormatException If encoding is invalid + */ + public static Ed25519Signature read(ByteBuffer bb) throws BadFormatException { + byte[] sigData=new byte[SIGNATURE_LENGTH]; + bb.get(sigData); + return wrap(sigData); + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.SIGNATURE; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + System.arraycopy(signatureBytes, 0, bs, pos, SIGNATURE_LENGTH); + return pos+SIGNATURE_LENGTH; + } + + @Override + public void print(StringBuilder sb) { + sb.append("{:signature 0x"+Utils.toHexString(signatureBytes)+"}"); + } + + //@Override + //public boolean verify(Hash hash, AccountKey address) { + // PublicKey pk=Ed25519KeyPair.publicKeyFromBytes(address.getBytes()); + // return verify(hash,pk); + //} + + @Override + public boolean verify(ABlob message, AccountKey address) { + boolean verified = Providers.SODIUM_SIGN.cryptoSignVerifyDetached(signatureBytes, message.getBytes(), (int)message.count(), address.getBytes()); + return verified; + } + +// private boolean verify(Hash hash, PublicKey publicKey) { +// try { +// Signature verifier = Signature.getInstance("Ed25519"); +// verifier.initVerify(publicKey); +// verifier.update(hash.getInternalArray(),hash.getOffset(),Hash.LENGTH); +// return verifier.verify(signatureBytes); +// } catch (SignatureException | InvalidKeyException e) { +// return false; +// } catch (NoSuchAlgorithmException e) { +// throw new Error(e); +// } +// } + + @Override + public void validateCell() throws InvalidDataException { + // TODO Auto-generated method stub + + } + + @Override + public int estimatedEncodingSize() { + return 1+SIGNATURE_LENGTH; + } + + @Override + public int getRefCount() { + return 0; + } + + @Override + public String toHexString() { + return Utils.toHexString(signatureBytes); + } + + @Override + public Blob getSignatureBlob() { + return Blob.wrap(signatureBytes); + } + + + +} diff --git a/convex-core/src/main/java/convex/core/crypto/Encoding.java b/convex-core/src/main/java/convex/core/crypto/Encoding.java new file mode 100644 index 000000000..0dd2d20d1 --- /dev/null +++ b/convex-core/src/main/java/convex/core/crypto/Encoding.java @@ -0,0 +1,12 @@ +package convex.core.crypto; + +/** + * Class for crypto encoding constants + */ +public class Encoding { + + /** + * Format for private keys + */ + public static final String PRIVATE_KEY_FORMAT="PKCS#8"; +} diff --git a/convex-core/src/main/java/convex/core/crypto/Hashing.java b/convex-core/src/main/java/convex/core/crypto/Hashing.java new file mode 100644 index 000000000..5b46c6b14 --- /dev/null +++ b/convex-core/src/main/java/convex/core/crypto/Hashing.java @@ -0,0 +1,143 @@ +package convex.core.crypto; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.jcajce.provider.digest.Keccak; + +import convex.core.data.Hash; + +/** + * Class for static Hashing functionality + */ +public class Hashing { + + /** + * Computes the SHA-256 hash of a string + * + * @param message Message to hash (in UTF-8 encoding) + * @return Hash of UTF-8 encoded string + */ + public static Hash sha3(String message) { + return sha3(message.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Computes the SHA3-256 hash of byte data + * + * @param data Byte array to hash + * @return SHA3-256 Hash value + */ + public static Hash sha3(byte[] data) { + MessageDigest md = getSHA3Digest(); + byte[] hash = md.digest(data); + return Hash.wrap(hash); + } + + /** + * Gets a thread-local instance of a SHA3-256 MessageDigest + * + * @return MessageDigest instance + */ + public static MessageDigest getSHA3Digest() { + return sha3Store.get(); + } + + /** + * Gets the Convex default MessageDigest. + * + * Guaranteed thread safe, will be either a new or ThreadLocal instance. + * + * @return MessageDigest + */ + public static MessageDigest getDigest() { + return getSHA3Digest(); + } + + /** + * Gets a MessageDigest for Keccak256. + * + * Guaranteed thread safe, will be either a new or ThreadLocal instance. + * + * @return MessageDigest + */ + public static MessageDigest getKeccak256Digest() { + // MessageDigest md= KECCAK_DIGEST.get(); + // md.reset(); + MessageDigest md = new Keccak.Digest256(); + return md; + } + + /** + * Gets a thread-local instance of a SHA256 MessageDigest + * + * @return MessageDigest instance + */ + public static MessageDigest getSHA256Digest() { + return sha256Store.get(); + } + + /** + * Computes the SHA3-256 hash of byte data + * + * @param data Byte array to hash + * @return SHA3-256 Hash value + */ + public static Hash sha256(byte[] data) { + MessageDigest md = getSHA256Digest(); + byte[] hash = md.digest(data); + return Hash.wrap(hash); + } + + /** + * Computes the SHA-256 hash of a string + * + * @param message Message to Hash (in UTF8 encoding) + * @return Hash of UTF-8 encoded string + */ + public static Hash sha256(String message) { + return sha256(message.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Private store for thread-local MessageDigent objects. Avoids cost of + * recreating these every time they are needed. + */ + private static final ThreadLocal sha256Store; + /** + * Private store for thread-local MessageDigent objects. Avoids cost of + * recreating these every time they are needed. + */ + private static final ThreadLocal sha3Store; + static { + sha256Store = ThreadLocal.withInitial(() -> { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new Error("SHA-256 algorithm not available", e); + } + }); + + sha3Store = ThreadLocal.withInitial(() -> { + try { + return MessageDigest.getInstance("SHA3-256"); + } catch (NoSuchAlgorithmException e) { + throw new Error("SHA3-256 algorithm not available", e); + } + }); + } + /** + * Threadlocal store for MessageDigets instances. TODO: figure out if this is + * useful for performance. Probably not since digest initialisation is the + * bottleneck anyway? + */ + @SuppressWarnings("unused") + private static final ThreadLocal KECCAK_DIGEST = new ThreadLocal() { + @Override + protected MessageDigest initialValue() { + return new Keccak.Digest256(); + } + }; + +} diff --git a/convex-core/src/main/java/convex/core/crypto/InsecureRandom.java b/convex-core/src/main/java/convex/core/crypto/InsecureRandom.java new file mode 100644 index 000000000..5281cb816 --- /dev/null +++ b/convex-core/src/main/java/convex/core/crypto/InsecureRandom.java @@ -0,0 +1,83 @@ +package convex.core.crypto; + +import java.security.Provider; +import java.security.SecureRandom; +import java.security.SecureRandomSpi; +import java.util.Arrays; + +/** + * A SecureRandom instance that returns deterministic values given an initial seed. + * + * SECURITY: Neither secure nor truly random, but useful for testing. Please don't use for protecting real assets.... + * + */ +@SuppressWarnings("serial") +public class InsecureRandom extends SecureRandom { + + /** + * Create an InsecureRandom instance with a specified seed + * @param seed Seed value to use + */ + public InsecureRandom(long seed) { + super(new InsecureRandomSpi(seed), SECURITY_PROVIDER); + } + + private static final Provider SECURITY_PROVIDER = new InsecureRandomProvider(); + + /** + * Security provider instance used to register this random provider. + */ + private static class InsecureRandomProvider extends Provider { + private InsecureRandomProvider() { + super("InsecureRandom","0.01","Random number generator with deterministic values"); + } + } + + /** + * Actual work done with a SPI that extends java.security.SecureRandomSpi + */ + private static class InsecureRandomSpi extends SecureRandomSpi { + private long seed; + + private InsecureRandomSpi(long seed) { + this.seed=seed; + } + + private static long nextLong(long x) { + // This is a basic XORShift PRNG + x ^= (x << 21); + x ^= (x >>> 35); + x ^= (x << 4); + return x; + } + + private void initialise(byte[] seedBytes) { + this.seed=nextLong(Arrays.hashCode(seedBytes)); + } + + @Override + protected void engineSetSeed(byte[] seedBytes) { + initialise(seedBytes); + } + + @Override + protected void engineNextBytes(byte[] out) { + int n=out.length; + long x=seed; + for (int i=0; i>32); + x=nextLong(x); + } + seed=x; + } + + @Override + protected byte[] engineGenerateSeed(int length) { + byte[] newSeed = new byte[length]; + engineNextBytes(newSeed); + return newSeed; + } + } + + +} diff --git a/convex-core/src/main/java/convex/core/crypto/Mnemonic.java b/convex-core/src/main/java/convex/core/crypto/Mnemonic.java new file mode 100644 index 000000000..1b346777f --- /dev/null +++ b/convex-core/src/main/java/convex/core/crypto/Mnemonic.java @@ -0,0 +1,257 @@ +package convex.core.crypto; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.HashMap; + +import convex.core.data.Blob; +import convex.core.data.Hash; +import convex.core.util.Utils; + +/** + * Static utility functions for Mnemonic encoding + */ +public class Mnemonic { + + // word list from https://tools.ietf.org/html/rfc1751 + private static final String[] WORDS = { "a", "abe", "ace", "act", "ad", "ada", "add", "ago", "aid", "aim", "air", + "all", "alp", "am", "amy", "an", "ana", "and", "ann", "ant", "any", "ape", "aps", "apt", "arc", "are", + "ark", "arm", "art", "as", "ash", "ask", "at", "ate", "aug", "auk", "ave", "awe", "awk", "awl", "awn", "ax", + "aye", "bad", "bag", "bah", "bam", "ban", "bar", "bat", "bay", "be", "bed", "bee", "beg", "ben", "bet", + "bey", "bib", "bid", "big", "bin", "bit", "bob", "bog", "bon", "boo", "bop", "bow", "boy", "bub", "bud", + "bug", "bum", "bun", "bus", "but", "buy", "by", "bye", "cab", "cal", "cam", "can", "cap", "car", "cat", + "caw", "cod", "cog", "col", "con", "coo", "cop", "cot", "cow", "coy", "cry", "cub", "cue", "cup", "cur", + "cut", "dab", "dad", "dam", "dan", "dar", "day", "dee", "del", "den", "des", "dew", "did", "die", "dig", + "din", "dip", "do", "doe", "dog", "don", "dot", "dow", "dry", "dub", "dud", "due", "dug", "dun", "ear", + "eat", "ed", "eel", "egg", "ego", "eli", "elk", "elm", "ely", "em", "end", "est", "etc", "eva", "eve", + "ewe", "eye", "fad", "fan", "far", "fat", "fay", "fed", "fee", "few", "fib", "fig", "fin", "fir", "fit", + "flo", "fly", "foe", "fog", "for", "fry", "fum", "fun", "fur", "gab", "gad", "gag", "gal", "gam", "gap", + "gas", "gay", "gee", "gel", "gem", "get", "gig", "gil", "gin", "go", "got", "gum", "gun", "gus", "gut", + "guy", "gym", "gyp", "ha", "had", "hal", "ham", "han", "hap", "has", "hat", "haw", "hay", "he", "hem", + "hen", "her", "hew", "hey", "hi", "hid", "him", "hip", "his", "hit", "ho", "hob", "hoc", "hoe", "hog", + "hop", "hot", "how", "hub", "hue", "hug", "huh", "hum", "hut", "i", "icy", "ida", "if", "ike", "ill", "ink", + "inn", "io", "ion", "iq", "ira", "ire", "irk", "is", "it", "its", "ivy", "jab", "jag", "jam", "jan", "jar", + "jaw", "jay", "jet", "jig", "jim", "jo", "job", "joe", "jog", "jot", "joy", "jug", "jut", "kay", "keg", + "ken", "key", "kid", "kim", "kin", "kit", "la", "lab", "lac", "lad", "lag", "lam", "lap", "law", "lay", + "lea", "led", "lee", "leg", "len", "leo", "let", "lew", "lid", "lie", "lin", "lip", "lit", "lo", "lob", + "log", "lop", "los", "lot", "lou", "low", "loy", "lug", "lye", "ma", "mac", "mad", "mae", "man", "mao", + "map", "mat", "maw", "may", "me", "meg", "mel", "men", "met", "mew", "mid", "min", "mit", "mob", "mod", + "moe", "moo", "mop", "mos", "mot", "mow", "mud", "mug", "mum", "my", "nab", "nag", "nan", "nap", "nat", + "nay", "ne", "ned", "nee", "net", "new", "nib", "nil", "nip", "nit", "no", "nob", "nod", "non", "nor", + "not", "nov", "now", "nu", "nun", "nut", "o", "oaf", "oak", "oar", "oat", "odd", "ode", "of", "off", "oft", + "oh", "oil", "ok", "old", "on", "one", "or", "orb", "ore", "orr", "os", "ott", "our", "out", "ova", "ow", + "owe", "owl", "own", "ox", "pa", "pad", "pal", "pam", "pan", "pap", "par", "pat", "paw", "pay", "pea", + "peg", "pen", "pep", "per", "pet", "pew", "phi", "pi", "pie", "pin", "pit", "ply", "po", "pod", "poe", + "pop", "pot", "pow", "pro", "pry", "pub", "pug", "pun", "pup", "put", "quo", "rag", "ram", "ran", "rap", + "rat", "raw", "ray", "reb", "red", "rep", "ret", "rib", "rid", "rig", "rim", "rio", "rip", "rob", "rod", + "roe", "ron", "rot", "row", "roy", "rub", "rue", "rug", "rum", "run", "rye", "sac", "sad", "sag", "sal", + "sam", "san", "sap", "sat", "saw", "say", "sea", "sec", "see", "sen", "set", "sew", "she", "shy", "sin", + "sip", "sir", "sis", "sit", "ski", "sky", "sly", "so", "sob", "sod", "son", "sop", "sow", "soy", "spa", + "spy", "sub", "sud", "sue", "sum", "sun", "sup", "tab", "tad", "tag", "tan", "tap", "tar", "tea", "ted", + "tee", "ten", "the", "thy", "tic", "tie", "tim", "tin", "tip", "to", "toe", "tog", "tom", "ton", "too", + "top", "tow", "toy", "try", "tub", "tug", "tum", "tun", "two", "un", "up", "us", "use", "van", "vat", "vet", + "vie", "wad", "wag", "war", "was", "way", "we", "web", "wed", "wee", "wet", "who", "why", "win", "wit", + "wok", "won", "woo", "wow", "wry", "wu", "yam", "yap", "yaw", "ye", "yea", "yes", "yet", "you", "abed", + "abel", "abet", "able", "abut", "ache", "acid", "acme", "acre", "acta", "acts", "adam", "adds", "aden", + "afar", "afro", "agee", "ahem", "ahoy", "aida", "aide", "aids", "airy", "ajar", "akin", "alan", "alec", + "alga", "alia", "ally", "alma", "aloe", "also", "alto", "alum", "alva", "amen", "ames", "amid", "ammo", + "amok", "amos", "amra", "andy", "anew", "anna", "anne", "ante", "anti", "aqua", "arab", "arch", "area", + "argo", "arid", "army", "arts", "arty", "asia", "asks", "atom", "aunt", "aura", "auto", "aver", "avid", + "avis", "avon", "avow", "away", "awry", "babe", "baby", "bach", "back", "bade", "bail", "bait", "bake", + "bald", "bale", "bali", "balk", "ball", "balm", "band", "bane", "bang", "bank", "barb", "bard", "bare", + "bark", "barn", "barr", "base", "bash", "bask", "bass", "bate", "bath", "bawd", "bawl", "bead", "beak", + "beam", "bean", "bear", "beat", "beau", "beck", "beef", "been", "beer", "beet", "bela", "bell", "belt", + "bend", "bent", "berg", "bern", "bert", "bess", "best", "beta", "beth", "bhoy", "bias", "bide", "bien", + "bile", "bilk", "bill", "bind", "bing", "bird", "bite", "bits", "blab", "blat", "bled", "blew", "blob", + "bloc", "blot", "blow", "blue", "blum", "blur", "boar", "boat", "boca", "bock", "bode", "body", "bogy", + "bohr", "boil", "bold", "bolo", "bolt", "bomb", "bona", "bond", "bone", "bong", "bonn", "bony", "book", + "boom", "boon", "boot", "bore", "borg", "born", "bose", "boss", "both", "bout", "bowl", "boyd", "brad", + "brae", "brag", "bran", "bray", "bred", "brew", "brig", "brim", "brow", "buck", "budd", "buff", "bulb", + "bulk", "bull", "bunk", "bunt", "buoy", "burg", "burl", "burn", "burr", "burt", "bury", "bush", "buss", + "bust", "busy", "byte", "cady", "cafe", "cage", "cain", "cake", "calf", "call", "calm", "came", "cane", + "cant", "card", "care", "carl", "carr", "cart", "case", "cash", "cask", "cast", "cave", "ceil", "cell", + "cent", "cern", "chad", "char", "chat", "chaw", "chef", "chen", "chew", "chic", "chin", "chou", "chow", + "chub", "chug", "chum", "cite", "city", "clad", "clam", "clan", "claw", "clay", "clod", "clog", "clot", + "club", "clue", "coal", "coat", "coca", "cock", "coco", "coda", "code", "cody", "coed", "coil", "coin", + "coke", "cola", "cold", "colt", "coma", "comb", "come", "cook", "cool", "coon", "coot", "cord", "core", + "cork", "corn", "cost", "cove", "cowl", "crab", "crag", "cram", "cray", "crew", "crib", "crow", "crud", + "cuba", "cube", "cuff", "cull", "cult", "cuny", "curb", "curd", "cure", "curl", "curt", "cuts", "dade", + "dale", "dame", "dana", "dane", "dang", "dank", "dare", "dark", "darn", "dart", "dash", "data", "date", + "dave", "davy", "dawn", "days", "dead", "deaf", "deal", "dean", "dear", "debt", "deck", "deed", "deem", + "deer", "deft", "defy", "dell", "dent", "deny", "desk", "dial", "dice", "died", "diet", "dime", "dine", + "ding", "dint", "dire", "dirt", "disc", "dish", "disk", "dive", "dock", "does", "dole", "doll", "dolt", + "dome", "done", "doom", "door", "dora", "dose", "dote", "doug", "dour", "dove", "down", "drab", "drag", + "dram", "draw", "drew", "drub", "drug", "drum", "dual", "duck", "duct", "duel", "duet", "duke", "dull", + "dumb", "dune", "dunk", "dusk", "dust", "duty", "each", "earl", "earn", "ease", "east", "easy", "eben", + "echo", "eddy", "eden", "edge", "edgy", "edit", "edna", "egan", "elan", "elba", "ella", "else", "emil", + "emit", "emma", "ends", "eric", "eros", "even", "ever", "evil", "eyed", "face", "fact", "fade", "fail", + "fain", "fair", "fake", "fall", "fame", "fang", "farm", "fast", "fate", "fawn", "fear", "feat", "feed", + "feel", "feet", "fell", "felt", "fend", "fern", "fest", "feud", "fief", "figs", "file", "fill", "film", + "find", "fine", "fink", "fire", "firm", "fish", "fisk", "fist", "fits", "five", "flag", "flak", "flam", + "flat", "flaw", "flea", "fled", "flew", "flit", "floc", "flog", "flow", "flub", "flue", "foal", "foam", + "fogy", "foil", "fold", "folk", "fond", "font", "food", "fool", "foot", "ford", "fore", "fork", "form", + "fort", "foss", "foul", "four", "fowl", "frau", "fray", "fred", "free", "fret", "frey", "frog", "from", + "fuel", "full", "fume", "fund", "funk", "fury", "fuse", "fuss", "gaff", "gage", "gail", "gain", "gait", + "gala", "gale", "gall", "galt", "game", "gang", "garb", "gary", "gash", "gate", "gaul", "gaur", "gave", + "gawk", "gear", "geld", "gene", "gent", "germ", "gets", "gibe", "gift", "gild", "gill", "gilt", "gina", + "gird", "girl", "gist", "give", "glad", "glee", "glen", "glib", "glob", "glom", "glow", "glue", "glum", + "glut", "goad", "goal", "goat", "goer", "goes", "gold", "golf", "gone", "gong", "good", "goof", "gore", + "gory", "gosh", "gout", "gown", "grab", "grad", "gray", "greg", "grew", "grey", "grid", "grim", "grin", + "grit", "grow", "grub", "gulf", "gull", "gunk", "guru", "gush", "gust", "gwen", "gwyn", "haag", "haas", + "hack", "hail", "hair", "hale", "half", "hall", "halo", "halt", "hand", "hang", "hank", "hans", "hard", + "hark", "harm", "hart", "hash", "hast", "hate", "hath", "haul", "have", "hawk", "hays", "head", "heal", + "hear", "heat", "hebe", "heck", "heed", "heel", "heft", "held", "hell", "helm", "herb", "herd", "here", + "hero", "hers", "hess", "hewn", "hick", "hide", "high", "hike", "hill", "hilt", "hind", "hint", "hire", + "hiss", "hive", "hobo", "hock", "hoff", "hold", "hole", "holm", "holt", "home", "hone", "honk", "hood", + "hoof", "hook", "hoot", "horn", "hose", "host", "hour", "hove", "howe", "howl", "hoyt", "huck", "hued", + "huff", "huge", "hugh", "hugo", "hulk", "hull", "hunk", "hunt", "hurd", "hurl", "hurt", "hush", "hyde", + "hymn", "ibis", "icon", "idea", "idle", "iffy", "inca", "inch", "into", "ions", "iota", "iowa", "iris", + "irma", "iron", "isle", "itch", "item", "ivan", "jack", "jade", "jail", "jake", "jane", "java", "jean", + "jeff", "jerk", "jess", "jest", "jibe", "jill", "jilt", "jive", "joan", "jobs", "jock", "joel", "joey", + "john", "join", "joke", "jolt", "jove", "judd", "jude", "judo", "judy", "juju", "juke", "july", "june", + "junk", "juno", "jury", "just", "jute", "kahn", "kale", "kane", "kant", "karl", "kate", "keel", "keen", + "keno", "kent", "kern", "kerr", "keys", "kick", "kill", "kind", "king", "kirk", "kiss", "kite", "klan", + "knee", "knew", "knit", "knob", "knot", "know", "koch", "kong", "kudo", "kurd", "kurt", "kyle", "lace", + "lack", "lacy", "lady", "laid", "lain", "lair", "lake", "lamb", "lame", "land", "lane", "lang", "lard", + "lark", "lass", "last", "late", "laud", "lava", "lawn", "laws", "lays", "lead", "leaf", "leak", "lean", + "lear", "leek", "leer", "left", "lend", "lens", "lent", "leon", "lesk", "less", "lest", "lets", "liar", + "lice", "lick", "lied", "lien", "lies", "lieu", "life", "lift", "like", "lila", "lilt", "lily", "lima", + "limb", "lime", "lind", "line", "link", "lint", "lion", "lisa", "list", "live", "load", "loaf", "loam", + "loan", "lock", "loft", "loge", "lois", "lola", "lone", "long", "look", "loon", "loot", "lord", "lore", + "lose", "loss", "lost", "loud", "love", "lowe", "luck", "lucy", "luge", "luke", "lulu", "lund", "lung", + "lura", "lure", "lurk", "lush", "lust", "lyle", "lynn", "lyon", "lyra", "mace", "made", "magi", "maid", + "mail", "main", "make", "male", "mali", "mall", "malt", "mana", "mann", "many", "marc", "mare", "mark", + "mars", "mart", "mary", "mash", "mask", "mass", "mast", "mate", "math", "maul", "mayo", "mead", "meal", + "mean", "meat", "meek", "meet", "meld", "melt", "memo", "mend", "menu", "mert", "mesh", "mess", "mice", + "mike", "mild", "mile", "milk", "mill", "milt", "mimi", "mind", "mine", "mini", "mink", "mint", "mire", + "miss", "mist", "mite", "mitt", "moan", "moat", "mock", "mode", "mold", "mole", "moll", "molt", "mona", + "monk", "mont", "mood", "moon", "moor", "moot", "more", "morn", "mort", "moss", "most", "moth", "move", + "much", "muck", "mudd", "muff", "mule", "mull", "murk", "mush", "must", "mute", "mutt", "myra", "myth", + "nagy", "nail", "nair", "name", "nary", "nash", "nave", "navy", "neal", "near", "neat", "neck", "need", + "neil", "nell", "neon", "nero", "ness", "nest", "news", "newt", "nibs", "nice", "nick", "nile", "nina", + "nine", "noah", "node", "noel", "noll", "none", "nook", "noon", "norm", "nose", "note", "noun", "nova", + "nude", "null", "numb", "oath", "obey", "oboe", "odin", "ohio", "oily", "oint", "okay", "olaf", "oldy", + "olga", "olin", "oman", "omen", "omit", "once", "ones", "only", "onto", "onus", "oral", "orgy", "oslo", + "otis", "otto", "ouch", "oust", "outs", "oval", "oven", "over", "owly", "owns", "quad", "quit", "quod", + "race", "rack", "racy", "raft", "rage", "raid", "rail", "rain", "rake", "rank", "rant", "rare", "rash", + "rate", "rave", "rays", "read", "real", "ream", "rear", "reck", "reed", "reef", "reek", "reel", "reid", + "rein", "rena", "rend", "rent", "rest", "rice", "rich", "rick", "ride", "rift", "rill", "rime", "ring", + "rink", "rise", "risk", "rite", "road", "roam", "roar", "robe", "rock", "rode", "roil", "roll", "rome", + "rood", "roof", "rook", "room", "root", "rosa", "rose", "ross", "rosy", "roth", "rout", "rove", "rowe", + "rows", "rube", "ruby", "rude", "rudy", "ruin", "rule", "rung", "runs", "runt", "ruse", "rush", "rusk", + "russ", "rust", "ruth", "sack", "safe", "sage", "said", "sail", "sale", "salk", "salt", "same", "sand", + "sane", "sang", "sank", "sara", "saul", "save", "says", "scan", "scar", "scat", "scot", "seal", "seam", + "sear", "seat", "seed", "seek", "seem", "seen", "sees", "self", "sell", "send", "sent", "sets", "sewn", + "shag", "sham", "shaw", "shay", "shed", "shim", "shin", "shod", "shoe", "shot", "show", "shun", "shut", + "sick", "side", "sift", "sigh", "sign", "silk", "sill", "silo", "silt", "sine", "sing", "sink", "sire", + "site", "sits", "situ", "skat", "skew", "skid", "skim", "skin", "skit", "slab", "slam", "slat", "slay", + "sled", "slew", "slid", "slim", "slit", "slob", "slog", "slot", "slow", "slug", "slum", "slur", "smog", + "smug", "snag", "snob", "snow", "snub", "snug", "soak", "soar", "sock", "soda", "sofa", "soft", "soil", + "sold", "some", "song", "soon", "soot", "sore", "sort", "soul", "sour", "sown", "stab", "stag", "stan", + "star", "stay", "stem", "stew", "stir", "stow", "stub", "stun", "such", "suds", "suit", "sulk", "sums", + "sung", "sunk", "sure", "surf", "swab", "swag", "swam", "swan", "swat", "sway", "swim", "swum", "tack", + "tact", "tail", "take", "tale", "talk", "tall", "tank", "task", "tate", "taut", "teal", "team", "tear", + "tech", "teem", "teen", "teet", "tell", "tend", "tent", "term", "tern", "tess", "test", "than", "that", + "thee", "them", "then", "they", "thin", "this", "thud", "thug", "tick", "tide", "tidy", "tied", "tier", + "tile", "till", "tilt", "time", "tina", "tine", "tint", "tiny", "tire", "toad", "togo", "toil", "told", + "toll", "tone", "tong", "tony", "took", "tool", "toot", "tore", "torn", "tote", "tour", "tout", "town", + "trag", "tram", "tray", "tree", "trek", "trig", "trim", "trio", "trod", "trot", "troy", "true", "tuba", + "tube", "tuck", "tuft", "tuna", "tune", "tung", "turf", "turn", "tusk", "twig", "twin", "twit", "ulan", + "unit", "urge", "used", "user", "uses", "utah", "vail", "vain", "vale", "vary", "vase", "vast", "veal", + "veda", "veil", "vein", "vend", "vent", "verb", "very", "veto", "vice", "view", "vine", "vise", "void", + "volt", "vote", "wack", "wade", "wage", "wail", "wait", "wake", "wale", "walk", "wall", "walt", "wand", + "wane", "wang", "want", "ward", "warm", "warn", "wart", "wash", "wast", "wats", "watt", "wave", "wavy", + "ways", "weak", "weal", "wean", "wear", "weed", "week", "weir", "weld", "well", "welt", "went", "were", + "wert", "west", "wham", "what", "whee", "when", "whet", "whoa", "whom", "wick", "wife", "wild", "will", + "wind", "wine", "wing", "wink", "wino", "wire", "wise", "wish", "with", "wolf", "wont", "wood", "wool", + "word", "wore", "work", "worm", "worn", "wove", "writ", "wynn", "yale", "yang", "yank", "yard", "yarn", + "yawl", "yawn", "yeah", "year", "yell", "yoga", "yoke" }; + + private static final HashMap CODES = buildCodes(); + + /** + * Encode bytes as a mnemonic string + * + * @param data Byte array to encode + * @return Mnemonic String + */ + public static String encode(byte[] data) { + int bitLength = data.length * 8; + int n = (bitLength + 10) / 11; + String[] words = new String[n]; + for (int i = 0; i < n; i++) { + // extract 11 bits for each word + int bits = 0x7FF & Utils.extractBits(data, 11, i * 11); + words[i] = WORDS[bits]; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) { + if (i > 0) sb.append(' '); + sb.append(words[i]); + } + return sb.toString(); + } + + /** + * Encode bytes as a mnemonic string + * + * @param x Bytes to encode + * @param bitLength Length of key to encode + * @return Mnemonic String + */ + public static String encode(BigInteger x, int bitLength) { + return Mnemonic.encode(Utils.hexToBytes(Utils.toHexString(x, bitLength >> 2))); + } + + private static HashMap buildCodes() { + HashMap hm = new HashMap<>(3000); // big enough to avoid resize + for (int i = 0; i < WORDS.length; i++) { + hm.put(WORDS[i], i); + } + return hm; + } + + /** + * Decode from a Mnemonic string + * @param phrase Mnemonic string + * @param bitLength Bits to extract + * @return Decoded byte array + */ + public static byte[] decode(String phrase, int bitLength) { + int nByte = (bitLength + 7) / 8; + byte[] result = new byte[nByte]; + + phrase = phrase.trim().toLowerCase(); + String[] words = phrase.split("\\s+"); + int n = words.length; + if (n * 11 < bitLength) + throw new IllegalArgumentException("Insufficient words (" + n + ") to cover bitlength of " + bitLength); + + for (int i = 0; i < n; i++) { + String word = words[i]; + Integer x = CODES.get(word); + if (x == null) throw new IllegalArgumentException( + "Can't find word (" + word + ") in mnemonic dictionary for phrase " + phrase); + Utils.setBits(result, 11, i * 11, x); + } + + return result; + } + + /** + * Create a secure random mnemonic string + * + * @return Mnemonic String + */ + public static String createSecureRandom() { + byte[] bs = Blob.createRandom(new SecureRandom(), 16).getBytes(); + return encode(bs); + } + + public static AKeyPair decodeKeyPair(String s) { + byte[] bs = Mnemonic.decode(s, 128); + Hash h = Blob.wrap(bs).getContentHash(); + Ed25519KeyPair kp = Ed25519KeyPair.create(h.getBytes()); + return kp; + } +} diff --git a/convex-core/src/main/java/convex/core/crypto/PBE.java b/convex-core/src/main/java/convex/core/crypto/PBE.java new file mode 100644 index 000000000..3cb3ebaff --- /dev/null +++ b/convex-core/src/main/java/convex/core/crypto/PBE.java @@ -0,0 +1,34 @@ +package convex.core.crypto; + +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +public class PBE { + + /** + * Gets a key of the given length (in bits) from a password using key derivation + * function + * + * @param password Password stored in a char array. + * @param salt Salt bytes + * @param bitLength Bit length + * @return Decrypted key + */ + public static byte[] deriveKey(char[] password, byte[] salt, int bitLength) { + try { + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + PBEKeySpec pbeKeySpec = new PBEKeySpec(password, salt, 1000, bitLength); + SecretKey secretKey = factory.generateSecret(pbeKeySpec); + int byteLen = bitLength / 8; + byte[] key = new byte[byteLen]; + System.arraycopy(secretKey.getEncoded(), 0, key, 0, byteLen); + return key; + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new Error(e); + } + } +} diff --git a/convex-core/src/main/java/convex/core/crypto/PEMTools.java b/convex-core/src/main/java/convex/core/crypto/PEMTools.java new file mode 100644 index 000000000..d817ceabc --- /dev/null +++ b/convex-core/src/main/java/convex/core/crypto/PEMTools.java @@ -0,0 +1,163 @@ +package convex.core.crypto; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Security; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; + +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PKCS8Generator; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator; +import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder; +import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.operator.InputDecryptorProvider; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.OutputEncryptor; +import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; +import org.bouncycastle.pkcs.PKCSException; +import org.bouncycastle.util.io.pem.PemObject; + +public class PEMTools { + // private static String encryptionAlgorithm="AES-128-CBC"; + + /** + * Writes a key pair to a String + * @param kp Key pair to write + * @return PEM String representation of key pair + */ + public static String writePEM(AKeyPair kp) { + + PrivateKey priv=kp.getPrivate(); + // PublicKey pub=kp.getPublic(); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(priv.getEncoded()); + + byte[] encoded=keySpec.getEncoded(); + String base64=Base64.getEncoder().encodeToString(encoded); + + StringBuilder sb=new StringBuilder(); + sb.append("-----BEGIN PRIVATE KEY-----"); + sb.append(System.lineSeparator()); + sb.append(base64); + sb.append(System.lineSeparator()); + sb.append("-----END PRIVATE KEY-----"); + String pem=sb.toString(); + return pem; + } + + /** + * Read a key pair from a PEM String + * @param pem PEM String + * @return Key pair instance + * @throws GeneralSecurityException If a security error occurs + */ + public static AKeyPair readPEM(String pem) throws GeneralSecurityException { + String publicKeyPEM = pem.trim() + .replace("-----BEGIN PRIVATE KEY-----", "") + .replaceAll(System.lineSeparator(), "") + .replace("-----END PRIVATE KEY-----", ""); + + byte[] bs = Base64.getDecoder().decode(publicKeyPEM); + + KeyFactory keyFactory = KeyFactory.getInstance("Ed25519"); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bs); + PrivateKey priv=keyFactory.generatePrivate(keySpec); + PublicKey pub=keyFactory.generatePublic(keySpec); + return Ed25519KeyPair.create(pub, priv); + } + + /** + * Encrypt a priavte key into a PEM formated text + * + * @param privateKey Private key to encrypt + * + * @param password Password to use for encryption + * + * @return PEM text that can be saved or sent to another keystore + * + * @throws Error Any encryption error that occurs + */ + public static String encryptPrivateKeyToPEM(PrivateKey privateKey, char[] password) throws Error { + StringWriter stringWriter = new StringWriter(); + JcaPEMWriter writer = new JcaPEMWriter(stringWriter); + JceOpenSSLPKCS8EncryptorBuilder builder = new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.AES_256_CBC); + builder.setPassword(password); + try { + OutputEncryptor encryptor = builder.build(); + JcaPKCS8Generator generator = new JcaPKCS8Generator(privateKey, encryptor); + writer.writeObject(generator); + writer.close(); + } catch (IOException | OperatorCreationException e) { + throw new Error("cannot encrypt private key to PEM: " + e); + } + return stringWriter.toString(); + } + + /** + * Decrypt a PEM string to a private key. The PEM string must contain the "ENCRYPTED PRIVATE KEY" type. + * + * @param PEM PEM string to decode + * + * @param password Password that was used to encrypt the priavte key + * + * @return PrivateKey stored in the PEM + * + * @throws Error on reading the PEM, decryption and decoding the private key + */ + public static PrivateKey decryptPrivateKeyFromPEM(String pemText, char[] password) throws Error { + PrivateKey privateKey = null; + StringReader stringReader = new StringReader(pemText); + PEMParser pemParser = new PEMParser(stringReader); + PemObject pemObject = null; + Security.addProvider(new BouncyCastleProvider()); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); + try { + pemObject = pemParser.readPemObject(); + while (pemObject != null) { + if (pemObject.getType().equals("ENCRYPTED PRIVATE KEY")) { + break; + } + pemObject = pemParser.readPemObject(); + } + + } catch (IOException e) { + throw new Error("cannot read PEM " + e); + } + + if (pemObject == null) { + throw new Error("no encrypted private key found in pem text"); + } + try { + PKCS8EncryptedPrivateKeyInfo encryptedInfo = new PKCS8EncryptedPrivateKeyInfo(pemObject.getContent()); + + JceOpenSSLPKCS8DecryptorProviderBuilder inputBuilder = new JceOpenSSLPKCS8DecryptorProviderBuilder(); + inputBuilder.setProvider("BC"); + InputDecryptorProvider decryptor = inputBuilder.build(password); + + PrivateKeyInfo privateKeyInfo = encryptedInfo.decryptPrivateKeyInfo(decryptor); + privateKey = converter.getPrivateKey(privateKeyInfo); + } catch (IOException | OperatorCreationException | PKCSException e) { + throw new Error("cannot decrypt password from PEM " + e); + } + return privateKey; + } + + public static void main(String[] args) throws Exception { + AKeyPair kp=AKeyPair.createSeeded(1337); + String pem=writePEM(kp); + System.out.println(pem); + + AKeyPair kp2=readPEM(pem); + System.out.println(kp2); + } + +} diff --git a/convex-core/src/main/java/convex/core/crypto/PFXTools.java b/convex-core/src/main/java/convex/core/crypto/PFXTools.java new file mode 100644 index 000000000..15ebe4443 --- /dev/null +++ b/convex-core/src/main/java/convex/core/crypto/PFXTools.java @@ -0,0 +1,170 @@ +package convex.core.crypto; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.util.Date; + +import org.bouncycastle.jce.X509Principal; +import org.bouncycastle.x509.X509V3CertificateGenerator; + +@SuppressWarnings("deprecation") +public class PFXTools { + public static final String KEYSTORE_TYPE="PKCS12"; + + public static final String CERTIFICATE_ALGORITHM = "RSA"; + + /** + * Creates a new PKCS12 key store. + * @param keyFile File to use for creating the key store + * @param passPhrase Passphrase used to protect the key store, may be null + * @return New KeyStore instance + */ + @SuppressWarnings("javadoc") + public static KeyStore createStore(File keyFile, String passPhrase) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { + KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE); + + // need to load in bouncy castle crypto providers to set/get keys from the keystore + Providers.init(); + + char[] pwdArray = (passPhrase==null)?null:passPhrase.toCharArray(); + ks.load(null, pwdArray); + + try (FileOutputStream fos = new FileOutputStream(keyFile)) { + ks.store(fos, pwdArray); + } + return ks; + } + + /** + * Loads an existing PKCS12 Key store. + * @param keyFile File for the existing key store + * @param passPhrase Passphrase for decrypting the key store. May be blank or null if not encrypted. + * @return Found key store + * @throws IOException If an IO error occurs + * @throws GeneralSecurityException If a security error occurs + */ + public static KeyStore loadStore(File keyFile, String passPhrase) throws IOException,GeneralSecurityException { + + // Need to load in bouncy castle crypto providers to set/get keys from the keystore. + Providers.init(); + + KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE); + + char[] pwdArray = (passPhrase==null)?null:passPhrase.toCharArray(); + try (FileInputStream fis = new FileInputStream(keyFile)) { + ks.load(fis, pwdArray); + } + return ks; + } + + /** + * Saves a PKCS12 Key store to disk. + * @param ks Key store to save + * @param keyFile Target file + * @param passPhrase Passphrase for encrypting the key store. May be blank or null if not need for encryption. + * @return Same key store instance. + * @throws IOException If an IO error occurs accessing the key store + * @throws GeneralSecurityException if a security exception occurs + */ + public static KeyStore saveStore(KeyStore ks, File keyFile, String passPhrase) throws GeneralSecurityException, IOException { + + char[] pwdArray = (passPhrase==null)?null:passPhrase.toCharArray(); + try (FileOutputStream fos = new FileOutputStream(keyFile)) { + ks.store(fos, pwdArray); + } + + return ks; + } + + /** + * Generates a self-signed certificate. + * @param kpToSign Key pair + * @return New certificate + * @throws GeneralSecurityException If a security exception occurs + */ + public static Certificate createSelfSignedCertificate(AKeyPair kpToSign) throws GeneralSecurityException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(CERTIFICATE_ALGORITHM); + KeyPair kp=keyPairGenerator.generateKeyPair(); + + X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator(); + + String domainName="convex.world"; + v3CertGen.setSerialNumber(BigInteger.valueOf(new SecureRandom().nextInt(Integer.MAX_VALUE))); + v3CertGen.setIssuerDN(new X509Principal("CN=" + domainName + ", OU=None, O=None L=None, C=None")); + v3CertGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60 * 60 * 24 * 30)); + v3CertGen.setNotAfter(new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 365*10))); + v3CertGen.setSubjectDN(new X509Principal("CN=" + domainName + ", OU=None, O=None L=None, C=None")); + + v3CertGen.setPublicKey(kpToSign.getPublic()); + v3CertGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); + + Certificate cert = v3CertGen.generateX509Certificate(kp.getPrivate()); + return cert; + } + + /** + * Retrieves a key pair from a key store. + * @param ks Key store + * @param alias Alias used for finding the key pair in the store + * @param passphrase Passphrase used for decrypting the key pair. Mandatory. + * @return Found key pair + */ + public static AKeyPair getKeyPair(KeyStore ks, String alias, String passPhrase) throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException { + char[] pwdArray = passPhrase.toCharArray(); + + Certificate cert = ks.getCertificate(alias); + if (cert == null) return null; + Key sk=ks.getKey(alias,pwdArray); + return Ed25519KeyPair.create(cert.getPublicKey(),(PrivateKey) sk); + } + + /** + * Adds a key pair to a key store. + * @param ks Key store + * @param kp Key pair + * @param passPhrase Passphrase for encrypting the key pair. Mandatory. + * @return Updated key store. + * @throws IOException If an IO error occurs accessing the key store + * @throws GeneralSecurityException if a security exception occurs + */ + public static KeyStore setKeyPair(KeyStore ks, AKeyPair kp, String passPhrase) throws IOException, GeneralSecurityException { + + return setKeyPair(ks, kp.getAccountKey().toHexString(), kp, passPhrase); + } + + /** + * Adds a key pair to a key store. + * @param ks Key store + * @param kp Key pair + * @param passPhrase Passphrase for encrypting the key pair. Mandatory. + * @return Updated key store. + * @throws IOException If an IO error occurs accessing the key store + * @throws GeneralSecurityException if a security exception occurs + */ + public static KeyStore setKeyPair(KeyStore ks, String alias, AKeyPair kp, String passPhrase) throws IOException, GeneralSecurityException { + + if (passPhrase == null) throw new IllegalArgumentException("Password is mandatory for private key"); + char[] pwdArray = passPhrase.toCharArray(); + + Certificate cert = createSelfSignedCertificate(kp); + ks.setKeyEntry(alias, kp.getPrivate(), pwdArray, new Certificate[] {cert}); + + return ks; + } + +} diff --git a/convex-core/src/main/java/convex/core/crypto/Providers.java b/convex-core/src/main/java/convex/core/crypto/Providers.java new file mode 100644 index 000000000..20bf259e6 --- /dev/null +++ b/convex-core/src/main/java/convex/core/crypto/Providers.java @@ -0,0 +1,27 @@ +package convex.core.crypto; + +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import com.goterl.lazysodium.LazySodiumJava; +import com.goterl.lazysodium.SodiumJava; +import com.goterl.lazysodium.interfaces.Sign; + + + +public class Providers { + private static final SodiumJava NATIVE_SODIUM=new SodiumJava(); + + public static final LazySodiumJava SODIUM= new LazySodiumJava(NATIVE_SODIUM); + + public static final Sign.Native SODIUM_SIGN=(Sign.Native) SODIUM; + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + public static void init() { + // static method to ensure static initialisation happens + } +} diff --git a/convex-core/src/main/java/convex/core/crypto/Symmetric.java b/convex-core/src/main/java/convex/core/crypto/Symmetric.java new file mode 100644 index 000000000..5202ae59f --- /dev/null +++ b/convex-core/src/main/java/convex/core/crypto/Symmetric.java @@ -0,0 +1,160 @@ +package convex.core.crypto; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import convex.core.util.Utils; + +/** + * Class providing symmetric encryption functionality using AES + */ +public class Symmetric { + // Bouncy castle provider + static final BouncyCastleProvider PROVIDER = new org.bouncycastle.jce.provider.BouncyCastleProvider(); + + private static final String SYMMETRIC_ENCRYPTION_ALGO = "AES/CBC/PKCS5Padding"; + private static final int IV_LENGTH = 16; + private static final String SYMMETRIC_KEY_ALGORITHM = "AES"; + + private static final int KEY_LENGTH = 128; + + /** + * Encrypts a String with a given AES secret key, using standard UTF-8 encoding + * + * @param key AES secret key + * @param data String to encrypt + * @return Encrypted representation of the given string + */ + public static byte[] encrypt(SecretKey key, String data) { + return encrypt(key, data.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Encrypt bytes with a given AES SecretKey Prepends the IV to the ciphertext. + * + * @param key Secret encryption key + * @param data Data to encrypt + * @return Encrypted representation of the given byte array data + */ + public static byte[] encrypt(SecretKey key, byte[] data) { + Cipher cipher = null; + byte[] iv = null; + try { + cipher = Cipher.getInstance(SYMMETRIC_ENCRYPTION_ALGO); + cipher.init(Cipher.ENCRYPT_MODE, key); + iv = cipher.getIV(); + } catch (Exception e) { + throw new Error("Failed to initialise encryption cipher", e); + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + bos.write(iv); + } catch (IOException e) { + throw new Error("Problem writing IV with value " + Utils.toHexString(iv), e); + } + + CipherOutputStream cos = new CipherOutputStream(bos, cipher); + try { + cos.write(data); + cos.flush(); + cos.close(); + } catch (IOException e) { + throw new Error("Error encrypting data,e"); + } + return bos.toByteArray(); + } + + /** + * Decrypts a string from ciphertext, assuming UTF-8 format data + * + * @param key AES Secret Key + * @param encryptedData encrypted byte[] data to decrypt (ciphertext) + * @return The decrypted String + */ + public static String decryptString(SecretKey key, byte[] encryptedData) { + return new String(decrypt(key, encryptedData), StandardCharsets.UTF_8); + } + + /** + * Decrypts AES ciphertext with a given secret key. IV is assumed to be + * prepended to the cipherText + * + * @param key Secret encryption key + * @param encryptedData Encrypted data to decrypt + * @return A new byte array containing the decrypted data + */ + public static byte[] decrypt(SecretKey key, byte[] encryptedData) { + ByteArrayInputStream bis = new ByteArrayInputStream(encryptedData); + try { + return decrypt(key, bis); + } catch (IOException e) { + throw new Error("Unexpected IO exception", e); + } + } + + /** + * Decrypts AES ciphertext with a given secret key. IV is assumed to be + * prepended to the cipherText + * + * @param key Secret encryption key + * @param bis InputStream of data to decrypt + * @return A new byte array containing the decrypted data + * @throws IOException If an IO error occurs + */ + public static byte[] decrypt(SecretKey key, InputStream bis) throws IOException { + byte[] iv = new byte[IV_LENGTH]; + int x = bis.read(iv, 0, IV_LENGTH); + if (x != IV_LENGTH) throw new Error("IV not read correctly, " + x + " byes read"); + + // Create a Cipher using the provided key + Cipher cipher = null; + try { + cipher = Cipher.getInstance(SYMMETRIC_ENCRYPTION_ALGO); + IvParameterSpec ivParamSpec = new IvParameterSpec(iv); + cipher.init(Cipher.DECRYPT_MODE, key, ivParamSpec); + } catch (Exception e) { + throw new Error("Failed to initialise decryption cipher", e); + } + + // Read unencrypted bytes using CipherInputStream + CipherInputStream cis = new CipherInputStream(bis, cipher); + try { + return Utils.readBytes(cis); + } catch (IOException e) { + throw new Error("Failed to decrypt from input stream", e); + } + } + + /** + * Creates an AES secret key + * + * @return The generated SecretKey + */ + public static SecretKey createSecretKey() { + KeyGenerator kgen; + + try { + kgen = KeyGenerator.getInstance(SYMMETRIC_KEY_ALGORITHM); + kgen.init(KEY_LENGTH); + } catch (NoSuchAlgorithmException e) { + throw new Error("Key generator not initialised sucessfully", e); + } + SecretKey key = kgen.generateKey(); + return key; + } + +} diff --git a/convex-core/src/main/java/convex/core/crypto/Wallet.java b/convex-core/src/main/java/convex/core/crypto/Wallet.java new file mode 100644 index 000000000..042d51293 --- /dev/null +++ b/convex-core/src/main/java/convex/core/crypto/Wallet.java @@ -0,0 +1,69 @@ +package convex.core.crypto; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.security.KeyStore; +import java.util.Enumeration; +import java.util.HashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import convex.core.data.Address; + +public class Wallet { + public static final String KEYSTORE_TYPE="pkcs12"; + + private static final Logger log = LoggerFactory.getLogger(Wallet.class.getName()); + + private HashMap data; + + private Wallet(HashMap data) { + this.data = data; + } + + public static Wallet create() { + return new Wallet(new HashMap()); + } + + public WalletEntry get(Address a) { + return data.get(a); + } + + public static File createTempStore(String password) { + try { + KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE); + char[] pwdArray = "password".toCharArray(); + ks.load(null, pwdArray); + File file=File.createTempFile("temp-keystore", "p12"); + file.deleteOnExit(); + try (FileOutputStream fos = new FileOutputStream(file)) { + ks.store(fos, pwdArray); + } + return file; + } catch (Throwable t) { + throw new Error("Unable to create temp keystore",t); + } + } + + public static Wallet load(File file,String password) { + try { + KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE); + char[] pwdArray = password.toCharArray(); + ks.load(new FileInputStream(file), pwdArray); + Enumeration aliases=ks.aliases(); + Wallet wallet=Wallet.create(); + + while (aliases.hasMoreElements()) { + String alias=aliases.nextElement(); + ks.getKey(alias, pwdArray); + log.info("Loading private key with alias: "+alias); + } + return wallet; + } catch (Throwable t) { + throw new Error("Unable to load keystore with file: "+file,t); + } + } +} diff --git a/convex-core/src/main/java/convex/core/crypto/WalletEntry.java b/convex-core/src/main/java/convex/core/crypto/WalletEntry.java new file mode 100644 index 000000000..7527feb80 --- /dev/null +++ b/convex-core/src/main/java/convex/core/crypto/WalletEntry.java @@ -0,0 +1,86 @@ +package convex.core.crypto; + +import convex.core.data.ACell; +import convex.core.data.AMap; +import convex.core.data.AccountKey; +import convex.core.data.Address; +import convex.core.data.Keyword; +import convex.core.data.Maps; +import convex.core.data.SignedData; +import convex.core.exceptions.TODOException; + +/** + * Class implementing a Wallet Entry. + * + * May be in a locked locked or unlocked state. Unlocking requires passphrase. + */ +public class WalletEntry { + private final Address address; + private final AKeyPair keyPair; + private final AMap data; + + private WalletEntry(Address address, AMap data, AKeyPair kp) { + this.address=address; + this.data = data; + this.keyPair = kp; + } + + private WalletEntry(AMap data) { + this(null,data, null); + } + + public static WalletEntry create(Address address,AKeyPair kp) { + return new WalletEntry(address, Maps.empty(), kp); + } + + public AccountKey getAccountKey() { + return keyPair.getAccountKey(); + } + + public Address getAddress() { + return address; + } + + public AKeyPair getKeyPair() { + if (keyPair == null) throw new IllegalStateException("Wallet not unlocked!"); + return keyPair; + } + + public WalletEntry unlock(char[] password) { + if (keyPair != null) throw new IllegalStateException("Wallet already unlocked!"); + + // byte[] privateKey=PBE.deriveKey(password, data); + + // SignKeyPair kp=SignKeyPair.create(privateKey); + // return this.withKeyPair(kp); + + throw new TODOException(); + } + + public WalletEntry withKeyPair(AKeyPair kp) { + return new WalletEntry(address,data, kp); + } + + public WalletEntry withAddress(Address address) { + return new WalletEntry(null,data, keyPair); + } + + public WalletEntry lock() { + if (keyPair == null) throw new IllegalStateException("Wallet already locked!"); + // Clear keypair + return this.withKeyPair(null); + } + + public boolean isLocked() { + return (keyPair == null); + } + + @Override + public String toString() { + return getAddress() +" : " +getAccountKey().toChecksumHex(); + } + + public SignedData sign(R message) { + return keyPair.signData(message); + } +} diff --git a/convex-core/src/main/java/convex/core/data/AArrayBlob.java b/convex-core/src/main/java/convex/core/data/AArrayBlob.java new file mode 100644 index 000000000..0ef4dd828 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/AArrayBlob.java @@ -0,0 +1,299 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.util.Arrays; + +import convex.core.exceptions.InvalidDataException; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * Abstract base class for binary data stored in Java arrays. + * + */ +public abstract class AArrayBlob extends ABlob { + protected final byte[] store; + protected final int offset; + protected final int length; + + protected AArrayBlob(byte[] bytes, int offset, int length) { + this.store = bytes; + this.length = length; + this.offset = offset; + } + + @Override + public void updateDigest(MessageDigest digest) { + digest.update(store, offset, length); + } + + @Override + public Blob slice(long start, long length) { + if (length < 0) throw new IllegalArgumentException(Errors.negativeLength(length)); + if (start < 0) throw new IndexOutOfBoundsException("Start out of bounds: " + start); + if ((start + length) > this.length) + throw new IndexOutOfBoundsException("End out of bounds: " + (start + length)); + return Blob.wrap(store, Utils.checkedInt(start + offset), Utils.checkedInt(length)); + } + + @Override + public ABlob append(ABlob d) { + int dlength = Utils.checkedInt(d.count()); + if (dlength == 0) return this; + int length = this.length; + if (length == 0) return d; + byte[] newData = new byte[length + dlength]; + getBytes(newData, 0); + d.getBytes(newData, length); + return Blob.wrap(newData); + } + + @Override + public Blob slice(long start) { + return slice(start, count() - start); + } + + @Override + public Blob toBlob() { + return Blob.wrap(store, offset, length); + } + + @Override + public final int compareTo(ABlob b) { + if (b instanceof AArrayBlob) { + return compareTo((AArrayBlob) b); + } else { + return compareTo(b.toBlob()); + } + } + + public final int compareTo(AArrayBlob b) { + if (this == b) return 0; + int alength = this.length; + int blength = b.length; + int compareLength = Math.min(alength, blength); + int c = Utils.compareByteArrays(this.store, this.offset, b.store, b.offset, compareLength); + if (c != 0) return c; + if (alength > compareLength) return 1; // this is bigger + if (blength > compareLength) return -1; // b is bigger + return 0; + } + + @Override + public final void getBytes(byte[] dest, int destOffset) { + System.arraycopy(store, offset, dest, destOffset, length); + } + + @Override + public ByteBuffer writeToBuffer(ByteBuffer bb) { + return bb.put(store, offset, length); + } + + public int writeToBuffer(byte[] bs, int pos) { + System.arraycopy(store, offset, bs, pos, length); + return Utils.checkedInt(pos + length); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + System.arraycopy(store, offset, bs, pos, length); + return pos + length; + } + + @Override + public final String toHexString() { + return Utils.toHexString(store, offset, length); + } + + @Override + public void toHexString(StringBuilder sb) { + sb.append(toHexString()); + } + + @Override + public final long count() { + return length; + } + + @Override + public final byte byteAt(long i) { + int ix = (int) i; + if ((ix != i) || (ix < 0) || (ix >= length)) { + throw new IndexOutOfBoundsException("Index: " + i); + } + return store[offset + ix]; + } + + @Override + public final byte getUnchecked(long i) { + int ix = (int) i; + return store[offset + ix]; + } + + @Override + public int getHexDigit(long digitPos) { + byte b = store[offset+ (int)(digitPos >> 1)]; + // if ((digitPos & 1) == 0) { + // return (b >> 4) & 0x0F; // first hex digit + // } else { + // return b & 0x0F; // second hex digit + // } + int shift = 4 - (((int) digitPos & 1) << 2); + return (b >> shift) & 0x0F; + } + + /** + * Gets the internal array backing this Blob. Use with caution! + * @return Byte array backing this blob + */ + public byte[] getInternalArray() { + return store; + } + + /** + * Gets this offset into the internal array backing this Blob. + * @return Offset into backing array + */ + public int getInternalOffset() { + return offset; + } + + @Override + public ByteBuffer getByteBuffer() { + return ByteBuffer.wrap(store, offset, length).asReadOnlyBuffer(); + } + + @Override + public boolean equalsBytes(byte[] bytes, int byteOffset) { + return Utils.arrayEquals(store, offset, bytes, byteOffset, length); + } + + public boolean equalsBytes(ABlob k) { + if (k.count()!=this.count()) return false; + return k.equalsBytes(store,offset); + } + + /** + * Tests if a specific range of bytes are exactly equal. + * @param b Blob to compare with + * @param start Start index of range (inclusive) + * @param end End index of range (exclusive) + * @return true if digits are equal, false otherwise + */ + public boolean rangeMatches(ABlob b, int start, int end) { + if (b instanceof AArrayBlob) return rangeMatches((AArrayBlob)b,start,end); + for (int i = start; i < end; i++) { + // null entry if key does not match prefix + if (store[offset+i] != b.getUnchecked(i)) return false; + } + return true; + } + + /** + * Tests if a specific range of bytes are exactly equal from this Blob with another Blob + * @param b Blob with which to compare + * @param start Start index in both Blobs + * @param end End index in both Blobs + * @return true if digits are equal, false otherwise + */ + public boolean rangeMatches(AArrayBlob b, int start, int end) { + return Arrays.equals(store, offset+start, offset+end, b.store, b.offset+start,b.offset+end); + } + + @Override + public long hexMatchLength(ABlob b, long start, long length) { + if (b == this) return length; + long end = start + length; + for (long i = start; i < end; i++) { + if (!(getHexDigit(i) == b.getHexDigit(i))) return i - start; + } + return length; + } + + /** + * Tests if a specific range of hex digits are exactly equal. + * @param key Blob to compare with + * @param start Start hex digit index (inclusive) + * @param end End hex digit index (Exclusive) + * @return true if digits are equal, false otherwise + */ + public boolean hexMatches(ABlob key, int start, int end) { + if (key==this) return true; + if (start==end) return true; + if ((start&1)!=0) if (key.getHexDigit(start) != getHexDigit(start)) return false; + if ((end&1)!=0) if (key.getHexDigit(end-1) != getHexDigit(end-1)) return false; + return rangeMatches(key,(start+1)>>1,end>>1); + } + + @Override + public long commonHexPrefixLength(ABlob b) { + if (b == this) return count() * 2; + + long max = Math.min(count(), b.count()); + for (long i = 0; i < max; i++) { + byte ai = getUnchecked(i); + byte bi = b.getUnchecked(i); + if (ai != bi) return (i * 2) + (Utils.firstDigitMatch(ai, bi) ? 1 : 0); + } + return max * 2; + } + + + + @Override + public void validate() throws InvalidDataException { + super.validate(); + } + + @Override + public void validateCell() throws InvalidDataException { + if (length < 0) throw new InvalidDataException("Negative length: " + length, this); + if (offset < 0) throw new InvalidDataException("Negative data offset: " + offset, this); + if ((offset + length) > store.length) { + throw new InvalidDataException( + "End out of range: " + (offset + length) + " with array size=" + store.length, this); + } + } + + @Override + public long longValue() { + if (length != 8) throw new IllegalStateException(Errors.wrongLength(8, length)); + return Utils.readLong(store, offset); + } + + @Override + public long toLong() { + if (length >= 8) { + return Utils.readLong(store, offset + length - 8); + } else { + long result = 0l; + int ix = offset; + if ((length & 4) != 0) { + result += 0xffffffffL & Utils.readInt(store, ix); + ix += 4; + } + if ((length & 2) != 0) { + result = (result >> 16) + (0xFFFF & Utils.readShort(store, ix)); + ix += 2; + } + if ((length & 1) != 0) { + result = (result >> 8) + (0xFF & store[ix]); + ix += 1; + } + // TODO: do we want to sign extend? + // int shift=8*(8-length); + // correct sign + // result=(result<>shift; + return result; + } + } + + @Override + public int getRefCount() { + // Array-backed blobs have no child Refs by default + return 0; + } + + +} \ No newline at end of file diff --git a/convex-core/src/main/java/convex/core/data/ABlob.java b/convex-core/src/main/java/convex/core/data/ABlob.java new file mode 100644 index 000000000..bfb2ddff1 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/ABlob.java @@ -0,0 +1,393 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.security.MessageDigest; + +import convex.core.crypto.Hashing; +import convex.core.data.prim.CVMByte; +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.InvalidDataException; +import convex.core.util.Utils; + +/** + * Abstract base class for data objects containing immutable chunks of binary + * data. Representation is equivalent to a fixed size immutable byte sequence. + * + * Rationale: - Allow data to be encapsulated as an immutable object - Provide + * specialised methods for processing byte data - Provide a cached Hash value, + * lazily computed on demand + * + */ +public abstract class ABlob extends ACountable implements Comparable { + /** + * Cached hash of the Blob data. Might be null. + */ + protected Hash contentHash = null; + + @Override + public AType getType() { + return Types.BLOB; + } + + /** + * Copies the bytes from this blob to a given destination + * + * @param dest Destination array + * @param destOffset Offset into destination array + */ + public abstract void getBytes(byte[] dest, int destOffset); + + /** + * Gets the length of this Blob + * + * @return The length in bytes of this data object + */ + @Override + public abstract long count(); + + @Override + public CVMByte get(long ix) { + return CVMByte.create(byteAt(ix)); + } + + @Override + public Ref getElementRef(long index) { + return get(index).getRef(); + } + + public Blob empty() { + return Blob.EMPTY; + } + + /** + * Converts this data object to a lowercase hex string representation + * @return Hex String representation + */ + public abstract String toHexString(); + + /** + * Converts this blob to a readable byte buffer + * @return ByteBuffer with position zero (ready to read) + */ + public ByteBuffer toByteBuffer() { + return ByteBuffer.wrap(getBytes()); + } + + /** + * Converts this data object to a hex string representation of the given length. + * Equivalent to truncating the full String representation. + * @param length Length to truncate String to (in hex characters) + * @return String representation of hex values in Blob + */ + public String toHexString(int length) { + return toHexString().substring(0, length); + } + + /** + * Gets a contiguous slice of this blob, as a new Blob. + * + * Shares underlying backing data where possible + * + * @param start Start position for the created slice + * @param length Length of the slice + * @return A blob of the specified length, representing a slice of this blob. + */ + public abstract ABlob slice(long start, long length); + + /** + * Gets a slice of this blob, as a new blob, starting from the given offset and + * extending to the end of the blob. + * + * Shares underlying backing data where possible. Returned Blob may not be the + * same type as the original Blob + * @param start Start position to slice from + * @return Slice of Blob + */ + public ABlob slice(long start) { + return slice(start, count() - start); + } + + /** + * Converts this object to a Blob instance + * + * @return A Blob instance containing the same data as this Blob. + */ + public abstract Blob toBlob(); + + /** + * Computes the length of the longest common hex prefix between two blobs + * + * @param b Blob to compare with + * @return The length of the longest common prefix in hex digits + */ + public abstract long commonHexPrefixLength(ABlob b); + + /** + * Computes the hash of the byte data stored in this Blob, using the default MessageDigest. + * + * This is the correct hash ID for a data value if this blob contains the data value's encoding + * + * @return The Hash + */ + public final Hash getContentHash() { + if (contentHash == null) { + contentHash = computeHash(Hashing.getDigest()); + } + return contentHash; + } + + /** + * Computes the hash of the byte data stored in this Blob, using the given MessageDigest. + * + * @param digest MessageDigest instance + * @return The hash + */ + public final Hash computeHash(MessageDigest digest) { + updateDigest(digest); + return Hash.wrap(digest.digest()); + } + + protected abstract void updateDigest(MessageDigest digest); + + /** + * Gets the byte at the specified position in this blob + * + * @param i Index of the byte to get + * @return The byte at the specified position + */ + public byte byteAt(long i) { + if ((i < 0) || (i >= count())) { + throw new IndexOutOfBoundsException("Index: " + i); + } + return getUnchecked(i); + } + + /** + * Gets the byte at the specified position in this data object, without bounds checking. + * Only safe if index is known to be in bounds, otherwise result is undefined. + * + * @param i Index of the byte to get + * @return The byte at the specified position + */ + public abstract byte getUnchecked(long i); + + /** + * Gets the specified hex digit from this data object. + * + * Result is undefined if index is out of bounds. + * + * @param digitPos The position of the hex digit + * @return The value of the hex digit, in the range 0-15 inclusive + */ + public int getHexDigit(long digitPos) { + byte b = getUnchecked(digitPos >> 1); + //if ((digitPos & 1) == 0) { + // return (b >> 4) & 0x0F; // first hex digit + //} else { + // return b & 0x0F; // second hex digit + //} + int shift = 4-(((int)digitPos&1)<<2); + return (b>>shift)&0x0F; + } + + /** + * Gets a byte array containing a copy of this data object. + * + * @return A new byte array containing the contents of this blob. + */ + public byte[] getBytes() { + byte[] result = new byte[Utils.checkedInt(count())]; + getBytes(result, 0); + return result; + } + + /** + * Append an additional data object to this, creating a new data object. + * + * @param d Blob to append + * @return A new blob, containing the additional data appended to this blob. + */ + public abstract ABlob append(ABlob d); + + /** + * Determines if this Blob is equal to another Object. + * + * Blobs are defined to be equal if they have the same on-chain representation, + * i.e. if and only if all of the following are true: + * + * - Blob is of the same general type - Blobs are of the same length - All byte + * values are equal + */ + @Override + public final boolean equals(ACell o) { + if (!(o instanceof ABlob)) return false; + return equals((ABlob)o); + } + + /** + * Determines if this Blob is equal to another Blob. + * + * Blobs are defined to be equal if they have the same on-chain representation, + * i.e. if and only if all of the following are true: + * + * - Blob is of the same general type - Blobs are of the same length - All byte + * values are equal + * + * @param o Blob to compare with + * @return true if Blobs are equal, false otherwise + */ + public abstract boolean equals(ABlob o); + + @Override + public abstract ABlob toCanonical(); + + /** + * Tests if this Blob is equal to a subset of a byte array + * @param bytes Byte array to compare with + * @param byteOffset Offset into byte array + * @return true if exactly equal, false otherwise + */ + public abstract boolean equalsBytes(byte[] bytes, int byteOffset); + + /** + * Compares this blob to another blob, in lexographic order sorting by first + * bytes. + * + * Note: This means that compareTo does not precisely match equality, because + * different blob types may be lexicographically equal but represent different values. + */ + @Override + public int compareTo(ABlob b) { + if (this == b) return 0; + long alength = this.count(); + long blength = b.count(); + long compareLength = Math.min(alength, blength); + for (long i = 0; i < compareLength; i++) { + int c = (0xFF & getUnchecked(i)) - (0xFF & b.getUnchecked(i)); + if (c > 0) return 1; + if (c < 0) return -1; + } + if (alength > compareLength) return 1; // this is bigger + if (blength > compareLength) return -1; // b is bigger + return 0; + } + + /** + * Writes the raw byte contents of this blob to a ByteBuffer. + * + * @param bb ByteBuffer to write to + * @return The passed ByteBuffer, after writing byte content + */ + public abstract ByteBuffer writeToBuffer(ByteBuffer bb); + + /** + * Writes the raw byte contents of this blob to a byte array + * + * @param bs Byte array to write to + * @param pos Starting position in byte array to write to + * @return The position in the array after writing + */ + public abstract int writeToBuffer(byte[] bs, int pos); + + /** + * Gets a chunk of this Blob, as a canonical Blob up to the maximum chunk size + * + * @param i Index of chunk + * @return A Blob containing the specified chunk data. + */ + public abstract Blob getChunk(long i); + + @Override + public void print(StringBuilder sb) { + sb.append("0x"); + toHexString(sb); + } + + /** + * Gets a byte buffer containing this Blob's data. Will have remaining bytes + * equal to this Blob's size. + * + * @return A ByteBuffer containing the Blob's data. + */ + public abstract ByteBuffer getByteBuffer(); + + public abstract void toHexString(StringBuilder sb); + + @Override + public void validate() throws InvalidDataException { + super.validate(); + } + + @Override + public void validateCell() throws InvalidDataException { + if (count() < 0) throw new InvalidDataException("Negative blob length", this); + } + + /** + * Returns the number of matching hex digits in the given hex range of another blob. Assumes + * range is valid for both blobs. + * + * Returns length if this Blob is exactly equal to the specified hex range. + * + * @param start Start position (in hex digits) + * @param length Length to compare (in hex digits) + * @param b Blob to compare with + * @return The number of matching hex characters + */ + public abstract long hexMatchLength(ABlob b, long start, long length); + + public boolean hexEquals(ABlob b) { + long c = count(); + if (b.count() != c) return false; + return hexMatchLength(b, 0L, c) == c; + } + + public boolean hexEquals(ABlob b, long start, long length) { + return hexMatchLength(b, start, length) == length; + } + + public long hexLength() { + return count() << 1; + } + + /** + * Converts this Blob to the corresponding long value. + * + * Assumes big-endian format, as if the entire blob is interpreted as a signed big integer. Higher bytes + * outside the Long range will be ignored. + * + * @return long value of this blob + */ + public abstract long toLong(); + + @Override + public int hashCode() { + // note: We use the Java hashcode of the last bytes for blobs + return Long.hashCode(toLong()); + } + + /** + * Gets the long value of this Blob if the length is exactly 8 bytes, otherwise + * throws an Exception + * + * @return The long value represented by the Blob + */ + public abstract long longValue(); + + /** + * Returns true if this object is a regular blob (i.e. not a special blob type like Hash or Address) + * @return True if a regular blob + */ + public boolean isRegularBlob() { + return getTag()==Tag.BLOB; + } + + /** + * Tests if this Blob has exactly the same bytes as another Blob + * @param b Blob to compare with + * @return True if byte content is exactly equal, false otherwise + */ + public abstract boolean equalsBytes(ABlob b); + +} diff --git a/convex-core/src/main/java/convex/core/data/ABlobMap.java b/convex-core/src/main/java/convex/core/data/ABlobMap.java new file mode 100644 index 000000000..9c8d9747d --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/ABlobMap.java @@ -0,0 +1,115 @@ +package convex.core.data; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.TODOException; + +/** + * Abstract base class for a sorted radix-tree map of Blobs to values. + * + * Primary benefits: - Provide sorted orderings for indexes - Support Schedule + * data structure + * + * @param Type of BlobMap keys + * @param Type of BlobMap values + */ +public abstract class ABlobMap extends AMap { + protected ABlobMap(long count) { + super(count); + } + + @Override + public final V get(ACell key) { + if (!(key instanceof ABlob)) return null; + return get((ABlob) key); + } + + @Override + public boolean containsKey(ACell key) { + if (!(key instanceof ABlob)) return false; + return (getEntry((ABlob) key) != null); + } + + /** + * Gets the map entry for a given blob + * + * @param key Key to lookup up + * @return The value specified by the given blob key or null if not present. + */ + public abstract V get(ABlob key); + + @Override + public boolean equalsKeys(AMap map) { + // TODO: probably not needed? Only need this for set implementations + throw new TODOException(); + } + + @Override + public Set> entrySet() { + HashSet> hs=new HashSet<>(size()); + long n=count(); + for (long i=0; i me=entryAt(i); + hs.add(me); + } + return Collections.unmodifiableSet(hs); + } + + @Override + public abstract int getRefCount(); + + @Override + public abstract Ref getRef(int i); + + @Override + public boolean isCanonical() { + return true; + } + + @Override + public AType getType() { + return Types.BLOBMAP; + } + + /** + * Associates a blob key with a value in this data structure. + * + * Returns null if the key is not a valid BlobMap key + */ + @Override + public abstract ABlobMap assoc(ACell key, ACell value); + + @SuppressWarnings("unchecked") + @Override + public final ABlobMap dissoc(ACell key) { + if (key instanceof ABlob) { + return dissoc((K)key); + } + return this; + } + + public abstract ABlobMap dissoc(K key); + + @Override + public MapEntry getKeyRefEntry(Ref ref) { + return getEntry(ref.getValue()); + } + + @Override + public abstract MapEntry entryAt(long i); + + public MapEntry getEntry(ACell key) { + if (key instanceof ABlob) return getEntry((ABlob)key); + return null; + } + + public abstract MapEntry getEntry(ABlob key); + + @Override + public abstract int estimatedEncodingSize(); + +} diff --git a/convex-core/src/main/java/convex/core/data/ACell.java b/convex-core/src/main/java/convex/core/data/ACell.java new file mode 100644 index 000000000..a5d3eaa6f --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/ACell.java @@ -0,0 +1,495 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.util.function.Consumer; + +import convex.core.Constants; +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.InvalidDataException; +import convex.core.exceptions.TODOException; +import convex.core.store.AStore; +import convex.core.store.Stores; +import convex.core.util.Utils; + +/** + * Abstract base class for Cells. + * + * Cells may contain Refs to other Cells, which can be tested with getRefCount() + * + * All data objects intended for on-chain usage serialisation should extend this. The only + * exceptions are data objects which are Embedded (inc. certain JVM types like Long etc.) + * + * "It is better to have 100 functions operate on one data structure than + * to have 10 functions operate on 10 data structures." - Alan Perlis + */ +public abstract class ACell extends AObject implements IWriteable, IValidated { + + + /** + * An empty Java array of cells + */ + public static final ACell[] EMPTY_ARRAY = new ACell[0]; + + /** + * We cache the computed memorySize. May be 0 for embedded objects + * -1 is initial value for when size is not calculated + */ + private long memorySize=-1; + + /** + * Cached Ref. This is useful to manage persistence + */ + protected Ref cachedRef=null; + + @Override + public void validate() throws InvalidDataException { + validateCell(); + } + + /** + * Validates the local structure and invariants of this cell. Called by validate() super implementation. + * + * Should validate directly contained data, but should not validate all other structure of this cell. + * + * In particular, should not traverse potentially missing child Refs. + * + * @throws InvalidDataException If the Cell is invalid + */ + public abstract void validateCell() throws InvalidDataException; + + /** + * Hash of data Encoding of this cell, equivalent to the Value ID. Calling this method + * may force hash computation if needed. + * + * @return The Hash of this cell's encoding. + */ + public final Hash getHash() { + // final method to avoid any mistakes. + return getEncoding().getContentHash(); + } + + /** + * Gets the tag byte for this cell. The tag byte will be the first byte of the encoding + * @return Tag byte for this Cell + */ + public abstract byte getTag(); + + /** + * Gets the Hash if already computed, or null if not yet available + * @return Cached Hash value, or null if not available + */ + protected final Hash cachedHash() { + if (cachedRef!=null) { + Hash h=cachedRef.cachedHash(); + if (h!=null) return h; + } + if (encoding==null) return null; + return encoding.contentHash; + } + + /** + * Gets the Java hashCode for this cell. Must be consistent with equals. + * + * Default is the first bytes (big-endian) of the Cell Encoding's hash, since this is consistent with + * encoding-based equality. However, different Types may provide more efficient hashcodes provided that + * the usual invariants are preserved + * + * @return integer hash code. + */ + @Override + public int hashCode() { + return getHash().firstInt(); + } + + @Override + public final boolean equals(Object a) { + if (!(a instanceof ACell)) return false; + return equals((ACell)a); + } + + /** + * Gets the encoded byte representation of this cell. + * + * @return A blob representing this cell in encoded form + */ + public final Blob getEncoding() { + if (encoding==null) encoding=createEncoding(); + return encoding; + } + + /** + * Checks for equality with another object. In general, data objects should be considered equal + * if they have the same canonical representation, i.e. an identical encoding with the same hash value. + * + * Subclasses should override this if they have a more efficient equals implementation. + * + * @param a Cell to compare with. May be null?? + * @return True if this cell is equal to the other object + */ + public boolean equals(ACell a) { + if (this==a) return true; // important optimisation for e.g. hashmap equality + if (a==null) return false; + if (!(a.getTag()==this.getTag())) return false; + return getEncoding().equals(a.getEncoding()); + } + + /** + * Writes this Cell's encoding to a ByteBuffer, including a tag byte which will be written first + * + * @param bb A ByteBuffer to which to write the encoding + * @return The passed ByteBuffer, after the representation of this object has been written. + */ + @Override + public final ByteBuffer write(ByteBuffer bb) { + return getEncoding().writeToBuffer(bb); + } + + /** + * Writes this Cell's encoding to a byte array, including a tag byte which will be written first + * + * @param bs A byte array to which to write the encoding + * @param pos The offset into the byte array + * + * @return New position after writing + */ + public abstract int encode(byte[] bs, int pos); + + /** + * Writes this Cell's encoding to a byte array, excluding the tag byte + * + * @param bs A byte array to which to write the encoding + * @param pos The offset into the byte array + * @return New position after writing + */ + public abstract int encodeRaw(byte[] bs, int pos); + + @Override + public final Blob createEncoding() { + int capacity=estimatedEncodingSize(); + byte[] bs=new byte[capacity]; + int pos=0; + boolean done=false; + while (!done) { + try { + pos=encode(bs,pos); + done=true; + } catch (IndexOutOfBoundsException be) { + // We really want to eliminate these, because exception handling is expensive + // System.out.println("Insufficient encoding size: "+capacity+ " for "+this.getClass()); + capacity=capacity*2+10; + bs=new byte[capacity]; + } + } + return Blob.wrap(bs,0,pos); + } + + /** + * Returns the String representation of this Cell. + * + * The String representation is intended to be a easy-to-read textual representation of the Cell's data content. + * + */ + @Override + public String toString() { + StringBuilder sb=new StringBuilder(); + print(sb); + return sb.toString(); + } + + /** + * Gets the cached blob representing this Cell's Encoding in binary format, if it exists. + * + * @return The cached blob for this cell, or null if not available. + */ + public ABlob cachedEncoding() { + return encoding; + } + + /** + * Calculates the Memory Size for this Cell. + * + * Requires any child Refs to be either Direct or of persisted status at minimum, + * or you might get a MissingDataException + * + * @return Memory Size of this Cell + */ + protected long calcMemorySize() { + // add size for each child Ref (might be zero if embedded) + long result=0; + int n=getRefCount(); + for (int i=0; i childRef=getRef(i); + long childSize=childRef.getMemorySize(); + result+=childSize; + } + + if (!isEmbedded()) { + // We need to count this cell's own encoding length + result+=getEncodingLength(); + + // Add overhead for storage of non-embedded cell + result+=Constants.MEMORY_OVERHEAD; + } + return result; + } + + /** + * Method to calculate the encoding length of a Cell. May be overridden to avoid + * creating encodings during memory size calculations. This reduces hashing! + * + * @return Exact encoding length of this Cell + */ + public long getEncodingLength() { + return getEncoding().count(); + } + + /** + * Gets the Memory Size of this Cell, computing it if required. + * + * The memory size is the total storage requirement for this cell. Embedded cells do not require storage for + * their own encoding, but may require storage for nested non-embedded Refs. + * + * @return Memory Size of this Cell + */ + public final long getMemorySize() { + long ms=memorySize; + if (ms>=0) return ms; + ms=calcMemorySize(); + this.memorySize=ms; + return ms; + } + + /** + * Determines if this Cell Represents an embedded object. Embedded objects are encoded directly into + * the encoding of the containing Cell (avoiding the need for a hashed reference). + * + * Subclasses should override this if they have a cheap O(1) + * way to determine if they are embedded or otherwise. + * + * @return true if Cell is embedded, false otherwise + */ + public boolean isEmbedded() { + if (cachedRef!=null) { + int flags=cachedRef.flags; + if ((flags&Ref.KNOWN_EMBEDDED_MASK)!=0) return true; + if ((flags&Ref.NON_EMBEDDED_MASK)!=0) return false; + } + boolean embedded= getEncodingLength()<=Format.MAX_EMBEDDED_LENGTH; + if (cachedRef!=null) { + cachedRef.flags|=(embedded)?Ref.KNOWN_EMBEDDED_MASK:Ref.NON_EMBEDDED_MASK; + } + return embedded; + } + + /** + * Returns true if this Cell is in a canonical format for message writing. + * Reading or writing a non-canonical value should be considered illegal, but + * non-canonical objects may be used on a temporary internal basis. + * + * @return true if the object is in canonical format, false otherwise + */ + public abstract boolean isCanonical(); + + /** + * Converts this Cell to its canonical version. Returns this if already canonical + * + * @return Canonical version of Cell + */ + public abstract ACell toCanonical(); + + /** + * Returns true if this object represents a first class CVM Value. Sub-structural cells that are not themselves first class values + * should return false. + * + * CVM values might not be in a canonical format, e.g. temporary data structures + * + * @return true if the object is a CVM Value, false otherwise + */ + public abstract boolean isCVMValue(); + + /** + * Gets the number of Refs contained within this Cell. This number is + * final / immutable for any given instance. + * + * Contained Refs may be either external or embedded. + * + * @return The number of Refs in this Cell + */ + public abstract int getRefCount(); + + /** + * Gets the Ref for this Cell, creating a new direct reference if necessary + * + * @param Type of Cell + * @return Ref for this Cell + */ + @SuppressWarnings("unchecked") + public final Ref getRef() { + if (cachedRef!=null) return (Ref) cachedRef; + return createRef(); + } + + /** + * Creates a new Ref for this Cell + * @param Type of Cell + * @return New Ref instance + */ + @SuppressWarnings("unchecked") + protected Ref createRef() { + Ref newRef= RefDirect.create(this,cachedHash()); + cachedRef=newRef; + return (Ref) newRef; + } + + /** + * Gets a numbered child Ref from within this Cell. + * + * @param Type of referenced Cell + * @param i Index of ref to get + * @return The Ref at the specified index + */ + public Ref getRef(int i) { + // This will always throw an error if not overridden. Provided for warning purposes if accidentally used. + if (getRefCount()==0) { + throw new IndexOutOfBoundsException("No Refs to get in "+Utils.getClassName(this)); + } else { + throw new TODOException(Utils.getClassName(this) +" does not yet implement getRef(i) for i = "+i); + } + } + + /** + * Updates all Refs in this object using the given function. + * + * The function *must not* change the hash value of Refs, in order to ensure + * structural integrity of modified data structures. + * + * This is a building block for a very sneaky trick that enables use to do a lot + * of efficient operations on large trees of smart references. + * + * Must return the same object if no Refs are altered. + * @param func Ref update function + * @return Cell with updated Refs + */ + public ACell updateRefs(IRefFunction func) { + if (getRefCount()==0) return this; + throw new TODOException(Utils.getClassName(this) +" does not yet implement updateRefs(...)"); + } + + /** + * Gets an array of child Refs for this Cell, in the same order as order accessible by + * getRef. + * + * Concrete implementations may override this to optimise performance. + * + * @param Type of referenced Cell + * @return Array of Refs + */ + @SuppressWarnings("unchecked") + public Ref[] getChildRefs() { + int n = getRefCount(); + Ref[] refs = new Ref[n]; + for (int i = 0; i < n; i++) { + refs[i] = getRef(i); + } + return refs; + } + + /** + * Gets the most specific known runtime Type for this Cell. + * @return The Type of this Call + */ + public AType getType() { + return Types.ANY; + } + + /** + * Updates the memorySize of this Cell + * + * Not valid for embedded Cells, may throw IllegalOperationException() + * + * @param memorySize Memory size to assign + */ + public void attachMemorySize(long memorySize) { + if (this.memorySize<0) { + this.memorySize=memorySize; + assert (this.memorySize>0) : "Attempting to attach memory size "+memorySize+" to object of class "+Utils.getClassName(this); + } else { + assert (this.memorySize==memorySize) : "Attempting to attach memory size "+memorySize+" to object of class "+Utils.getClassName(this)+" which already has memorySize "+this.memorySize; + } + } + + /** + * Updates the cached ref of this Cell + * + * @param ref Ref to assign + */ + @SuppressWarnings("unchecked") + public void attachRef(Ref ref) { + this.cachedRef=(Ref) ref; + } + + /** + * Creates an ANNOUNCED Ref with the given value in the current store. + * + * Novelty handler is called for all new Refs that are persisted (recursively), + * starting from lowest levels. + * + * @param Type of Value + * @param value Value to announce + * @param noveltyHandler Novelty handler to call for any Novelty (may be null) + * @return Persisted Ref + */ + public static T createAnnounced(T value, Consumer> noveltyHandler) { + if (value==null) return null; + return value.announce(noveltyHandler); + } + + public T announce() { + return announce(null); + } + + @SuppressWarnings("unchecked") + public T announce(Consumer> noveltyHandler) { + Ref ref = getRef(); + AStore store=Stores.current(); + ref= store.storeTopRef(ref, Ref.ANNOUNCED,noveltyHandler); + cachedRef=ref; + return (T) this; + } + + + /** + * Creates a persisted Ref with the given value in the current store. + * + * Novelty handler is called for all new Refs that are persisted (recursively), + * starting from lowest levels (depth first order) + * + * @param Type of Value + * @param value Any CVM value to persist + * @param noveltyHandler Novelty handler to call for any Novelty (may be null) + * @return Persisted Ref + */ + @SuppressWarnings("unchecked") + public static Ref createPersisted(T value, Consumer> noveltyHandler) { + Ref ref = Ref.get(value); + if (ref.isPersisted()) return ref; + AStore store=Stores.current(); + ref = (Ref) store.storeTopRef(ref, Ref.PERSISTED,noveltyHandler); + value.cachedRef=(Ref)ref; + return ref; + } + + /** + * Creates a persisted Ref with the given value in the current store. Returns + * the current Ref if already persisted + * + * @param Type of Value + * @param value Any CVM value to persist + * @return Ref to the given value + */ + public static Ref createPersisted(T value) { + return createPersisted(value, null); + } + +} diff --git a/convex-core/src/main/java/convex/core/data/ACollection.java b/convex-core/src/main/java/convex/core/data/ACollection.java new file mode 100644 index 000000000..5827ae1fd --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/ACollection.java @@ -0,0 +1,211 @@ +package convex.core.data; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.function.Function; + +import convex.core.data.type.AType; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * Abstract base class for Persistent Merkle Collections + * + *

+ * A Collection is a data structure that contains zero or more elements. Possible collection subtypes include: + *

+ *
    + *
  • Sequential collections (Lists, Vectors)
  • + *
  • Sets (with unique elements)
  • + *
+ * + * @param Type of elements in this collection + */ +public abstract class ACollection extends ADataStructure implements Collection { + + protected ACollection(long count) { + super(count); + } + + @Override + public abstract AType getType(); + + @Override + public abstract int encode(byte[] bs, int pos); + + @Override + public abstract boolean contains(Object o); + + @Override + public Iterator iterator() { + return new BasicIterator(0); + } + + /** + * Custom ListIterator for ListVector + */ + private class BasicIterator implements ListIterator { + long pos; + + public BasicIterator(long index) { + if (index < 0L) throw new IndexOutOfBoundsException((int)index); + + long c = count(); + if (index > c) throw new IndexOutOfBoundsException((int)index); + pos = index; + } + + @Override + public boolean hasNext() { + return pos < count(); + } + + @Override + public T next() { + return get(pos++); + } + + @Override + public boolean hasPrevious() { + if (pos > 0) return true; + return false; + } + + @Override + public T previous() { + if (pos > 0) return get(--pos); + throw new NoSuchElementException(); + } + + @Override + public int nextIndex() { + return Utils.checkedInt(pos); + } + + @Override + public int previousIndex() { + return Utils.checkedInt(pos - 1); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public void set(T e) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public void add(T e) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + } + + @Override + public final boolean add(T e) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public final boolean remove(Object o) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public boolean containsAll(Collection c) { + HashSet h=new HashSet(this.size()); + h.addAll(this); + for (Object o: c) { + if (!h.contains(o)) return false; + } + return true; + } + + @Override + public final boolean addAll(Collection c) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public final boolean removeAll(Collection c) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public final boolean retainAll(Collection c) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public final void clear() { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + /** + * Converts this collection to a canonical vector of elements + * @return This collection coerced to a vector + */ + public abstract AVector toVector(); + + /** + * Copies the elements of this collection in order to an array at the specified offset + * + * @param Type of array elements required + * @param arr + * @param offset + */ + protected abstract void copyToArray(R[] arr, int offset); + + /** + * Converts this collection to a new Cell array + * @return A new cell array containing the elements of this sequence + */ + public ACell[] toCellArray() { + int n=Utils.checkedInt(count()); + ACell[] cells=new ACell[n]; + int i=0; + for (ACell cell: this) { + cells[i++]=cell; + } + return cells; + } + + @SuppressWarnings("unchecked") + @Override + public V[] toArray(V[] a) { + int s = size(); + if (s > a.length) { + Class c = (Class) a.getClass().getComponentType(); + a = (V[]) Array.newInstance(c, s); + } + copyToArray(a, 0); + if (s < a.length) a[s] = null; + return a; + } + + /** + * Adds an element to this collection, according to the natural semantics of the collection + * @param x Value to add + * @return The updated collection + */ + @Override + public abstract ACollection conj(R x); + + /** + * Maps a function over a collection, applying it to each element in turn. + * + * @param Type of element in resulting collection + * @param mapper Function to map over collection + * @return Collection after function applied to each element + */ + public abstract ACollection map(Function mapper); + + +} diff --git a/convex-core/src/main/java/convex/core/data/ACountable.java b/convex-core/src/main/java/convex/core/data/ACountable.java new file mode 100644 index 000000000..ef31c7c32 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/ACountable.java @@ -0,0 +1,56 @@ +package convex.core.data; + +/** + * Abstract base class for Countable objects. + * + * Countable values support a count of sub-elements and the ability to get by index. + * + * @param Type of element that is counted + */ +public abstract class ACountable extends ACell { + + /** + * Returns the number of elements in this data structure + * + * @return Number of elements in this collection. + */ + public abstract long count(); + + /** + * Gets the element at the specified index in this collection + * + * @param index Index of element to get + * @return Element at the specified index + */ + public abstract E get(long index); + + + /** + * Gets a Ref to the element at the specified index in this collection + * + * @param index Index of element to get + * @return Element at the specified index + */ + public abstract Ref getElementRef(long index); + + /** + * Checks if this data structure is empty, i.e. has a count of zero elements. + * + * @return true if this data structure is empty, false otherwise + */ + public boolean isEmpty() { + return count() == 0L; + } + + /** + * Gets the size of this data structure as an int. + * + * Returns Integer.MAX_SIZE if the count is larger than can fit in an int. If + * this might be a problem, use count() instead. + * + * @return Number of elements in this collection. + */ + public int size() { + return (int) (Math.min(count(), Integer.MAX_VALUE)); + } +} diff --git a/convex-core/src/main/java/convex/core/data/ADataStructure.java b/convex-core/src/main/java/convex/core/data/ADataStructure.java new file mode 100644 index 000000000..5382a9179 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/ADataStructure.java @@ -0,0 +1,121 @@ +package convex.core.data; + +/** + * Abstract base class for Persistent data structures. Each can be regarded as a + * countable, immutable collection of elements. + * + * Data structures in general support: + *
    + *
  • Immutability
  • + *
  • Addition of an element(s) of appropriate type
  • + *
  • Construction of an empty (zero) element
  • + *
+ * + *

+ * "When you know your data can never change out from underneath you, everything + * is different." - Rich Hickey + *

+ * + * @param Type of Data Structure elements + */ +public abstract class ADataStructure extends ACountable { + + protected final long count; + + protected ADataStructure(long count) { + this.count=count; + } + + /** + * Gets the count of elements in this data structure + */ + @Override + public final long count() { + return count; + } + + @Override + public final int size() { + return (int) (Math.min(count, Integer.MAX_VALUE)); + } + + /** + * Returns an empty instance of the same general type as this data structure. + * + * @return An empty data structure + */ + public abstract ADataStructure empty(); + + @Override + public final boolean isEmpty() { + return count==0L; + } + + /** + * Adds an element to this data structure, in the natural manner defined by the + * general data structure type. e.g. append at the end of a vector. + * + * @param Type of Value added + * @param x New element to add + * @return The updated data structure, or null if a failure occurred due to invalid element type + */ + public abstract ADataStructure conj(R x); + + /** + * Adds multiple elements to this data structure, in the natural manner defined by the + * general data structure type. e.g. append at the end of a vector. + * + * This may be more efficient than using 'conj' for individual items. + * + * @param Type of Value added + * @param xs New elements to add + * @return The updated data structure, or null if a failure occurred due to invalid elementtypes + */ + @SuppressWarnings("unchecked") + public ADataStructure conjAll(ACollection xs) { + ADataStructure result=(ADataStructure) this; + for (R x: xs) { + result=result.conj(x); + if (result==null) return null; + } + return result; + } + + /** + * Associates a key with a value in this associative data structure. + * + * May return null if the Key or Value is incompatible with the data structure. + * + * @param key Associative key + * @param value Value to associate with key + * @return Updates data structure, or null if data types are invalid + */ + public abstract ADataStructure assoc(ACell key,ACell value); + + /** + * Get the value associated with a given key. + * + * @param key Associative key to look up + * @return Value from collection, or a falsey value (null or false) if not found + */ + public abstract ACell get(ACell key); + + /** + * Get the value associated with a given key. + * + * @param key Key to look up in data structure + * @param notFound Value to return if key is not found + * @return Value from collection, or notFound value if not found + */ + public abstract ACell get(ACell key, ACell notFound); + + + /** + * Checks if the data structure contains the specified key + * + * @param key Associative key to look up + * @return true if the data structure contains the key, false otherwise + */ + public abstract boolean containsKey(ACell key); + +} diff --git a/convex-core/src/main/java/convex/core/data/AHashMap.java b/convex-core/src/main/java/convex/core/data/AHashMap.java new file mode 100644 index 000000000..7727f1190 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/AHashMap.java @@ -0,0 +1,171 @@ +package convex.core.data; + +import java.util.function.Function; +import java.util.function.Predicate; + +import convex.core.data.type.Types; +import convex.core.exceptions.InvalidDataException; +import convex.core.util.MergeFunction; +import convex.core.util.Utils; + +public abstract class AHashMap extends AMap { + + protected AHashMap(long count) { + super(count); + } + + @Override + public AHashMap empty() { + return Maps.empty(); + } + + /** + * Dissoc given a Ref to the key value. + * @param key Ref of key to remove + * @return Map with specified key removed. + */ + public abstract AHashMap dissocRef(Ref key); + + public abstract AHashMap assocRef(Ref keyRef, V value); + + @Override + public abstract AHashMap assoc(ACell key, ACell value); + + @Override + public abstract AHashMap dissoc(ACell key); + + protected abstract AHashMap assocRef(Ref keyRef, V value, int shift); + + public abstract AHashMap assocEntry(MapEntry e); + + protected abstract AHashMap assocEntry(MapEntry e, int shift); + + /** + * Merge another map into this map. Replaces existing entries if they are + * different + * + * O(n) in size of map to merge. + * + * @param m HashMap to merge into this HashMap + * @return Merged HashMap + */ + public AHashMap merge(AHashMap m) { + AHashMap result = this; + long n = m.count(); + for (int i = 0; i < n; i++) { + result = result.assocEntry(m.entryAt(i)); + } + return result; + } + + /** + * Merge this map with another map, using the given function for each key that + * is present in either map and has a different value + * + * The function is passed null for missing values in either map, and must return + * type V. + * + * If the function returns null, the entry is removed. + * + * Returns the same map if no changes occurred. + * + * @param b Other map to merge with + * @param func Merge function, returning a new value for each key + * @return A merged map, or this map if no changes occurred + */ + public abstract AHashMap mergeDifferences(AHashMap b, MergeFunction func); + + protected abstract AHashMap mergeDifferences(AHashMap b, MergeFunction func, int shift); + + /** + * Merge this map with another map, using the given function for each key that + * is present in either map. The function is applied to the corresponding values + * with the same key. + * + * The function is passed null for missing values in either map, and must return + * type V. + * + * If the function returns null, the entry is removed. + * + * Returns the same map if no changes occurred. + * + * PERF WARNING: This method's contract requires calling the function on all + * values in both sets, which will cause a full data structure traversal. If the + * function will only return one or other of the compared values consider using + * mergeDifferences instead. + * + * @param b Other map to merge with + * @param func Merge function, returning a new value for each key + * @return A merged map, or this map if no changes occurred + */ + public abstract AHashMap mergeWith(AHashMap b, MergeFunction func); + + protected abstract AHashMap mergeWith(AHashMap b, MergeFunction func, int shift); + + @Override + public AHashMap filterValues(Predicate pred) { + // TODO make more efficient? + return mergeWith(this, (a, b) -> pred.test(a) ? a : null); + } + + @Override + public boolean equals(AMap a) { + if (this == a) return true; // important optimisation for e.g. hashmap equality + if (a == null) return false; + if (a.getType()!=Types.MAP) return false; + long n=this.count(); + if (n != a.count()) return false; + return getHash().equals(a.getHash()); + } + + /** + * Maps a function over all entries in this Map to produce updated entries. + * + * May not change keys, but may return null to remove an entry. + * + * @param func A function that maps old map entries to updated map entries. + * @return The updated Map, or this Map if no changes + */ + public abstract AHashMap mapEntries(Function, MapEntry> func); + + /** + * Validates the map with a given hex prefix. This is necessary to ensure that + * child maps are valid, in particular have the correct shift level and that all + * key hashes start with the correct prefix of hex characters. + * + * TODO: consider faster way of passing prefix than hex string, probably a + * byte[] stack. + * + * @param string + * @throws InvalidDataException + */ + protected abstract void validateWithPrefix(String string) throws InvalidDataException; + + @Override + public abstract AHashMap updateRefs(IRefFunction func); + + /** + * Returns true if this map contains all the same keys as another map + * @param map Map to compare with + * @return True if this map contains all the keys of the other + */ + public abstract boolean containsAllKeys(AHashMap map); + + /** + * Writes this HashMap to a byte array. Will include values by default. + * @param bs Byte array to encode into + * @param pos Start position to encode at + * @return Updated position + */ + public abstract int encode(byte[] bs, int pos); + + public AVector getKeys() { + int n=Utils.checkedInt(count); + ACell[] keys=new ACell[n]; + for (int i=0; i extends ASet { + + protected static final int OP_UNION=1; + protected static final int OP_INTERSECTION=2; + protected static final int OP_DIFF_LEFT=3; + protected static final int OP_DIFF_RIGHT=4; + + protected static final int MAX_SHIFT = Hash.LENGTH*2-1; + + + protected AHashSet(long count) { + super(count); + } + + protected abstract AHashSet mergeWith(AHashSet b, int setOp); + + protected abstract AHashSet mergeWith(AHashSet b, int setOp, int shift); + + @SuppressWarnings("unchecked") + public ASet includeAll(ASet elements) { + return (ASet) mergeWith((AHashSet) elements,OP_UNION); + }; + + protected final int reverseOp(int setOp) { + if (setOp>=OP_DIFF_LEFT) { + setOp=OP_DIFF_LEFT+OP_DIFF_RIGHT-setOp; + } + return setOp; + } + + protected final Ref applyOp(int setOp, Ref a, Ref b) { + switch (setOp) { + case OP_UNION: return (a==null)?b:a; + case OP_INTERSECTION: return (a==null)?null:((b==null)?null:a); + case OP_DIFF_LEFT: return (a==null)?null:((b==null)?a:null); + case OP_DIFF_RIGHT: return (b==null)?null:((a==null)?b:null); + default: throw new Error("Invalid setOp: "+setOp); + } + } + + protected final AHashSet applySelf(int setOp) { + switch (setOp) { + case OP_UNION: return this; + case OP_INTERSECTION: return this; + case OP_DIFF_LEFT: return Sets.empty(); + case OP_DIFF_RIGHT: return Sets.empty(); + default: throw new Error("Invalid setOp: "+setOp); + } + } + + public ASet intersectAll(ASet elements) { + return mergeWith((AHashSet) elements,OP_INTERSECTION); + }; + + public ASet excludeAll(ASet elements) { + return mergeWith((AHashSet) elements,OP_DIFF_LEFT); + }; + + public abstract AHashSet toCanonical(); + + public ASet conjAll(ACollection elements) { + if (elements instanceof AHashSet) return includeAll((AHashSet) elements); + @SuppressWarnings("unchecked") + AHashSet result=(AHashSet) this; + long n=elements.count(); + for (long i=0; i disjAll(ACollection b) { + if (b instanceof AHashSet) return excludeAll((AHashSet) b); + AHashSet result=this; + long n=b.count(); + for (long i=0; i excludeRef(Ref valueRef); + + public abstract AHashSet includeRef(Ref ref) ; + + @SuppressWarnings("unchecked") + @Override + public AHashSet conj(R a) { + return (AHashSet) includeRef((Ref) Ref.get(a)); + } + + @Override + public ASet exclude(T a) { + return excludeRef((Ref) Ref.get(a)); + } + + @SuppressWarnings("unchecked") + @Override + public AHashSet include(R a) { + return (AHashSet) includeRef((Ref) Ref.get(a)); + } + + /** + * Validates the set with a given hex prefix. This is necessary to ensure that + * child maps are valid, in particular have the correct shift level and that all + * hashes start with the correct prefix of hex characters. + * + * @param prefix Hash for earlier prefix values + * @param digit Hex digit expected at position [shift] + * @throws InvalidDataException + */ + protected abstract void validateWithPrefix(Hash prefix, int digit, int shift) throws InvalidDataException; + + @Override + public Object[] toArray() { + int s = size(); + Object[] result = new Object[s]; + copyToArray(result, 0); + return result; + } + + @Override + public final CVMBool get(ACell key) { + Ref me = getValueRef(key); + if (me == null) return CVMBool.FALSE; + return CVMBool.TRUE; + } + + /** + * Gets the Value in the set for the given hash, or null if not found + * @param hash Hash of value to check in set + * @return The Value for the given Hash if found, null otherwise. + */ + public T getByHash(Hash hash) { + Ref ref=getRefByHash(hash); + if (ref==null) return null; + return ref.getValue(); + } + + protected abstract AHashSet includeRef(Ref e, int i); + + /** + * Tests if this Set contains a given hash + * @param hash Hash to test for set membership + * @return True if set contains value for given hash, false otherwise + */ + public abstract boolean containsHash(Hash hash); + + @Override + public boolean contains(ACell key) { + return getValueRef(key) != null; + } +} diff --git a/convex-core/src/main/java/convex/core/data/AList.java b/convex-core/src/main/java/convex/core/data/AList.java new file mode 100644 index 000000000..471073d40 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/AList.java @@ -0,0 +1,74 @@ +package convex.core.data; + +import java.util.function.Function; + +import convex.core.data.type.AType; +import convex.core.data.type.Types; + +/** + * Abstract base class for lists. + * + * Lists are immutable sequences of values, with efficient access and change to + * the head of the list. Lists are most importantly used for representing code + * as data, in the fine tradition of Lisp. + * + * For general manipulation of sequential data, vectors are recommended. + * + * There are multiple possible implementations for different list types, but all + * should conform to the general AList interface. We use an abstract base class + * in preference to an interface because we control the hierarchy and it offers + * some mild performance advantages. + * + * General design goals: - Immutability - Optimised performance for front of + * list (cons, first etc.) - Able to share vector implementations where + * appropriate + * + * @param Type of list + */ +public abstract class AList extends ASequence { + + public AList(long count) { + super(count); + } + + @Override + public final AType getType() { + return Types.LIST; + } + + @Override + public abstract AList cons(T x); + + /** + * Adds an element to this list, in first position. + * + * Returns a new list. + */ + @Override + public abstract AList conj(R x); + + @Override + public AList empty() { + return Lists.empty(); + } + + @Override + public abstract AList map(Function mapper); + + @Override + public abstract AList concat(ASequence vals); + + @Override + public abstract AList assoc(long i, R value); + + // TODO: make sure this is O(1)? + /** + * Drops elements from the front of the list. + * @param n Number of elements to drop + * @return List with n elements removed, or null if not possible + */ + public abstract AList drop(long n); + + + +} diff --git a/convex-core/src/main/java/convex/core/data/ALongBlob.java b/convex-core/src/main/java/convex/core/data/ALongBlob.java new file mode 100644 index 000000000..f02c15cb9 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/ALongBlob.java @@ -0,0 +1,150 @@ +package convex.core.data; + +import java.nio.ByteBuffer; + +import convex.core.util.Errors; +import convex.core.util.Utils; + +public abstract class ALongBlob extends ABlob { + + protected static final long LENGTH = 8; + + protected final long value; + + protected ALongBlob(long value) { + this.value=value; + } + + @Override + public final long count() { + return 8; + } + + @Override + @SuppressWarnings("unchecked") + protected Ref createRef() { + // Create Ref at maximum status to reflect internal embedded nature + Ref newRef= RefDirect.create(this,cachedHash(),Ref.INTERNAL|Ref.KNOWN_EMBEDDED_MASK); + cachedRef=newRef; + return (Ref) newRef; + } + + @Override + public final String toHexString() { + return Utils.toHexString(value); + } + + @Override + public abstract ABlob slice(long start, long length); + + @Override + public abstract Blob toBlob(); + + @Override + public long commonHexPrefixLength(ABlob b) { + return toBlob().commonHexPrefixLength(b); + } + + private void checkIndex(long i) { + if ((i < 0) || (i >= LENGTH)) throw new IndexOutOfBoundsException(Errors.badIndex(i)); + } + + @Override + public final byte byteAt(long i) { + checkIndex(i); + return (byte) (value >> ((LENGTH - i - 1) * 8)); + } + + @Override + public final byte getUnchecked(long i) { + return (byte) (value >> ((LENGTH - i - 1) * 8)); + } + + @Override + public final ABlob append(ABlob d) { + return toBlob().append(d); + } + + @Override + public abstract boolean equals(ABlob o); + + @Override + public final ByteBuffer writeToBuffer(ByteBuffer bb) { + return bb.putLong(value); + } + + @Override + public final int writeToBuffer(byte[] bs, int pos) { + Utils.writeLong(bs, pos, value); + return pos+8; + } + + @Override + public final Blob getChunk(long i) { + if (i == 0L) return toBlob(); + throw new IndexOutOfBoundsException(Errors.badIndex(i)); + } + + @Override + public final ByteBuffer getByteBuffer() { + return toBlob().getByteBuffer(); + } + + @Override + protected final long calcMemorySize() { + // always embedded and no child Refs, so memory size == 0 + return 0; + } + + @Override + public final void toHexString(StringBuilder sb) { + String s= Utils.toHexString(value); + sb.append(s); + } + + @Override + public long hexMatchLength(ABlob b, long start, long length) { + return toBlob().hexMatchLength(b,start,length); + } + + @Override + public final long toLong() { + return value; + } + + @Override + public long longValue() { + return value; + } + + @Override + public final boolean equalsBytes(ABlob b) { + if (b.count()!=LENGTH) return false; + return value==b.longValue(); + } + + @Override + public abstract byte getTag(); + + @Override + public boolean isCanonical() { + return true; + } + + @Override + public boolean isCVMValue() { + return true; + } + + @Override + public final int getRefCount() { + return 0; + } + + @Override + public final boolean isEmbedded() { + // Always embedded + return true; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/AMap.java b/convex-core/src/main/java/convex/core/data/AMap.java new file mode 100644 index 000000000..317a0420e --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/AMap.java @@ -0,0 +1,323 @@ +package convex.core.data; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Predicate; + +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.TODOException; +import convex.core.lang.RT; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * Abstract base class for maps. + * + * Maps are Smart Data Structures that represent an immutable mapping of keys to + * values. The can also be seen as a data structure where the elements are map entries + * (equivalent to length 2 vectors) + * + * Ordering of map entries (as seen through iterators etc.) depends on map type. + * + * @param Type of keys + * @param Type of values + */ +public abstract class AMap extends ADataStructure> + implements Map { + + protected AMap(long count) { + super(count); + } + + @Override + public AType getType() { + return Types.MAP; + } + + /** + * Gets the values from this map, in map-determined order + */ + @Override + public AVector values() { + int len = size(); + ArrayList al = new ArrayList(len); + accumulateValues(al); + return Vectors.create(al); + } + + // TODO: Review plausible alternative implementation for values() + // + // @Override + // public AVector values() { + // return reduceValues((v,e)->((AVector)v).append(e), Vectors.empty()); + // } + + /** + * Associates the given key with the specified value. + * + * @param key Map key to associate + * @param value Map value + * @return An updated map with the new association, or null if the association fails + */ + public abstract AMap assoc(ACell key, ACell value); + + + /** + * Dissociates a key from this map, returning an updated map if the key was + * removed, or the same unchanged map if the key is not present. + * + * @param key Key to remove. + * @return Updated map + */ + public abstract AMap dissoc(ACell key); + + public final boolean containsKeyRef(Ref ref) { + return getKeyRefEntry(ref) != null; + } + + @SuppressWarnings("unchecked") + public boolean containsKey(ACell key) { + return getEntry((K)key)!=null; + } + + @Override + public final boolean containsKey(Object key) { + if ((key==null)||(key instanceof ACell)) { + return containsKey((ACell)key); + } + return false; + } + + /** + * Get an entry given a Ref to the key value. This is more efficient than + * directly looking up using the key for some map types, and should be preferred + * if the caller already has a Ref available. + * + * @param ref Ref to Map key + * @return MapEntry for the given key ref + */ + public abstract MapEntry getKeyRefEntry(Ref ref); + + /** + * Accumulate all entries from this map in the given HashSet. + * + * @param h HashSet in which to accumulate entries + */ + protected abstract void accumulateEntrySet(HashSet> h); + + /** + * Accumulate all keys from this map in the given HashSet. + * + * @param h HashSet in which to accumulate keys + */ + protected abstract void accumulateKeySet(HashSet h); + + /** + * Accumulate all values from this map in the given ArrayList. + * + * @param al ArrayList in which to accumulate values + */ + protected abstract void accumulateValues(ArrayList al); + + @Override + public final V put(K key, V value) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public final V remove(Object key) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public final void putAll(Map m) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public final void clear() { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public abstract void forEach(BiConsumer action); + + @Override + public void print(StringBuilder sb) { + sb.append('{'); + this.forEach((k, v) -> { + Utils.print(sb,k); + sb.append(' '); + Utils.print(sb,v); + sb.append(','); + }); + if (count() > 0) sb.setLength(sb.length() - 1); // delete trailing comma + sb.append('}'); + } + + /** + * Associate the given map entry into the map. May return null if the map entry is not valid for this map type. + * + * @param e A map entry + * @return The updated map + */ + public abstract AMap assocEntry(MapEntry e); + + /** + * Gets the entry in this map at a specified index, according to the + * map-specific order. + * + * @param i Index of entry + * @return MapEntry at the specified index. + * @throws IndexOutOfBoundsException If this index is not valid + */ + public abstract MapEntry entryAt(long i); + + @Override + public Ref> getElementRef(long index) { + return entryAt(index).getRef(); + } + + @Override + public final MapEntry get(long i) { + return entryAt(i); + } + + /** + * Gets the MapEntry for the given key + * + * @param k Key to lookup in Map + * @return The map entry, or null if the key is not found + */ + public abstract MapEntry getEntry(ACell k); + + @Override + public V get(Object key) { + if (key instanceof ACell) return (V) get((ACell)key); + return null; + } + + public abstract V get(ACell key); + + /** + * Gets the value at a specified key, or returns the fallback value if not found + * + * @param key Key to lookup in Map + * @param notFound Fallback value to return if key is not present + * @return Value for the specified key, or the notFound value. + */ + @SuppressWarnings("unchecked") + public final V get(ACell key, ACell notFound) { + MapEntry me = getEntry((K) key); + if (me == null) { + return (V) notFound; + } else { + return me.getValue(); + } + } + + /** + * Reduce over all values in this map + * + * @param Type of reduction return value + * @param func A function taking the reduction value and a map value + * @param initial Initial reduction value + * @return The final reduction value + */ + public abstract R reduceValues(BiFunction func, R initial); + + + /** + * Filters all values in this map with the given predicate. + * + * @param pred A predicate specifying which elements to retain. + * @return The updated map containing those entries where the predicate returned + * true. + */ + public AMap filterValues(Predicate pred) { + throw new TODOException(); + } + + /** + * Reduce over all map entries in this map + * + * @param Type of reduction return value + * @param func A function taking the reduction value and a map entry + * @param initial Initial reduction value + * @return The final reduction value + */ + public abstract R reduceEntries(BiFunction, ? extends R> func, R initial); + + @SuppressWarnings("unchecked") + @Override + public Set keySet() { + ASet ks=reduceEntries((s,me)->s.conj(me.getKey()), (ASet)(Sets.empty())); + return ks; + } + + + /** + * Returns true if this map has exactly the same keys as the other map + * + * @param map Map to compare with + * @return true if maps have the same keys, false otherwise + */ + public abstract boolean equalsKeys(AMap map); + + @SuppressWarnings("unchecked") + @Override + public final boolean equals(ACell a) { + if (!(a instanceof AMap)) return false; + return equals((AMap) a); + } + + /** + * Checks this map for equality with another map. In general, maps should be + * considered equal if they have the same canonical representation, i.e. the + * same hash value. + * + * Subclasses may override this this they have a more efficient equals + * implementation or a more specific definition of equality. + * + * @param a Map to compare with + * @return true if maps are equal, false otherwise. + */ + public abstract boolean equals(AMap a); + + /** + * Gets the map entry with the specified hash + * + * @param hash Hash of key to lookup + * @return The specified MapEntry, or null if not found. + */ + protected abstract MapEntry getEntryByHash(Hash hash); + + /** + * Adds a new map entry to this map. The argument must be a valid map entry or + * length 2 vector. + * + * @param x An object that can be cast to a MapEntry + * @return Updated map with the specified entry added, or null if the argument + * is not a valid map entry + */ + @SuppressWarnings("unchecked") + public ADataStructure conj(R x) { + MapEntry me = RT.ensureMapEntry(x); + if (me == null) return null; + return (ADataStructure) assocEntry(me); + } + + /** + * Gets a vector of all map entries. + * + * @return Vector map entries, in map-defined order. + */ + public AVector> entryVector() { + return reduceEntries((acc, e) -> acc.conj(e), Vectors.empty()); + } +} diff --git a/convex-core/src/main/java/convex/core/data/AMapEntry.java b/convex-core/src/main/java/convex/core/data/AMapEntry.java new file mode 100644 index 000000000..0b8db0e79 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/AMapEntry.java @@ -0,0 +1,156 @@ +package convex.core.data; + +import java.util.Iterator; +import java.util.ListIterator; +import java.util.Map; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import convex.core.exceptions.TODOException; +import convex.core.util.Errors; +import convex.core.util.Utils; + +public abstract class AMapEntry extends AVector implements Map.Entry { + + public AMapEntry(long count) { + super(2); + } + + @Override + public abstract ACell get(long i); + + @Override + public final AVector appendChunk(VectorLeaf listVector) { + throw new IllegalArgumentException("Can't append chunk to a MapEntry of size: 2"); + } + + @Override + public final VectorLeaf getChunk(long offset) { + return toVector().getChunk(offset); + } + + @Override + public final boolean isPacked() { + return false; + } + + @Override + public abstract int getRefCount(); + + @Override + public abstract Ref getRef(int i); + + @Override + public abstract K getKey(); + + @Override + public abstract V getValue(); + + @Override + public final V setValue(V value) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public abstract boolean isCanonical(); + + @Override + public AVector append(ACell value) { + return toVector().append(value); + } + + @Override + public Spliterator spliterator(long position) { + return toVector().spliterator(position); + } + + @Override + public ListIterator listIterator(long index) { + return toVector().listIterator(index); + } + + @Override + public ListIterator listIterator() { + return toVector().listIterator(); + } + + @Override + public Iterator iterator() { + return toVector().iterator(); + } + + @Override + public long longIndexOf(Object o) { + return toVector().longIndexOf(o); + } + + @Override + public long longLastIndexOf(Object o) { + return toVector().longLastIndexOf(o); + } + + @Override + public long commonPrefixLength(AVector b) { + if (b == this) return 2; + long bc = b.count(); + if (bc == 0) return 0; + if (!Utils.equals(getKey(), b.get(0))) return 0; + if (bc == 1) return 1; + if (!Utils.equals(getValue(), b.get(1))) return 1; + return 2; + } + + /** + * Create a new MapEntry with an updated key. Shares old value. Returns the same + * MapEntry if unchanged + * + * @param key Key to update + * @return + */ + protected abstract AMapEntry withKey(K key); + + /** + * Create a new MapEntry with an updated value. Shares old key. Returns the same + * MapEntry if unchanged + * + * @param value Value to update + * @return + */ + protected abstract AMapEntry withValue(V value); + + @Override + public AVector next() { + return Vectors.of(getValue()); + } + + @Override + public abstract int encode(byte[] bs, int pos); + + @Override + public final ACell set(int index, ACell element) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean anyMatch(Predicate pred) { + return toVector().anyMatch(pred); + } + + @Override + public boolean allMatch(Predicate pred) { + return toVector().allMatch(pred); + } + + @Override + public void forEach(Consumer action) { + action.accept(getKey()); + action.accept(getValue()); + } + + @Override + public T[] toArray(T[] a) { + throw new TODOException(); + } + +} diff --git a/convex-core/src/main/java/convex/core/data/ANumericBlob.java b/convex-core/src/main/java/convex/core/data/ANumericBlob.java new file mode 100644 index 000000000..f432b6e49 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/ANumericBlob.java @@ -0,0 +1,65 @@ +package convex.core.data; + +import java.util.Arrays; + +import convex.core.exceptions.TODOException; + +/** + * Base class for Blobs which represent an integral numeric value + */ +public abstract class ANumericBlob extends AArrayBlob { + + protected ANumericBlob(byte[] bytes, int offset, int length) { + super(bytes, offset, length); + } + + @Override + public int estimatedEncodingSize() { + // Tag+reasonable length+raw bytes + return 10+length; + } + + @Override + public boolean equals(ABlob a) { + if (a instanceof ANumericBlob) { + return equals((ANumericBlob)a); + } + return false; + } + + public boolean equals(ANumericBlob a) { + // TODO: should be overridden to handle specific types + return Arrays.equals(store, offset, offset+length, a.store, a.offset, a.offset+a.length); + } + + @Override + public Blob getChunk(long i) { + return toBlob().getChunk(i); + } + + @Override + public boolean isRegularBlob() { + return false; + } + + + // TODO: these should be abstract + @Override + public int encode(byte[] bs, int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCanonical() { + return false; + } + + @Override public final boolean isCVMValue() { + return true; + } + + @Override + public byte getTag() { + throw new TODOException(); + } +} diff --git a/convex-core/src/main/java/convex/core/data/AObject.java b/convex-core/src/main/java/convex/core/data/AObject.java new file mode 100644 index 000000000..970e4d6f5 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/AObject.java @@ -0,0 +1,56 @@ +package convex.core.data; + +public abstract class AObject { + /** + * We cache the Blob for the binary encoding of this Cell + */ + protected Blob encoding; + + /** + * Prints this Object to a readable String Representation + * + * @param sb StringBuilder to append to + */ + public abstract void print(StringBuilder sb); + + /** + * Renders this object as a String value + * @return String representation + */ + public final String print() { + StringBuilder sb = new StringBuilder(); + print(sb); + return sb.toString(); + } + + /** + * Gets the encoded byte representation of this cell. + * + * @return A blob representing this cell in encoded form + */ + public Blob getEncoding() { + if (encoding==null) encoding=createEncoding(); + return encoding; + } + + /** + * Creates a Blob object representing this object. Should be called only after + * the cached encoding has been checked. + * + * @return Blob Encoding of Object + */ + protected abstract Blob createEncoding(); + + /** + * Attach the given encoding Blob to this object, if no encoding is currently cached + * + * Warning: Blob must be the correct canonical representation of this Cell, + * otherwise bad things may happen (incorrect hashcode, etc.) + * + * @param data Encoding of Value. Must be a correct canonical encoding. + */ + public final void attachEncoding(Blob data) { + this.encoding=data; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/ARecord.java b/convex-core/src/main/java/convex/core/data/ARecord.java new file mode 100644 index 000000000..51eae1698 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/ARecord.java @@ -0,0 +1,340 @@ +package convex.core.data; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +import convex.core.Block; +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.lang.impl.RecordFormat; +import convex.core.util.Utils; + +/** + * Base class for record data types. + * + * Records are map-like data structures with fixed sets of keys, and optional custom behaviour. + * + * Ordering of fields is defined by the Record's RecordFormat + * + */ +public abstract class ARecord extends AMap { + + protected final RecordFormat format; + + // TODO: need a better default value? + public static final ARecord DEFAULT_VALUE=Block.create(0, AccountKey.ZERO, Vectors.empty()); + + protected ARecord(RecordFormat format) { + super(format.count()); + this.format=format; + } + + public AType getType() { + return Types.RECORD; + } + + @Override + public int estimatedEncodingSize() { + return (int) (Format.MAX_EMBEDDED_LENGTH*format.count()); + } + + @Override + public boolean isCanonical() { + // Records should always be canonical + return true; + } + + @Override public final boolean isCVMValue() { + return true; + } + + /** + * Writes the raw fields of this record in declared order + * @param bs Array to write to + */ + @Override + public int encodeRaw(byte[] bs, int pos) { + List keys=getKeys(); + for (Keyword key: keys) { + pos=Format.write(bs,pos, get(key)); + } + return pos; + } + + @Override + public void print(StringBuilder sb) { + sb.append("{"); + long n=format.count(); + for (int i=0; i me=entryAt(i); + Keyword k=me.getKey(); + k.print(sb); + sb.append(' '); + Object v=me.getValue(); + Utils.print(sb, v); + if (i<(n-1)) sb.append(','); + } + sb.append("}"); + } + + /** + * Gets a vector of keys for this record + * + * @return Vector of Keywords + */ + public final AVector getKeys() { + return format.getKeys(); + } + + /** + * Gets a vector of values for this record, in format-determined order + * + * @return Vector of Values + */ + @Override + public AVector values() { + int n=size(); + ACell[] os=new ACell[n]; + for (int i=0; i Ref getRef(int index) { + long n=size(); + int si=index; + if (index<0) throw new IndexOutOfBoundsException("Negative ref index: "+index); + for (int i=0; i keys=getKeys(); + for (int i=0; i keys=format.getKeys(); + for (int i=0; i keySet() { + return format.keySet(); + } + + @Override + public Set> entrySet() { + return toHashMap().entrySet(); + } + + @Override + public AMap assoc(ACell key, ACell value) { + // TODO: OK to convert records to hashmaps? + return toHashMap().assoc(key, value); + } + + public AMap dissoc(Keyword key) { + if (!containsKey(key)) return this; + return toHashMap().dissoc(key); + } + + @Override + public AMap dissoc(ACell key) { + if (!containsKey(key)) return this; + return toHashMap().dissoc(key); + } + + @Override + public MapEntry getKeyRefEntry(Ref ref) { + // TODO: could maybe be more efficient? + return getEntry(ref.getValue()); + } + + @Override + protected void accumulateEntrySet(HashSet> h) { + for (long i=0; i h) { + AVector keys=format.getKeys(); + for (long i=0; i al) { + toHashMap().accumulateValues(al); + } + + @Override + public void forEach(BiConsumer action) { + throw new UnsupportedOperationException(); + } + + @Override + public AMap assocEntry(MapEntry e) { + return assoc(e.getKey(),e.getValue()); + } + + @Override + public MapEntry entryAt(long i) { + if ((i<0)||(i>=count)) throw new IndexOutOfBoundsException("Index:"+i); + Keyword k=format.getKeys().get(i); + return getEntry(k); + } + + @Override + public MapEntry getEntry(ACell k) { + if (!containsKey(k)) return null; + return MapEntry.create((Keyword)k,get(k)); + } + + @Override + public R reduceValues(BiFunction func, R initial) { + for (int i=0; i R reduceEntries(BiFunction, ? extends R> func, R initial) { + for (int i=0; i map) { + return toHashMap().equalsKeys(map); + } + + /** + * Converts this record to a hashmap + * @return HashMap instance + */ + protected AHashMap toHashMap() { + AHashMap m=Maps.empty(); + for (int i=0; i getEntryByHash(Hash hash) { + return toHashMap().getEntryByHash(hash); + } + + @Override + public AHashMap empty() { + // coerce to AHashMap since we are removing all keys + return Maps.empty(); + } + + /** + * Gets the RecordFormat instance that describes this Record's layout + * @return RecordFormat instance + */ + public RecordFormat getFormat() { + return format; + } + + @Override + public ARecord toCanonical() { + // Should already be canonical + return this; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/ARecordGeneric.java b/convex-core/src/main/java/convex/core/data/ARecordGeneric.java new file mode 100644 index 000000000..e0e15066f --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/ARecordGeneric.java @@ -0,0 +1,102 @@ +package convex.core.data; + +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.impl.RecordFormat; +import convex.core.util.Utils; + +/** + * Abstract base class for generic records. + * + * Generic records are backed by a vector + */ +public abstract class ARecordGeneric extends ARecord { + + protected AVector values; + + protected ARecordGeneric(RecordFormat format, AVector values) { + super(format); + if (values.count()!=format.count()) throw new IllegalArgumentException("Wrong number of field values for record: "+values.count()); + this.values=values; + } + + @Override + public MapEntry entryAt(long i) { + return MapEntry.create(format.getKey(Utils.checkedInt(i)), values.get(i)); + } + + @Override + public ACell get(ACell key) { + Long ix=format.indexFor(key); + if (ix==null) return null; + return values.get((long)ix); + } + + @Override + public abstract byte getTag(); + + @Override + public int getRefCount() { + return values.getRefCount(); + } + + @Override + public boolean equals(AMap a) { + if (this == a) return true; // important optimisation for e.g. hashmap equality + if (a == null) return false; + if (a.getTag()!=getTag()) return false; + Hash h=this.cachedHash(); + if (h!=null) { + Hash ha=a.cachedHash(); + if (ha!=null) return Utils.equals(h, ha); + } + return values.equals(((ARecordGeneric)a).values); + } + + @Override + public Ref getRef(int index) { + return values.getRef(index); + } + + @Override + public ARecord updateRefs(IRefFunction func) { + AVector newValues=values.updateRefs(func); + return withValues(newValues); + } + + @Override + protected ARecord updateAll(ACell[] newVals) { + int n=size(); + if (newVals.length!=n) throw new IllegalArgumentException("Wrong number of values: "+newVals.length); + boolean changed = false; + for (int i=0; i newVector=Vectors.create(newVals); + return withValues(newVector); + } + + @Override + public AVector values() { + return values; + } + + /** + * Updates the record with a new set of values. + * + * Returns this if and only if values vector is identical. + * + * @param newValues New values to use + * @return Updated Record + */ + protected abstract ARecord withValues(AVector newValues); + + @Override + public void validateCell() throws InvalidDataException { + values.validateCell(); + } + +} diff --git a/convex-core/src/main/java/convex/core/data/ASequence.java b/convex-core/src/main/java/convex/core/data/ASequence.java new file mode 100644 index 000000000..f30ee34a7 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/ASequence.java @@ -0,0 +1,262 @@ +package convex.core.data; + +import java.util.Collection; +import java.util.List; +import java.util.ListIterator; +import java.util.function.Consumer; +import java.util.function.Function; + +import convex.core.data.prim.CVMLong; +import convex.core.lang.RT; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * Abstract base class for persistent lists and vectors + * + * @param Type of list elements + */ +public abstract class ASequence extends ACollection implements List, IAssociative { + + public ASequence(long count) { + super(count); + } + + @Override + public boolean contains(Object o) { + return longIndexOf(o) >= 0; + } + + /** + * Gets the first long index at which the specified value appears in the the sequence. + * @param value Any value which could appear as an element of the sequence. + * @return Index of the value, or -1 if not found. + */ + public abstract long longIndexOf(Object value); + + /** + * Gets the last long index at which the specified value appears in the the sequence. + * @param value Any value which could appear as an element of the sequence. + * @return Index of the value, or -1 if not found. + */ + public abstract long longLastIndexOf(Object value); + + public abstract ASequence map(Function mapper); + + @Override + public abstract void forEach(Consumer action); + + /** + * Visits all elements in this sequence, calling the specified consumer for each. + * + * @param f Function to call for each element + */ + public abstract void visitElementRefs(Consumer> f); + + @SuppressWarnings("unchecked") + public ASequence flatMap(Function> mapper) { + ASequence> vals = this.map(mapper); + ASequence result = (ASequence) this.empty(); + for (ASequence seq : vals) { + result = result.concat(seq); + } + return result; + } + + /** + * Concatenates the elements from another sequence to the end of this sequence. + * Potentially O(n) in size of resulting sequence + * + * @param vals A sequence of values to concatenate. + * @return The concatenated sequence, of the same type as this sequence. + */ + public abstract ASequence concat(ASequence vals); + + @Override + public final boolean addAll(int index, Collection c) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + /** + * Gets the sequence of all elements after the first, or null if no elements + * remain + * + * @return Sequence following the first element + */ + public abstract ASequence next(); + + /** + * Gets the element at the specified index in this sequence. + * + * Behaves as if the index was considered as a long + * + * @param index Index of element to get + * @return Element at the specified index + */ + @Override + public T get(int index) { + return get((long) index); + } + + @Override + public abstract T get(long index); + + /** + * Gets the element at the specified key + * + * @param key Key of element to get + * @return The value at the specified index, or null if not valid + */ + @Override + public T get(ACell key) { + if (key instanceof CVMLong) { + long ix = ((CVMLong) key).longValue(); + if ((ix >= 0) && (ix < count())) return get(ix); + } + return null; + } + + @SuppressWarnings("unchecked") + @Override + public ACell get(ACell key, ACell notFound) { + if (key instanceof CVMLong) { + long ix = ((CVMLong) key).longValue(); + if ((ix >= 0) && (ix < count())) return get(ix); + } + return (T) notFound; + } + + @Override + public boolean containsKey(ACell key) { + if (key instanceof CVMLong) { + long ix = ((CVMLong) key).longValue(); + if ((ix >= 0) && (ix < count())) return true; + } + return false; + } + + + + /** + * Gets the element Ref at the specified index + * + * @param index Index of element to get + * @return Ref to element at specified index + */ + public abstract Ref getElementRef(long index); + + @Override + public T set(int index, T element) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @SuppressWarnings("unchecked") + @Override + public ASequence assoc(ACell key, ACell value) { + CVMLong ix=RT.ensureLong(key); + if (ix==null) return null; + return assoc(ix.longValue(),(T)value); + } + + /** + * Updates a value at the given position in the sequence. + * + * @param i Index of element to update + * @param value New element value + * @return Updated sequence, or null if index is out of range + */ + public abstract ASequence assoc(long i, R value); + + /** + * Checks if an index range is valid for this sequence + * + * @param start + * @param length + */ + protected void checkRange(long start, long length) { + if (start < 0) throw Utils.sneakyThrow(new IndexOutOfBoundsException("Negative start: " + start)); + if (length < 0L) throw Utils.sneakyThrow(new IndexOutOfBoundsException("Negative length: " + length)); + if ((start + length) > count()) + throw Utils.sneakyThrow(new IndexOutOfBoundsException("End out of bounds: " + start + length)); + } + + @Override + public final void add(int index, T element) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public final T remove(int index) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + /** + * Converts this sequence to a new Cell array + * @return A new cell array containing the elements of this sequence + */ + public ACell[] toCellArray() { + int n=Utils.checkedInt(count()); + ACell[] cells=new ACell[n]; + for (int i=0; i ASequence conj(R value); + + /** + * Produces a slice of this sequence, beginning with the specified start index and of the given length. + * The start and length must be contained within this sequence. Will return the same sequence if the + * start is zero and the length matches this sequence. + * + * @param start Index of the start element + * @param length Length of slice to create. + * @return A sequence representing the requested slice. + */ + public abstract ASequence slice(long start, long length); + + /** + * Prepends an element to this sequence, returning a list. + * @param x Any new element value + * @return A list starting with the new element. + */ + public abstract AList cons(T x); + + /** + * Gets a vector containing the specified subset of this sequence. + * + * @param start Start index of sub vector + * @param length Length of sub vector to produce + * @return Sub-vector of this sequence + */ + public abstract AVector subVector(long start, long length); + + @Override + public final java.util.List subList(int fromIndex, int toIndex) { + long start = fromIndex; + long length = toIndex - fromIndex; + return subVector(start, length); + } + + /** + * Gets the ListIterator for a long position + * + * @param l + * @return ListIterator instance. + */ + protected abstract ListIterator listIterator(long l); + + /** + * Reverses a sequence, converting Lists to Vectors and vice versa + * @return Reversed sequence + */ + public abstract ASequence reverse(); +} diff --git a/convex-core/src/main/java/convex/core/data/ASet.java b/convex-core/src/main/java/convex/core/data/ASet.java new file mode 100644 index 000000000..27ca9c326 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/ASet.java @@ -0,0 +1,216 @@ +package convex.core.data; + +import java.util.function.Function; + +import convex.core.Constants; +import convex.core.data.prim.CVMBool; +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.util.Utils; + +/** + * Abstract based class for sets. + * + * Sets are immutable Smart Data Structures representing an unordered + * collection of distinct values. + * + * Iteration order is dependent on the Set implementation. In general, it + * is bad practice to depend on any specific ordering for sets. + * + * @param Type of set elements + */ +public abstract class ASet extends ACollection implements java.util.Set, IAssociative { + + protected ASet(long count) { + super(count); + } + + @Override + public final AType getType() { + return Types.SET; + } + + @Override + public final byte getTag() { + return Tag.SET; + } + + /** + * Updates the set to include the given element + * @param a Value to include + * @return Updated set + */ + public abstract ASet include(R a); + + /** + * Updates the set to exclude the given element + * @param a Value to exclude + * @return Updated set + */ + public abstract ASet exclude(T a) ; + + /** + * Updates the set to include all the given elements. + * Can be used to implement union of sets + * + * @param elements Elements to include + * @return Updated set + */ + public abstract ASet includeAll(ASet elements) ; + + /** + * Updates the set to exclude all the given elements. + * + * @param elements Elements to exclude + * @return Updated set + */ + public abstract ASet excludeAll(ASet elements) ; + + @Override + public abstract ASet conjAll(ACollection xs); + + /** + * Removes all elements from this set, returning a new set. + * @param xs Collection of elements to remove + * @return Set with specified element(s) removed + */ + public abstract ASet disjAll(ACollection xs); + + @Override + public AVector toVector() { + int n=Utils.checkedInt(count); + ACell[] elements=new ACell[n]; + copyToArray(elements,0); + return Vectors.create(elements); + } + + @Override + public ASet map(Function mapper) { + ASet result=Sets.empty(); + for (long i=0; i intersectAll(ASet xs); + + @Override + public CVMBool get(ACell key) { + return contains(key)?Constants.SET_INCLUDED:Constants.SET_EXCLUDED; + } + + @Override + public ACell get(ACell key, ACell notFound) { + if (contains(key)) return Constants.SET_INCLUDED; + return notFound; + } + + @Override + public T get(long index) { + return getElementRef(index).getValue(); + } + + /** + * Tests if this Set contains a given value + * @param o Value to test for set membership + * @return True if set contains value, false otherwise + */ + public abstract boolean contains(ACell o); + + @Override + public final boolean contains(Object o) { + if ((o==null)||(o instanceof ACell)) { + return contains((ACell)o); + } + return false; + } + + @SuppressWarnings("unchecked") + @Override + public final boolean equals(ACell o) { + if (o instanceof ASet) return equals((ASet)o); + return false; + } + + /** + * Checks if another set is exactly equal to this set + * + * @param other Set to compare with this set + * @return true if sets are equal, false otherwise + */ + public abstract boolean equals(ASet other); + + /** + * Adds a value to this set using a Ref to the value + * @param ref Ref to value to include + * @return Updated set + */ + public abstract ASet includeRef(Ref ref) ; + + @Override + public abstract ASet conj(R a); + + @SuppressWarnings("unchecked") + @Override + public ASet assoc(ACell key, ACell value) { + if (value==CVMBool.TRUE) return (ASet) include(key); + if (value==CVMBool.FALSE) return exclude((T) key); + return null; + } + + @Override + public boolean containsKey(ACell key) { + return contains(key); + } + + @Override + public ASet empty() { + return Sets.empty(); + } + + /** + * Gets the Ref in the Set for a given value, or null if not found + * @param k Value to check for set membership + * @return Ref to value, or null + */ + public abstract Ref getValueRef(ACell k); + + /** + * Gets the Ref in the Set for a given hash, or null if not found + * @param hash Hash to check for set membership + * @return Ref to value with given Hash, or null + */ + protected abstract Ref getRefByHash(Hash hash); + + /** + * Tests if this set contains all the elements of another set + * @param b Set to compare with + * @return True if other set is completely contained within this set, false otherwise + */ + public abstract boolean containsAll(ASet b); + + /** + * Tests if this set is a (non-strict) subset of another Set + * @param b Set to test against + * @return True if this is a subset of the other set, false otherwise. + */ + public boolean isSubset(ASet b) { + return b.containsAll(this); + } + + @Override + public void print(StringBuilder sb) { + sb.append("#{"); + for (long i=0; i0) sb.append(','); + Utils.print(sb,get(i)); + } + sb.append('}'); + } +} diff --git a/convex-core/src/main/java/convex/core/data/AString.java b/convex-core/src/main/java/convex/core/data/AString.java new file mode 100644 index 000000000..d485ef849 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/AString.java @@ -0,0 +1,84 @@ +package convex.core.data; + +import convex.core.data.prim.CVMChar; +import convex.core.data.type.AType; +import convex.core.data.type.Types; + +/** + * Class representing a CVM String + */ +public abstract class AString extends ACountable implements CharSequence, Comparable { + + protected int length; + + protected AString(int length) { + this.length=length; + } + + @Override + public AType getType() { + return Types.STRING; + } + + @Override + public void print(StringBuilder sb) { + sb.append('"'); + // TODO. Fix escaping. + sb.append(this); + sb.append('"'); + } + + @Override + public int length() { + return length; + } + + @Override + public long count() { + return length; + } + + public StringShort empty() { + return Strings.EMPTY; + } + + protected abstract AString append(char charValue); + + @Override + public CVMChar get(long i) { + return CVMChar.create(charAt((int)i)); + } + + @Override + public Ref getElementRef(long i) { + return get(i).getRef(); + } + + @Override + public int compareTo(AString o) { + return CharSequence.compare(this,o); + } + + @Override + public String toString() { + StringBuilder sb=new StringBuilder(length); + appendToStringBuffer(sb,0,length()); + return sb.toString(); + } + + protected abstract void appendToStringBuffer(StringBuilder sb, int start, int length); + + @Override + public abstract AString subSequence(int start, int end); + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.STRING; + return encodeRaw(bs,pos); + } + + @Override + public final byte getTag() { + return Tag.STRING; + } +} diff --git a/convex-core/src/main/java/convex/core/data/ASymbolic.java b/convex-core/src/main/java/convex/core/data/ASymbolic.java new file mode 100644 index 000000000..49cd549af --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/ASymbolic.java @@ -0,0 +1,70 @@ +package convex.core.data; + +import convex.core.Constants; +import convex.core.exceptions.InvalidDataException; + +/** + * Abstract based class for symbolic objects (Keywords, Symbols) + */ +public abstract class ASymbolic extends ACell { + + protected final String name; + + protected ASymbolic(String name) { + this.name = name; + } + + @Override + @SuppressWarnings("unchecked") + protected Ref createRef() { + // Create Ref at maximum status to reflect internal embedded status + Ref newRef= RefDirect.create(this,cachedHash(),Ref.INTERNAL_FLAGS); + cachedRef=newRef; + return (Ref) newRef; + } + + @Override public final boolean isCVMValue() { + return true; + } + + @Override + protected long calcMemorySize() { + // always embedded and no child Refs, so memory size == 0 + return 0; + } + + public String getName() { + return name; + } + + protected static boolean validateName(String name2) { + if (name2 == null) return false; + int n = name2.length(); + if ((n < 1) || (n > (Constants.MAX_NAME_LENGTH))) { + return false; + } + + // We have a valid name + return true; + } + + @Override + public boolean isEmbedded() { + // Symbolic values are always embedded + return true; + } + + @Override + public final int hashCode() { + return name.hashCode(); + } + + /** + * Validates the name of this Symbolic value + */ + @Override + public void validateCell() throws InvalidDataException { + if (!validateName(name)) throw new InvalidDataException("Invalid name: " + name, this); + } + +} diff --git a/convex-core/src/main/java/convex/core/data/AVector.java b/convex-core/src/main/java/convex/core/data/AVector.java new file mode 100644 index 000000000..5d28d880e --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/AVector.java @@ -0,0 +1,268 @@ +package convex.core.data; + +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Spliterator; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.util.MergeFunction; +import convex.core.util.Utils; + +/** + * Abstract base class for vectors. + * + * Vectors are immutable sequences of values, with efficient appends to the tail + * of the list. + * + * This is a hierarchy with multiple implementations for different vector types, + * but all should conform to the general AVector interface. We use an abstract + * base class in preference to an interface because we control the hierarchy and + * it offers some mild performance advantages. + * + * General design goals: - Immutability - Cell structure breakdown for larger + * vectors, while keeping a shallow tree - Optimised performance for end of + * vector (conj, pop, last etc.) - Fast prefix comparisons to support consensus + * algorithm + * + * "If I had any recommendation to you at all, it's just if you're thinking + * about designing a system and you're not sure, whether you can answer all that + * questions in the forward direction, choose immutability. You can almost back + * into a little more than 50% of this design just by haven taken immutability + * as a constraint, saying 'oh my god now what am I gonna do? I cannot change + * this. I better do this!' And keep forcing you into good answers. So if I had + * any architectural guidance from this: Just do it. Choose immutability and see + * where it takes you." - Rich Hickey + * + * @param Type of element in Vector + */ +public abstract class AVector extends ASequence { + + + public AVector(long count) { + super(count); + } + + @Override + public AType getType() { + return Types.VECTOR; + } + + /** + * Gets the element at the specified index in this vector + * + * @param i The index of the element to get + * @return The element value at the specified index + */ + @Override + public abstract T get(long i); + + /** + * Appends a ListVector chunk to this vector. This vector must contain a whole + * number of chunks + * + * @param listVector A chunk to append. Must be a ListVector of maximum size + * @return The updated vector, of the same type as this vector @ + */ + public abstract AVector appendChunk(VectorLeaf listVector); + + /** + * Gets the VectorLeaf chunk at a given offset + * + * @param offset Offset into this vector. Must be a valid chunk start position + * @return The chunk referenced + */ + public abstract VectorLeaf getChunk(long offset); + + /** + * Appends a single element to this vector + * + * @param value Value to append + * @return Updated vector + */ + public abstract AVector append(T value); + + /** + * Returns true if this Vector is a single fully packed tree. i.e. a full + * ListVector or TreeVector. + * + * @return true is fully packed, flase otherwise + */ + public abstract boolean isPacked(); + + @Override + public void print(StringBuilder sb) { + sb.append('['); + int size = size(); + for (int i = 0; i < size; i++) { + if (i > 0) sb.append(' '); + Utils.print(sb,get(i)); + } + sb.append(']'); + } + + + + @Override + public T get(int index) { + return get((long) index); + } + + public abstract boolean anyMatch(Predicate pred); + + public abstract boolean allMatch(Predicate pred); + + @Override + public abstract AVector map(Function mapper); + + @Override + @SuppressWarnings("unchecked") + public AVector flatMap(Function> mapper) { + ASequence> vals = this.map(mapper); + AVector result = (AVector) this.empty(); + for (ASequence seq : vals) { + result = result.concat(seq); + } + return result; + } + + @Override + public abstract AVector concat(ASequence b); + + public abstract R reduce(BiFunction func, R value); + + @Override + public Spliterator spliterator() { + return spliterator(0); + } + + public abstract Spliterator spliterator(long position); + + @Override + public Iterator iterator() { + return listIterator(); + } + + @Override + public Object[] toArray() { + int s = size(); + Object[] result = new Object[s]; + copyToArray(result, 0); + return result; + } + + @Override + public final int indexOf(Object o) { + return Utils.checkedInt(longIndexOf(o)); + } + + @Override + public final int lastIndexOf(Object o) { + return Utils.checkedInt(longLastIndexOf(o)); + } + + @Override + public final ListIterator listIterator(int index) { + return listIterator((long) index); + } + + @Override + public abstract ListIterator listIterator(long index); + + /** + * Returns true if this vector is in canonical format, i.e. suitable as + * top-level serialised representation of a vector. + * + * @return true if the vector is in canonical format, false otherwise + */ + @Override + public abstract boolean isCanonical(); + + @Override + public abstract AVector updateRefs(IRefFunction func); + + /** + * Computes the length of the longest common prefix of this vector and another + * vector. + * + * @param b Any vector + * @return Length of the longest common prefix + */ + public abstract long commonPrefixLength(AVector b); + + public AVector appendAll(List list) { + // TODO Optimise with chunks + AVector result = this; + for (T value : list) { + result = result.append(value); + } + return result; + } + + @SuppressWarnings("unchecked") + @Override + public final AVector conj(R value) { + return (AVector) append((T) value); + } + + public AVector conjAll(ACollection xs) { + if (xs instanceof ASequence) { + return concat((ASequence)xs); + } + return concat(Vectors.create(xs)); + } + + @Override + public AList cons(T x) { + return Lists.create(this).cons(x); + } + + @Override + public abstract AVector next(); + + @Override + public final AVector slice(long start, long length) { + return subVector(start, length); + } + + @Override + public abstract AVector assoc(long i, R value); + + @Override + public AVector empty() { + return Vectors.empty(); + } + + @Override + public AList reverse() { + return convex.core.data.List.reverse(this); + } + + /** + * Merges this vector with another vector, using the provided merge function. + * + * Returns the same vector if the result is equal to this vector, or the other + * vector if the result is exactly equal to the other vector. + * + * The merge function is passed null for elements where one vector is shorter + * than the other. + * + * @param b Another vector + * @param func A merge function to apply to all elements of this and the other + * vector + * @return A new vector, equal in length to the largest of the two vectors + * passed @ + */ + public AVector mergeWith(AVector b, MergeFunction func) { + throw new UnsupportedOperationException(); + } + + @Override + public byte getTag() { + return Tag.VECTOR; + } +} diff --git a/convex-core/src/main/java/convex/core/data/AccountKey.java b/convex-core/src/main/java/convex/core/data/AccountKey.java new file mode 100644 index 000000000..f869fb7a2 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/AccountKey.java @@ -0,0 +1,281 @@ +package convex.core.data; + +import java.nio.ByteBuffer; + +import convex.core.Constants; +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.InvalidDataException; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * Immutable class representing an Ed25519 Public Key for an Account + * + *

+ * Using Ed25519: + *

+ *
    + *
  • AccountKey is the Public Key (32 bytes)
  • + *
+ * + */ +public class AccountKey extends AArrayBlob { + public static final int LENGTH = Constants.KEY_LENGTH; + + public static final AType TYPE = Types.BLOB; + + + public static final int LENGTH_BITS = LENGTH * 8; + + public static final AccountKey ZERO = AccountKey.dummy("0"); + + private AccountKey(byte[] data, int offset, int length) { + super(data, offset, length); + if (length != LENGTH) throw new IllegalArgumentException("AccountKey length must be " + LENGTH + " bytes"); + } + + @Override + public AType getType() { + return TYPE; + } + + @Override + @SuppressWarnings("unchecked") + protected Ref createRef() { + // Create Ref at maximum status to reflect internal embedded status + Ref newRef= RefDirect.create(this,cachedHash(),Ref.INTERNAL_FLAGS); + cachedRef=newRef; + return (Ref) newRef; + } + + /** + * Wraps the specified bytes as an AccountKey object. Warning: underlying bytes are + * used directly. Use only if no external references to the byte array will be + * retained. + * + * @param data Byte array to wrap as Account Key + * @return An Address wrapping the given bytes + */ + public static AccountKey wrap(byte[] data) { + return new AccountKey(data, 0, data.length); + } + + /** + * Wraps the specified bytes as an AccountKey object. Warning: underlying bytes are + * used directly. Use only if no external references to the byte array will be + * retained. + * + * @param data Data array containing address bytes. + * @param offset Offset into byte array + * @return An Address wrapping the given bytes + */ + public static AccountKey wrap(byte[] data, int offset) { + return new AccountKey(data, offset, LENGTH); + } + + /** + * Creates an AccountKey from a blob. Must have correct length. + * @param b Blob to wrap as Account Key + * @return AccountKey instance, or null if not valid + */ + public static AccountKey create(ABlob b) { + if (b.count()!=LENGTH) return null; + if (b instanceof AccountKey) return (AccountKey) b; + if (b instanceof AArrayBlob) { + AArrayBlob ab=(AArrayBlob)b; + return new AccountKey(ab.getInternalArray(),ab.getInternalOffset(),LENGTH); + } + return wrap(b.getBytes()); + } + + /** + * Creates a "Dummy" Address that is not a valid public key, and therefore + * cannot have valid signed transactions. + * + * To do this, a short hex nonce is repeated to fill the entire address length. This + * construction makes it possible to examine an Address and assess whether it is (plausibly) + * a dummy address. + * + * @param nonce Hex string to repeat to produce a visible dummy address + * @return An Address that cannot be used to sign transactions. + */ + public static AccountKey dummy(String nonce) { + int n = nonce.length(); + if (n == 0) throw new Error("Empty nonce"); + if (n >= LENGTH / 2) throw new Error("Nonce too long for dummy address"); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < LENGTH * 2; i += n) { + sb.append(nonce); + } + return AccountKey.fromHex(sb.substring(0, LENGTH * 2)); + } + + @Override + public boolean equals(ABlob o) { + if (o==null) return false; + if (o instanceof AccountKey) return equals((AccountKey)o); + if (o.getType()!=TYPE) return false; + if (o.count()!=LENGTH) return false; + return o.equalsBytes(this.store, this.offset); + } + + public boolean equals(AccountKey o) { + if (o == this) return true; + return Utils.arrayEquals(o.store, o.offset, this.store, this.offset, LENGTH); + } + + /** + * Constructs an AccountKey object from a hex string. + * Throws an exception if string is not valid + * + * @param hexString Hex String + * @return An AccountKey constructed from the hex string + */ + public static AccountKey fromHex(String hexString) { + AccountKey result = fromHexOrNull(hexString); + if (result == null) throw new IllegalArgumentException("Invalid Address hex String [" + hexString + "]"); + return result; + } + + /** + * Constructs an AccountKey object from a hex string + * + * @param hexString Hex String + * @return An Address constructed from the hex string, or null if not a valid + * hex string + */ + public static AccountKey fromHexOrNull(String hexString) { + byte[] bs = Utils.hexToBytes(hexString, LENGTH * 2); + if (bs == null) return null; // invalid string + if (bs.length != LENGTH) return null; // wrong length + return wrap(bs); + } + + public static AccountKey fromHexOrNull(AString a) { + if (a.length()!=LENGTH*2) return null; + return fromHexOrNull(a.toString()); + } + + + /** + * Constructs an AccountKey object from a checksummed hex string. + * + * Throws an exception if checksum is not valid + * + * @param hexString Hex String + * @return An Address constructed from the hex string + */ + public static AccountKey fromChecksumHex(String hexString) { + byte[] bs = Utils.hexToBytes(hexString, LENGTH * 2); + AccountKey a = AccountKey.wrap(bs); + Hash h = a.getContentHash(); + for (int i = 0; i < LENGTH * 2; i++) { + int dh = h.getHexDigit(i); + char c = hexString.charAt(i); + if (Character.isDigit(c)) continue; + boolean check = (c >= 'a') ^ (dh >= 8); // note 'a' is higher than 'A' + if (!check) + throw new IllegalArgumentException("Bad checksum at position " + i + " in address " + hexString); + } + return a; + } + + /** + * Converts this AccountKey to a checksummed hex string. + * + * @return A String containing the checksummed hex representation of this + * Address + */ + public String toChecksumHex() { + StringBuilder sb = new StringBuilder(64); + Hash h = this.getContentHash(); + for (int i = 0; i < LENGTH * 2; i++) { + int dh = h.getHexDigit(i); + int da = this.getHexDigit(i); + if (da < 10) { + sb.append((char) ('0' + da)); + } else { + boolean up = (dh >= 8); + sb.append((char) ((up ? 'A' : 'a') + da - 10)); + } + } + return sb.toString(); + } + + public static AccountKey readRaw(ByteBuffer data) { + byte[] buff = new byte[LENGTH]; + data.get(buff); + return AccountKey.wrap(buff); + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.BLOB; + bs[pos++]=Constants.KEY_LENGTH; + return encodeRaw(bs,pos); + } + + @Override public final boolean isCVMValue() { + return true; + } + + @Override + public int estimatedEncodingSize() { + // tag plus LENGTH bytes + return 3 + LENGTH; + } + + @Override + public long getEncodingLength() { + // Always a fixed encoding length, tag plus count plus length + return 2 + LENGTH; + } + + @Override + public Blob getChunk(long i) { + if (i != 0) throw new IndexOutOfBoundsException(Errors.badIndex(i)); + return toBlob(); + } + + @Override + public void validateCell() throws InvalidDataException { + if (length != LENGTH) + throw new InvalidDataException("Address length must be " + LENGTH + " bytes = " + LENGTH_BITS + " bits", + this); + } + + @Override + public boolean isEmbedded() { + return true; + } + + @Override + protected long calcMemorySize() { + // always embedded and no child Refs, so memory size == 0 + return 0; + } + + @Override + public boolean isRegularBlob() { + return true; + } + + @Override + public byte getTag() { + return Tag.BLOB; + } + + @Override + public boolean isCanonical() { + return false; + } + + @Override + public Blob toCanonical() { + return toBlob(); + } + + + +} diff --git a/convex-core/src/main/java/convex/core/data/AccountStatus.java b/convex-core/src/main/java/convex/core/data/AccountStatus.java new file mode 100644 index 000000000..1dc23b07d --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/AccountStatus.java @@ -0,0 +1,500 @@ +package convex.core.data; + +import java.nio.ByteBuffer; + +import convex.core.Constants; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.AFn; +import convex.core.lang.RT; +import convex.core.lang.impl.RecordFormat; +import convex.core.util.Utils; + +/** + * Class representing the current on-chain status of an account. + * + * Accounts may be User accounts or Actor accounts. + * + * "People said I should accept the world. Bullshit! I don't accept the world." + * - Richard Stallman + */ +public class AccountStatus extends ARecord { + private final long sequence; + private final long balance; + private final long memory; + private final AHashMap environment; + private final AHashMap> metadata; + private final ABlobMap holdings; + private final Address controller; + private final AccountKey publicKey; + + private static final Keyword[] ACCOUNT_KEYS = new Keyword[] { Keywords.SEQUENCE, Keywords.BALANCE,Keywords.ALLOWANCE,Keywords.ENVIRONMENT,Keywords.METADATA, + Keywords.HOLDINGS, Keywords.CONTROLLER, Keywords.KEY}; + + private static final RecordFormat FORMAT = RecordFormat.of(ACCOUNT_KEYS); + + private static final int HAS_SEQUENCE=1< environment, + AHashMap> metadata, + ABlobMap holdings, + Address controller, + AccountKey publicKey) { + super(FORMAT); + this.sequence = sequence; + this.balance = balance; + this.memory = memory; + this.environment = environment; + this.metadata=metadata; + this.holdings=holdings; + this.controller=controller; + this.publicKey=publicKey; + } + + /** + * Create a regular account, with the specified balance and zero allowance + * + * @param sequence Sequence number + * @param balance Convex Coin balance of Account + * @param key Public Key of new Account + * @return New AccountStatus + */ + public static AccountStatus create(long sequence, long balance, AccountKey key) { + return new AccountStatus(sequence, balance, 0L, null,null,null,null,key); + } + + /** + * Create a governance account. + * + * @param balance Balance for governance account + * @return New governance AccountStatus + */ + public static AccountStatus createGovernance(long balance) { + return new AccountStatus(Constants.INITIAL_SEQUENCE, balance, 0L, null,null,null,null,null); + } + + public static AccountStatus createActor() { + return new AccountStatus(Constants.INITIAL_SEQUENCE, 0L, 0L,null,null,null,null,null); + } + + public static AccountStatus create(long balance, AccountKey key) { + return create(0, balance,key); + } + + /** + * Create a completely empty Account record, with no balance or public key + * @return Empty Account record + */ + public static AccountStatus create() { + return create(0, 0L,null); + } + + /** + * Gets the sequence number for this Account. The sequence number is the number + * of transactions executed by this account to date. It will be zero for new + * Accounts. + * + * The next transaction executed must have a nonce equal to this value plus one. + * + * @return The sequence number for this Account. + */ + public long getSequence() { + return sequence; + } + + public long getBalance() { + return balance; + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.ACCOUNT_STATUS; + return encodeRaw(bs,pos); + } + + private int getInclusion() { + int included=0; + if (sequence!=0L) included|=HAS_SEQUENCE; + if (balance!=0L) included|=HAS_BALANCE; + if (memory!=0L) included|=HAS_ALLOWANCE; + if (environment!=null) included|=HAS_ENVIRONMENT; + if (metadata!=null) included|=HAS_METADATA; + if (holdings!=null) included|=HAS_HOLDINGS; + if (controller!=null) included|=HAS_CONTROLLER; + if (publicKey!=null) included|=HAS_KEY; + return included; + + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + int included=getInclusion(); + bs[pos++]=(byte)included; + if ((included&HAS_SEQUENCE)!=0) pos = Format.writeVLCLong(bs, pos,sequence); + if ((included&HAS_BALANCE)!=0) pos = Format.writeVLCLong(bs,pos, balance); + if ((included&HAS_ALLOWANCE)!=0) pos = Format.writeVLCLong(bs,pos, memory); + if ((included&HAS_ENVIRONMENT)!=0) pos = Format.write(bs,pos, environment); + if ((included&HAS_METADATA)!=0) pos = Format.write(bs,pos, metadata); + if ((included&HAS_HOLDINGS)!=0) pos = Format.write(bs,pos, holdings); + if ((included&HAS_CONTROLLER)!=0) pos = Format.write(bs,pos, controller); + if ((included&HAS_KEY)!=0) pos = publicKey.writeToBuffer(bs, pos); + return pos; + } + + public static AccountStatus read(ByteBuffer bb) throws BadFormatException { + int included=bb.get(); + long sequence = ((included&HAS_SEQUENCE)!=0) ? Format.readVLCLong(bb) : 0L; + long balance = ((included&HAS_BALANCE)!=0) ? Format.readVLCLong(bb) : 0L; + long allowance = ((included&HAS_ALLOWANCE)!=0) ? Format.readVLCLong(bb) : 0L; + AHashMap environment = ((included&HAS_ENVIRONMENT)!=0) ? Format.read(bb):null; + AHashMap> metadata = ((included&HAS_METADATA)!=0) ? Format.read(bb) : null; + ABlobMap holdings = ((included&HAS_HOLDINGS)!=0) ? Format.read(bb) : null; + Address controller = ((included&HAS_CONTROLLER)!=0) ? Format.read(bb) : null; + AccountKey publicKey = ((included&HAS_KEY)!=0) ? AccountKey.readRaw(bb) : null; + return new AccountStatus(sequence, balance, allowance, environment,metadata,holdings,controller,publicKey); + } + + @Override + public int estimatedEncodingSize() { + return 30+Format.estimateSize(environment)+Format.estimateSize(holdings)+Format.estimateSize(controller)+33; + } + + @Override + public boolean isCanonical() { + return true; + } + + public boolean isActor() { + return publicKey==null; + } + + + + /** + * Get the controller for this Account + * @return Controller Address, or null if there is no controller + */ + public Address getController() { + return controller; + } + + /** + * Checks if this account has enough balance for a transaction consuming the + * specified amount. + * + * @param amt minimum amount that must be present in the specified balance + * @return true if Account has at least the balance specified, false otherwise + */ + public boolean hasBalance(long amt) { + if (amt < 0) return false; + if (amt > balance) return false; + return true; + } + + public AccountStatus withBalance(long newBalance) { + if (balance==newBalance) return this; + return new AccountStatus(sequence, newBalance, memory, environment,metadata,holdings,controller,publicKey); + } + + + public AccountStatus withAccountKey(AccountKey newKey) { + if (newKey==publicKey) return this; + return new AccountStatus(sequence, balance, memory, environment,metadata,holdings,controller,newKey); + } + + public AccountStatus withMemory(long newMemory) { + if (memory==newMemory) return this; + return new AccountStatus(sequence, balance, newMemory, environment,metadata,holdings,controller,publicKey); + } + + public AccountStatus withBalances(long newBalance, long newAllowance) { + if ((balance==newBalance)&&(memory==newAllowance)) return this; + return new AccountStatus(sequence, newBalance, newAllowance, environment,metadata,holdings,controller,publicKey); + } + + public AccountStatus withEnvironment(AHashMap newEnvironment) { + if ((newEnvironment!=null)&&newEnvironment.isEmpty()) newEnvironment=null; + if (environment==newEnvironment) return this; + return new AccountStatus(sequence, balance, memory,newEnvironment,metadata,holdings,controller,publicKey); + } + + public AccountStatus withMetadata(AHashMap> newMeta) { + if ((newMeta!=null)&&newMeta.isEmpty()) newMeta=null; + if (metadata==newMeta) return this; + return new AccountStatus(sequence, balance, memory,environment,newMeta,holdings,controller,publicKey); + } + + @Override + public boolean equals(AMap a) { + if (this == a) return true; // important optimisation for e.g. hashmap equality + if (a == null) return false; + if (a.getTag()!=getTag()) return false; + AccountStatus as=(AccountStatus)a; + return equals(as); + } + + /** + * Tests if this account is equal to another Account + * @param a AccountStatus to compare with + * @return true if equal, false otherwise + */ + public boolean equals(AccountStatus a) { + if (a == null) return false; + Hash h=this.cachedHash(); + if (h!=null) { + Hash ha=a.cachedHash(); + if (ha!=null) return Utils.equals(h, ha); + } + + if (balance!=a.balance) return false; + if (sequence!=a.sequence) return false; + if (memory!=a.memory) return false; + if (!(Utils.equals(controller, a.controller))) return false; + if (!(Utils.equals(publicKey, a.publicKey))) return false; + if (!(Utils.equals(holdings, a.holdings))) return false; + if (!(Utils.equals(metadata, a.metadata))) return false; + if (!(Utils.equals(environment, a.environment))) return false; + return true; + } + + /** + * Updates this account with a new sequence number. + * + * @param newSequence New sequence number + * @return Updated account, or null if the sequence number was wrong + */ + public AccountStatus updateSequence(long newSequence) { + // SECURITY: shouldn't ever be trying to call updateSequence on a Actor address! + if (isActor()) throw new Error("Trying to update Actor sequence number!"); + + long expected = sequence + 1; + if (expected != newSequence) { + return null; + } + + return new AccountStatus(newSequence, balance, memory, environment,metadata,holdings,controller,publicKey); + } + + @Override + public void validateCell() throws InvalidDataException { + if (environment != null) { + if (environment.isEmpty()) throw new InvalidDataException("Account should not have empty map as environment",this); + environment.validateCell(); + } + if (holdings != null) { + if (environment.isEmpty()) throw new InvalidDataException("Account should not have empty map as metadata",this); + holdings.validateCell(); + } + } + + /** + * Gets the value in the Account's environment for the given symbol. + * + * @param Result type + * @param symbol Symbol to get in Environment + * @return The value from the environment, or null if not found + */ + @SuppressWarnings("unchecked") + public R getEnvironmentValue(Symbol symbol) { + if (environment == null) return null; + ACell value = environment.get(symbol); + return (R) value; + } + + /** + * Gets the holdings for this account. Will always be a non-null map. + * @return Holdings map for this account + */ + public ABlobMap getHoldings() { + ABlobMap result=holdings; + if (result==null) return BlobMaps.empty(); + return result; + } + + public ACell getHolding(Address addr) { + if (holdings==null) return null; + return holdings.get(addr); + } + + public AccountStatus withHolding(Address addr,ACell value) { + ABlobMap newHoldings=getHoldings(); + if (value==null) { + newHoldings=newHoldings.dissoc(addr); + } else if (newHoldings==null) { + newHoldings=BlobMaps.of(addr,value); + } else { + newHoldings=newHoldings.assoc(addr, value); + } + return withHoldings(newHoldings); + } + + private AccountStatus withHoldings(ABlobMap newHoldings) { + if (newHoldings.isEmpty()) newHoldings=null; + if (holdings==newHoldings) return this; + return new AccountStatus(sequence, balance, memory, environment,metadata,newHoldings,controller,publicKey); + } + + public AccountStatus withController(Address newController) { + if (controller==newController) return this; + return new AccountStatus(sequence, balance, memory, environment,metadata,holdings,newController,publicKey); + } + + @Override + public int getRefCount() { + int rc=(environment==null)?0:environment.getRefCount(); + rc+=(metadata==null)?0:metadata.getRefCount(); + rc+=(holdings==null)?0:holdings.getRefCount(); + return rc; + } + + public Ref getRef(int i) { + if (i<0) throw new IndexOutOfBoundsException(i); + + int ec=(environment==null)?0:environment.getRefCount(); + if (i newEnv=(AHashMap) newVals[3]; + AHashMap> newMeta=(AHashMap>) newVals[4]; + ABlobMap newHoldings=(ABlobMap) newVals[5]; + if ((newHoldings!=null)&&newHoldings.isEmpty()) newHoldings=null; // switch empty maps to null + Address newController = (Address)newVals[6]; + AccountKey newKey=(AccountKey)newVals[7]; + + if ((balance==newBal)&&(sequence==newSeq)&&(newEnv==environment)&&(newMeta==metadata)&&(newHoldings==holdings)&&(newController==controller)&&(newKey==publicKey)) { + return this; + } + + return new AccountStatus(newSeq,newBal,newAllowance,newEnv,newMeta,newHoldings,newController,newKey); + } + + /** + * Gets the memory allowance for this account + * @return Memory allowance in bytes + */ + public long getMemory() { + return memory; + } + + /** + * Gets the memory usage for this Account. Memory usage is defined as the size of the AccountStatus Cell + * @return Memory usage of this Account in bytes. + */ + public long getMemoryUsage() { + return this.getMemorySize(); + } + + /** + * Adds a change in balance to this account. Must not cause an illegal balance. Returns this instance unchanged + * if the delta is zero + * @param delta Amount of Convex copper to add + * @return Updates account record + */ + public AccountStatus addBalance(long delta) { + if (delta==0) return this; + return withBalance(balance+delta); + } + + /** + * Gets the public key for this Account. May bu null (e.g. for Actors) + * @return Account public key + */ + public AccountKey getAccountKey() { + return publicKey; + } + + public AHashMap> getMetadata() { + if (metadata==null) return Maps.empty(); + return metadata; + } + + /** + * Gets the Environment for this account. Defaults to the an empty map if no Environment has been created. + * @return Environment map for this Account + */ + public AHashMap getEnvironment() { + if (environment==null) return Maps.empty(); + return environment; + } + + /** + * Gets the callable functions from this Account. + * @return Set of callable Symbols + */ + public ASet getCallableFunctions() { + ASet results=Sets.empty(); + if (metadata==null) return results; + for (Entry> me:metadata.entrySet()) { + ACell callVal=me.getValue().get(Keywords.CALLABLE_Q); + if (RT.bool(callVal)) { + Symbol sym=me.getKey(); + if (RT.ensureFunction(getEnvironmentValue(sym))==null) continue; + results=results.conj(sym); + } + } + return results; + } + + /** + * Gets a callable function from the environment, or null if not callable + * @param sym Symbol to look up + * @return Callable function if found, null otherwise + */ + public AFn getCallableFunction(Symbol sym) { + ACell exported=getEnvironmentValue(sym); + if (exported==null) return null; + AFn fn=RT.ensureFunction(exported); + if (fn==null) return null; + AHashMap md=getMetadata().get(sym); + if (RT.bool(md.get(Keywords.CALLABLE_Q))) { + // We have both a function and required metadata tag + return fn; + } + return null; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/Address.java b/convex-core/src/main/java/convex/core/data/Address.java new file mode 100644 index 000000000..951c048e8 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/Address.java @@ -0,0 +1,227 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.security.MessageDigest; + +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.util.Utils; + +/** + * Immutable class representing an Address. + * + * An Address is a specialised 8-byte long blob instance that wraps a non-negative long account number. This number + * serves as an index into the vector of accounts for the current state. + * + */ +public final class Address extends ALongBlob { + + + public static final Address ZERO = Address.create(0); + + private Address(long value) { + super(value); + } + + /** + * Creates an Address from a blob. Number be a valid non-negative long value. + * + * @param number Account number + * @return Address instance, or null if not valid + */ + public static Address create(long number) { + if (number<0) return null; + return new Address(number); + } + + /** + * Creates an Address from a blob. Must be a valid long value + * @param b Blob to convert to an Address + * @return Address instance, or null if not valid + */ + public static Address create(ABlob b) { + if (b.count()!=8) return null; + return create(b.longValue()); + } + + @Override + public AType getType() { + return Types.ADDRESS; + } + + + @Override + public int hashCode() { + // note: We use the Java hashcode of a long + return Long.hashCode(value); + } + + @Override + public boolean equals(ABlob o) { + if (!(o instanceof Address)) return false; + return value==((Address) o).value; + } + + public boolean equals(Address o) { + return value==o.value; + } + + /** + * Constructs an Address object from a hex string + * + * @param hexString String to read Address from + * @return An Address constructed from the hex string, or null if not a valid + * hex string + */ + public static Address fromHex(String hexString) { + // catch nulls just in case + if (hexString==null) return null; + + // catch odd length + if ((hexString.length()&1)!=0) return null; + + if (hexString.length()>16) return null; + Blob b=Blob.fromHex(hexString); + if (b==null) return null; + if (b.length!=8) return null; + return create(b.longValue()); + } + + /** + * Constructs an Address from an arbitrary String, attempting to parse different possible formats + * @param s String to parse + * @return Address parsed, or null if not valid + */ + public static Address parse(String s) { + s=s.trim(); + if (s.startsWith("#")) { + s=s.substring(1); + } + + if (s.startsWith("0x")) { + s=s.substring(2); + return fromHex(s); + } + + try { + long l=Long.parseLong(s); + return Address.create(l); + } catch (NumberFormatException e) { + // fall through + } + + return null; + } + + public static Address readRaw(ByteBuffer bb) throws BadFormatException { + long value=Format.readVLCLong(bb); + Address a= Address.create(value); + if (a==null) throw new BadFormatException("Invalid VLC encoding for Address"); + return a; + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.ADDRESS; + return encodeRaw(bs,pos); + } + + @Override + public void print(StringBuilder sb) { + sb.append("#"); + sb.append(value); + } + + @Override + public boolean isCanonical() { + // always canonical, since class invariants are maintained + return true; + } + + @Override + public int estimatedEncodingSize() { + // tag plus LENGTH bytes + return 1 + Format.MAX_VLC_LONG_LENGTH; + } + + @Override + public void validateCell() throws InvalidDataException { + if (value<0) + throw new InvalidDataException("Address must be positive",this); + + } + + @Override public final boolean isCVMValue() { + return true; + } + + + @Override + public boolean isRegularBlob() { + return false; + } + + @Override + public void getBytes(byte[] dest, int destOffset) { + Utils.writeLong(dest, destOffset, value); + } + + @Override + public Blob slice(long start, long length) { + return toBlob().slice(start,length); + } + + @Override + public Blob toBlob() { + byte[] bs=new byte[8]; + Utils.writeLong(bs, 0, value); + return Blob.wrap(bs); + } + + @Override + protected void updateDigest(MessageDigest digest) { + toBlob().updateDigest(digest); + } + + @Override + public boolean equalsBytes(byte[] bytes, int byteOffset) { + return value==Utils.readLong(bytes, byteOffset); + } + + @Override + public long longValue() { + return value; + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + return Format.writeVLCLong(bs, pos, value); + } + + public static final int MAX_ENCODING_LENGTH = 1+Format.MAX_VLC_LONG_LENGTH; + + @Override + public byte getTag() { + return Tag.ADDRESS; + } + + @Override + public Address toCanonical() { + return this; + } + + /** + * Creates a new Address at an offset to this Address + * @param offset Offset to add to this Address (may be negative) + * @return New Address + */ + public Address offset(long offset) { + return create(value+offset); + } + + + + +} diff --git a/convex-core/src/main/java/convex/core/data/Blob.java b/convex-core/src/main/java/convex/core/data/Blob.java new file mode 100644 index 000000000..9567f1647 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/Blob.java @@ -0,0 +1,269 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Random; + +import convex.core.exceptions.BadFormatException; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * General purpose immutable wrapper for byte array data. + * + * Can be serialised directly if 4096 bytes or less, otherwise needs to be + * structures as a BlobTree. + * + * Encoding format is: + * - Tag.BLOB tag byte + * - VLC encoded Blob length in bytes (one or two bytes describing a length in range 0..4096) + * - Byte data of the given length + */ +public class Blob extends AArrayBlob { + public static final Blob EMPTY = wrap(Utils.EMPTY_BYTES); + public static final Blob NULL_ENCODING = Blob.wrap(new byte[] {Tag.NULL}); + + public static final int CHUNK_LENGTH = 4096; + + private Blob(byte[] bytes, int offset, int length) { + super(bytes, offset, length); + } + + /** + * Creates a new data object using a copy of the specified byte range + * + * @param data Byte array + * @param offset Start offset in the byte array + * @param length Number of bytes to take from data array + * @return The new Data object + */ + public static Blob create(byte[] data, int offset, int length) { + if (length <= 0) { + if (length == 0) return EMPTY; + throw new IllegalArgumentException(Errors.negativeLength(length)); + } + byte[] store = Arrays.copyOfRange(data, offset, offset + length); + return wrap(store); + } + + /** + * Creates a new data object using a copy of the specified byte array. + * + * @param data Byte array + * @return Blob with the same byte contents as the given array + */ + public static Blob create(byte[] data) { + return create(data, 0, data.length); + } + + /** + * Wraps the specified bytes as a Data object Warning: underlying bytes are used + * directly. Use only if no other references to the byte array are kept which + * might be mutated. + * + * @param data Byte array + * @return Blob wrapping the given data + */ + public static Blob wrap(byte[] data) { + return new Blob(data, 0, data.length); + } + + /** + * Wraps the specified bytes as a Data object Warning: underlying bytes are used + * directly. Use only if no other references to the byte array are kept which + * might be mutated. + * + * @param data Byte array + * @param offset Offset into byte array + * @param length Length of byte array to wrap + * @return Blob wrapping the given byte array segment + */ + public static Blob wrap(byte[] data, int offset, int length) { + if (length < 0) throw new IllegalArgumentException(Errors.negativeLength(length)); + if ((offset < 0) || (offset + length > data.length)) + throw new IndexOutOfBoundsException(Errors.badRange(offset, length)); + return new Blob(data, offset, length); + } + + @Override + public Blob toBlob() { + return this; + } + + @Override + public Blob slice(long start, long length) { + if (start < 0) throw new IllegalArgumentException("Start out of bounds: " + start); + if ((start + length) > this.length) + throw new IllegalArgumentException("End out of bounds: " + (start + length)); + if (length < 0) throw new IllegalArgumentException("Negative length of slice: " + length); + if (length == 0) return EMPTY; + return Blob.wrap(store, Utils.checkedInt(start + offset), Utils.checkedInt(length)); + } + + @Override + public boolean equals(ABlob a) { + if (a==null) return false; + if (a instanceof Blob) return equals((Blob) a); + if (a.getType()!=getType()) return false; + if (a.count()!=count()) return false; + return a.equalsBytes(this.store, this.offset); + } + + public boolean equals(Blob b) { + if (length!=b.length) return false; + return Arrays.equals(store, offset, offset+length, b.store, b.offset, b.offset+length); + } + + /** + * Equality for array Blob objects + * + * Implemented by testing equality of byte data + * + * @param other Blob to comapre with + * @return true if blobs are equal, false otherwise. + */ + public boolean equals(AArrayBlob other) { + if (other == this) return true; + if (this.length != other.length) return false; + + // avoid false positives with other Blob types, especially Hash and Address + if (this.getType() != other.getType()) return false; + + if ((contentHash != null) && (other.contentHash != null) && contentHash.equals(other.contentHash)) return true; + return Utils.arrayEquals(other.store, other.offset, this.store, this.offset, this.length); + } + + /** + * Constructs a Blob object from a hex string + * + * @param hexString Hex String to read + * @return Blob with the provided hex value, or null if not a valid blob + */ + public static Blob fromHex(String hexString) { + byte[] bs=Utils.hexToBytes(hexString); + if (bs==null) return null; + return wrap(bs); + } + + /** + * Constructs a Blob object from all remaining bytes in a ByteBuffer + * + * @param bb ByteBuffer + * @return Blob containing the contents read from the ByteBuffer + */ + public static Blob fromByteBuffer(ByteBuffer bb) { + int count = bb.remaining(); + byte[] bs = new byte[count]; + bb.get(bs); + return Blob.wrap(bs); + } + + @Override + public ByteBuffer getByteBuffer() { + if (offset == 0) { + return ByteBuffer.wrap(store, offset, length).asReadOnlyBuffer(); + } else { + return ByteBuffer.wrap(this.getBytes()).asReadOnlyBuffer(); + } + } + + /** + * Fast read of a Blob from its representation insider another Blob object, + * + * Main benefit is to avoid reconstructing via ByteBuffer allocation, enabling + * retention of source Blob object as encoded data. + * + * @param source Source Blob object. + * @param len Length in bytes to take from the source Blob + * @return Blob read from the source + * @throws BadFormatException If encoding is invalid + */ + public static AArrayBlob read(Blob source, long len) throws BadFormatException { + // compute data length, excluding tag and encoded length + int headerLength = (1 + Format.getVLCLength(len)); + long rLen = source.count() - headerLength; + if (len != rLen) { + throw new BadFormatException("Invalid length for Blob, length field " + len + " but actual length " + rLen); + } + + return source.slice(headerLength, len); + } + + @Override + public int encode(byte[] bs, int pos) { + if (length > CHUNK_LENGTH) { + return BlobTree.create(this).encode(bs,pos); + } else { + // we have a Blob of canonical size + bs[pos++]=Tag.BLOB; + pos=Format.writeVLCLong(bs, pos, length); + pos=encodeRaw(bs,pos); + return pos; + } + } + + @Override + public int estimatedEncodingSize() { + // space for tag, generous VLC length, plus raw data + return 1 + Format.MAX_VLC_LONG_LENGTH + length; + } + + /** + * Maximum encoding size for a regular Blob + */ + public static int MAX_ENCODING_LENGTH=1+Format.getVLCLength(CHUNK_LENGTH)+CHUNK_LENGTH; + + @Override + public boolean isCanonical() { + return length <= Blob.CHUNK_LENGTH; + } + + @Override public final boolean isCVMValue() { + return true; + } + + /** + * Creates a Blob of random bytes of the given length + * + * @param random Any Random generator instance + * @param length Length of blob to generate in bytes + * @return Blob with the specified number of random bytes + */ + public static Blob createRandom(Random random, long length) { + byte[] randBytes = new byte[Utils.checkedInt(length)]; + random.nextBytes(randBytes); + return wrap(randBytes); + } + + @Override + public Blob getChunk(long i) { + if ((i == 0) && (length <= CHUNK_LENGTH)) return this; + long chunkStart = i * CHUNK_LENGTH; + return slice(chunkStart, Math.min(CHUNK_LENGTH, length - chunkStart)); + } + + public void attachContentHash(Hash hash) { + if (contentHash == null) contentHash = hash; + } + + @Override + public boolean isRegularBlob() { + return true; + } + + @Override + public byte getTag() { + return Tag.BLOB; + } + + @Override + public ABlob toCanonical() { + if (isCanonical()) return this; + return Blobs.toCanonical(this); + } + + + + + +} \ No newline at end of file diff --git a/convex-core/src/main/java/convex/core/data/BlobMap.java b/convex-core/src/main/java/convex/core/data/BlobMap.java new file mode 100644 index 000000000..c4c020930 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/BlobMap.java @@ -0,0 +1,744 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Predicate; + +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.RT; +import convex.core.util.Bits; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * BlobMap node implementation supporting: + * + *
    + *
  • An optional prefix string
  • + *
  • An optional entry with this prefix
  • + *
  • Up to 16 child entries at the next level of depth
  • + *
+ * @param Type of Keys + * @param Type of values + */ +public class BlobMap extends ABlobMap { + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static final Ref[] EMPTY_CHILDREN = new Ref[0]; + + /** + * Empty BlobMap singleton + */ + public static final BlobMap EMPTY = new BlobMap(0, 0, null, EMPTY_CHILDREN, + (short) 0, 0L); + + static { + // Set empty Ref flags as internal embedded constant + EMPTY.getRef().setFlags(Ref.INTERNAL_FLAGS); + } + + /** + * Child entries, i.e. nodes with keys where this node is a common prefix. Only contains children where mask is set. + * Child entries must have at least one entry. + */ + private final Ref>[] children; + + /** + * Entry for this node of the radix tree. Invariant assumption that the prefix + * is correct. May be null if there is no entry at this node. + */ + private final MapEntry entry; + + /** + * Mask of child entries, 16 bits for each hex digit that may be present. + */ + private final short mask; + + /** + * Depth of radix tree in number of hex digits. Top level is 0. + * Children should have depth = parent depth + parent prefixLength + 1 + */ + private final long depth; + + /** + * Length of prefix, where the tree branches beyond depth. 0 = no prefix. + */ + private final long prefixLength; + + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected BlobMap(long depth, long prefixLength, MapEntry entry, Ref[] entries, + short mask, long count) { + super(count); + this.depth = depth; + this.prefixLength = prefixLength; + this.entry = entry; + int cn = Utils.bitCount(mask); + if (cn != entries.length) throw new IllegalArgumentException( + "Illegal mask: " + Utils.toHexString(mask) + " for given number of children: " + cn); + this.children = (Ref[]) entries; + this.mask = mask; + } + + public static BlobMap create(MapEntry me) { + ACell k=me.getKey(); + if (!(k instanceof ABlob)) return null; + long hexLength = ((ABlob)k).hexLength(); + return new BlobMap(0, hexLength, me, EMPTY_CHILDREN, (short) 0, 1L); + } + + private static BlobMap createAtDepth(MapEntry me, long depth) { + Blob prefix = me.getKey().toBlob(); + long hexLength = prefix.hexLength(); + if (depth > hexLength) + throw new IllegalArgumentException("Depth " + depth + " too deep for key with hexLength: " + hexLength); + return new BlobMap(depth, hexLength - depth, me, EMPTY_CHILDREN, (short) 0, 1L); + } + + public static BlobMap create(K k, V v) { + MapEntry me = MapEntry.create(k, v); + long hexLength = k.hexLength(); + return new BlobMap(0, hexLength, me, EMPTY_CHILDREN, (short) 0, 1L); + } + + public static BlobMap of(Object k, Object v) { + return create(RT.cvm(k),RT.cvm(v)); + } + + @Override + public boolean isCanonical() { + return true; + } + + @Override public final boolean isCVMValue() { + return (depth==0); + } + + @SuppressWarnings("unchecked") + @Override + public BlobMap updateRefs(IRefFunction func) { + MapEntry newEntry = (entry == null) ? null : entry.updateRefs(func); + Ref>[] newChildren = Ref.updateRefs(children, func); + if ((entry == newEntry) && (children == newChildren)) return this; + return new BlobMap(depth, prefixLength, newEntry, (Ref[])newChildren, mask, count); + } + + @Override + public boolean containsValue(Object value) { + if ((entry != null) && Utils.equals(entry.getValue(), value)) return true; + for (int i = 0; i < count; i++) { + if (children[i].getValue().containsValue(value)) return true; + } + return false; + } + + @Override + public V get(ABlob key) { + MapEntry me = getEntry(key); + if (me == null) return null; + return me.getValue(); + } + + @Override + public MapEntry getEntry(ABlob key) { + long kl = key.hexLength(); + long pl = depth + prefixLength; + if (kl < pl) return null; // key is too short to start with current prefix + + if (kl == pl) { + if ((entry!=null)&&(key.equalsBytes(entry.getKey()))) return entry; // we matched this key exactly! + return null; // entry does not exist + } + + int digit = key.getHexDigit(pl); + BlobMap cc = getChild(digit); + + if (cc == null) return null; + return cc.getEntry(key); + } + + /** + * Gets the child for a specific digit, or null if not found + * + * @param digit + * @return + */ + private BlobMap getChild(int digit) { + int i = Bits.indexForDigit(digit, mask); + if (i < 0) return null; + return (BlobMap) children[i].getValue(); + } + + @Override + public int getRefCount() { + return ((entry == null) ? 0 : entry.getRefCount()) + children.length; + } + + @SuppressWarnings("unchecked") + @Override + public Ref getRef(int i) { + if (entry != null) { + int erc = entry.getRefCount(); + if (i < erc) return entry.getRef(i); + i -= erc; + } + int cl = children.length; + if (i < cl) return (Ref) children[i]; + throw new IndexOutOfBoundsException("No ref for index:" + i); + } + + @SuppressWarnings("unchecked") + public BlobMap assoc(ACell key, ACell value) { + if (!(key instanceof ABlob)) return null; + return assocEntry(MapEntry.create((K)key, (V)value)); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public BlobMap dissoc(ABlob k) { + if (count <= 1) { + if (count == 0) return this; // Must already be empty singleton + if (entry.getKey().equalsBytes(k)) { + return (depth==0)?empty():null; + } + return this; // leave existing entry in place + } + long pDepth = depth + prefixLength; // hex depth of this node including prefix + long kl = k.hexLength(); // hex length of key to dissoc + if (kl < pDepth) { + // no match for sure, so no change + return this; + } + if (kl == pDepth) { + // need to check for match with current entry + if (entry == null) return this; + if (!k.equalsBytes(entry.getKey())) return this; + // at this point have matched entry exactly. So need to remove it safely while + // preserving invariants + if (children.length == 1) { + // need to promote child to the current depth + BlobMap c = (BlobMap) children[0].getValue(); + return new BlobMap(depth, (c.depth + c.prefixLength) - depth, c.entry, c.children, c.mask, + count - 1); + } else { + // Clearing current entry, keeping existing children (must be 2+) + return new BlobMap(depth, prefixLength, null, children, mask, count - 1); + } + } + // dissoc beyond current prefix length, so need to check children + int digit = k.getHexDigit(pDepth); + int childIndex = Bits.indexForDigit(digit, mask); + if (childIndex < 0) return this; // key miss + // we know we need to replace a child + BlobMap oldChild = (BlobMap) children[childIndex].getValue(); + BlobMap newChild = oldChild.dissoc(k); + BlobMap r=this.withChild(digit, oldChild, newChild); + + // check if whole blobmap was emptied + if ((r==null)&&(depth==0)) r= empty(); + return r; + } + + /** + * Prefix blob, must contain hex digits in the range [depth,depth+prefixLength). + * + * May contain more hex digits in memory, this is irrelevant from the + * perspective of serialisation. + * + * Typically we populate with the key of the first entry added to avoid + * unnecessary blob instances being created. + */ + private ABlob getPrefix() { + if (entry!=null) return entry.getKey(); + int n=children.length; + if (n==0) return Blob.EMPTY; + return children[0].getValue().getPrefix(); + } + + @Override + protected void accumulateEntrySet(HashSet> h) { + for (int i = 0; i < children.length; i++) { + children[i].getValue().accumulateEntrySet(h); + } + if (entry != null) h.add(entry); + } + + @Override + protected void accumulateKeySet(HashSet h) { + for (int i = 0; i < children.length; i++) { + children[i].getValue().accumulateKeySet(h); + } + if (entry != null) h.add(entry.getKey()); + } + + @Override + protected void accumulateValues(ArrayList al) { + // add this entry first, since we want lexicographic order + if (entry != null) al.add(entry.getValue()); + for (int i = 0; i < children.length; i++) { + children[i].getValue().accumulateValues(al); + } + } + + @Override + public void forEach(BiConsumer action) { + if (entry != null) action.accept(entry.getKey(), entry.getValue()); + for (int i = 0; i < children.length; i++) { + children[i].getValue().forEach(action); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public BlobMap assocEntry(MapEntry e) { + if (count == 0L) return create(e); + if (count == 1L) { + assert (mask == (short) 0); // should be no children + if (entry.keyEquals(e)) { + if (entry == e) return this; + // recreate, preserving current depth + return createAtDepth(e, depth); + } + } + ABlob k = e.getKey(); + long pDepth = this.prefixDepth(); // hex depth of this node including prefix + long newKeyLength = k.hexLength(); // hex length of new key + long mkl; // matched key length + ABlob prefix=getPrefix(); + if (newKeyLength >= pDepth) { + // constrain relevant key length by match with current prefix + mkl = depth + k.hexMatchLength(prefix, depth, prefixLength); + } else { + mkl = depth + k.hexMatchLength(prefix, depth, newKeyLength - depth); + } + if (mkl < pDepth) { + // we collide at a point shorter than the current prefix length + if (mkl == newKeyLength) { + // new key is subset of the current prefix, so split prefix at key position mkl + // doesn't need to adjust child depths, since they are splitting at the same + // point + long newDepth=mkl+1; // depth for new child + BlobMap split = new BlobMap(newDepth, pDepth - newDepth, entry, (Ref[]) children, mask, + count); + int splitDigit = prefix.getHexDigit(mkl); + short splitMask = (short) (1 << splitDigit); + BlobMap result = new BlobMap(depth, mkl - depth, e, new Ref[] { split.getRef() }, + splitMask, count + 1); + return result; + } else { + // we need to fork the current prefix in two at position mkl + long newDepth=mkl+1; // depth for new children + + BlobMap branch1 = new BlobMap(newDepth, pDepth - newDepth, entry, (Ref[]) children, mask, + count); + BlobMap branch2 = new BlobMap(newDepth, newKeyLength - newDepth, e, (Ref[]) EMPTY_CHILDREN, + (short) 0, 1L); + int d1 = prefix.getHexDigit(mkl); + int d2 = k.getHexDigit(mkl); + if (d1 > d2) { + // swap to get in right order + BlobMap temp = branch1; + branch1 = branch2; + branch2 = temp; + } + Ref[] newChildren = new Ref[] { branch1.getRef(), branch2.getRef() }; + short newMask = (short) ((1 << d1) | (1 << d2)); + BlobMap fork = new BlobMap(depth, mkl - depth, null, newChildren, newMask, count + 1L); + return fork; + } + } + assert (newKeyLength >= pDepth); + if (newKeyLength == pDepth) { + // we must have matched the current entry exactly + if (entry == null) { + // just add entry at this position + return new BlobMap(depth, prefixLength, e, (Ref[]) children, mask, count + 1); + } + if (entry == e) return this; + + // swap entry, no need to change count + return new BlobMap(depth, prefixLength, e, (Ref[]) children, mask, count); + } + // at this point we have matched full prefix, but new key length is longer. + // so we need to update (or add) exactly one child + int childDigit = k.getHexDigit(pDepth); + BlobMap oldChild = getChild(childDigit); + BlobMap newChild; + if (oldChild == null) { + newChild = createAtDepth(e, pDepth+1); // Myst be at least 1 beyond current prefix + } else { + newChild = oldChild.assocEntry(e); + } + return withChild(childDigit, oldChild, newChild); // can't be null since associng + } + + /** + * Updates this BlobMap with a new child. + * + * Either oldChild or newChild may be null. Empty maps are treated as null. + * + * @param childDigit Digit for new child + * @param newChild + * @return BlobMap with child removed, or null if BlobMap was deleted entirely + */ + @SuppressWarnings({ "rawtypes", "unchecked"}) + private BlobMap withChild(int childDigit, BlobMap oldChild, BlobMap newChild) { + // consider empty children as null + //if (oldChild == EMPTY) oldChild = null; + //if (newChild == EMPTY) newChild = null; + if (oldChild == newChild) return this; + + int n = children.length; + // we need a new child array + Ref[] newChildren = children; + if (oldChild == null) { + // definitely need a new entry + newChildren = new Ref[n + 1]; + int newPos = Bits.positionForDigit(childDigit, mask); + short newMask = (short) (mask | (1 << childDigit)); + + System.arraycopy(children, 0, newChildren, 0, newPos); // earlier entries + newChildren[newPos] = newChild.getRef(); + System.arraycopy(children, newPos, newChildren, newPos + 1, n - newPos); // later entries + return new BlobMap(depth, prefixLength, entry, newChildren, newMask, + count + newChild.count()); + } else { + // dealing with an existing child + if (newChild == null) { + // need to delete an existing child + int delPos = Bits.positionForDigit(childDigit, mask); + + // handle special case where we need to promote the remaining child + if (entry == null) { + if (n == 2) { + BlobMap rm = (BlobMap) children[1 - delPos].getValue(); + long newPLength = prefixLength + rm.prefixLength+1; + return new BlobMap(depth, newPLength, rm.entry, (Ref[]) rm.children, rm.mask, + rm.count()); + } else if (n == 1) { + // deleting entire BlobMap! + return null; + } + } + if (n==0) { + System.out.print("BlobMap Bad!"); + } + newChildren = new Ref[n - 1]; + short newMask = (short) (mask & ~(1 << childDigit)); + System.arraycopy(children, 0, newChildren, 0, delPos); // earlier entries + System.arraycopy(children, delPos + 1, newChildren, delPos, n - delPos - 1); // later entries + return new BlobMap(depth, prefixLength, entry, newChildren, newMask, + count - oldChild.count()); + } else { + // need to replace a child + int childPos = Bits.positionForDigit(childDigit, mask); + newChildren = children.clone(); + newChildren[childPos] = newChild.getRef(); + long newCount = count + newChild.count() - oldChild.count(); + return new BlobMap(depth, prefixLength, entry, newChildren, mask, newCount); + } + } + } + + @Override + public R reduceValues(BiFunction func, R initial) { + if (entry != null) initial = func.apply(initial, entry.getValue()); + int n = children.length; + for (int i = 0; i < n; i++) { + initial = children[i].getValue().reduceValues(func, initial); + } + return initial; + } + + @Override + public R reduceEntries(BiFunction, ? extends R> func, R initial) { + if (entry != null) initial = func.apply(initial, entry); + int n = children.length; + for (int i = 0; i < n; i++) { + initial = children[i].getValue().reduceEntries(func, initial); + } + return initial; + } + + @Override + public BlobMap filterValues(Predicate pred) { + BlobMap r=this; + for (int i=0; i<16; i++) { + if (r==null) break; // might be null from dissoc + BlobMap oldChild=r.getChild(i); + if (oldChild==null) continue; + BlobMap newChild=oldChild.filterValues(pred); + r=r.withChild(i, oldChild, newChild); + } + + // check entry at this level. A child might have moved here during the above loop! + if (r!=null) { + if ((r.entry!=null)&&!pred.test(r.entry.getValue())) r=r.dissoc(r.entry.getKey()); + } + + // check if whole blobmap was emptied + if (r==null) { + // everything deleted, but need + if (depth==0) r=empty(); + } + return r; + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.BLOBMAP; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = Format.writeVLCLong(bs,pos, count); + if (count == 0) return pos; // nothing more to know... this must be the empty singleton + + pos = Format.writeVLCLong(bs,pos, depth); + pos = Format.writeVLCLong(bs,pos, prefixLength); + + pos = MapEntry.encodeCompressed(entry,bs,pos); // entry may be null + if (count == 1) return pos; // must be a single entry + + // finally write children + pos = Utils.writeShort(bs,pos,mask); + int n = children.length; + for (int i = 0; i < n; i++) { + pos = encodeChild(bs,pos,i); + } + return pos; + } + + private int encodeChild(byte[] bs, int pos, int i) { + Ref> cref = children[i]; + return cref.encode(bs, pos); + + // TODO: maybe compress single entries? +// ABlobMap c=cref.getValue(); +// if (c.count==1) { +// MapEntry me=c.entryAt(0); +// pos = me.getRef().encode(bs, pos); +// } else { +// pos = cref.encode(bs,pos); +// } +// return pos; + } + + @Override + public int estimatedEncodingSize() { + return 100 + (children.length*2+1) * Format.MAX_EMBEDDED_LENGTH; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static BlobMap read(ByteBuffer bb) throws BadFormatException { + long count = Format.readVLCLong(bb); + if (count < 0) throw new BadFormatException("Negative count!"); + if (count == 0) return (BlobMap) EMPTY; + + long depth = Format.readVLCLong(bb); + if (depth < 0) throw new BadFormatException("Negative depth!"); + long prefixLength = Format.readVLCLong(bb); + if (prefixLength < 0) throw new BadFormatException("Negative prefix length!"); + + // Get entry at this node, might be null + MapEntry me = MapEntry.readCompressed(bb); + + // single entry map + if (count == 1) return new BlobMap(depth, prefixLength, me, EMPTY_CHILDREN, (short) 0, 1L); + + short mask = bb.getShort(); + int n = Utils.bitCount(mask); + Ref[] children = new Ref[n]; + long childDepth=depth+prefixLength+1; // depth for children = this prefixDepth plus one extra hex digit + for (int i = 0; i < n; i++) { + children[i] = readChild(bb,childDepth); + } + return new BlobMap(depth, prefixLength, me, children, mask, count); + } + + @SuppressWarnings({ "rawtypes" }) + private static Ref readChild(ByteBuffer bb, long childDepth) throws BadFormatException { + Ref ref = Format.readRef(bb); + return ref; + // TODO: compression of single entries? +// ACell c=ref.getValue(); +// if (c instanceof BlobMap) { +// return ref; +// } else if (c instanceof AVector) { +// AVector v=(AVector)c; +// MapEntry me=MapEntry.convertOrNull(v); +// if (me==null) throw new BadFormatException("Invalid MApEntry vector as BlobMap child"); +// +// return createAtDepth(me,childDepth).getRef(); +// } else { +// throw new BadFormatException("Bad BlobMap child Type: "+RT.getType(c)); +// } + } + + @Override + protected MapEntry getEntryByHash(Hash hash) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + @Override + public void validate() throws InvalidDataException { + super.validate(); + + long ecount = (entry == null) ? 0 : 1; + int n = children.length; + long pDepth = prefixDepth(); + for (int i = 0; i < n; i++) { + ACell o = children[i].getValue(); + if (!(o instanceof BlobMap)) + throw new InvalidDataException("Illegal BlobMap child type: " + Utils.getClass(o), this); + BlobMap c = (BlobMap) o; + + long ccount=c.count(); + if (ccount==0) { + throw new InvalidDataException("Child "+i+" should not be empty! At depth "+depth,this); + } + + if (c.depth != (pDepth+1)) { + throw new InvalidDataException("Child must have depth: " + (pDepth+1) + " but was: " + c.depth, + this); + } + + if (c.prefixDepth() <= prefixDepth()) { + throw new InvalidDataException("Child must have greater total prefix depth than " + prefixDepth() + + " but was: " + c.prefixDepth(), this); + } + c.validate(); + + ecount += ccount; + } + + if (count != ecount) throw new InvalidDataException("Bad entry count: " + ecount + " expected: " + count, this); + } + + /** + * Gets the total depth of this node including prefix + * + * @return + */ + private long prefixDepth() { + return depth + prefixLength; + } + + @Override + public void validateCell() throws InvalidDataException { + if (prefixLength < 0) throw new InvalidDataException("Negative prefix length!" + prefixLength, this); + if (count == 0) { + if (this != EMPTY) throw new InvalidDataException("Non-singleton empty BlobMap", this); + return; + } else if (count == 1) { + if (entry == null) throw new InvalidDataException("Single entry BlobMap with null entry?", this); + if (mask != 0) throw new InvalidDataException("Single entry BlobMap with child mask?", this); + return; + } + + // at least count 2 from this point + int cn = Utils.bitCount(mask); + if (cn != children.length) throw new InvalidDataException( + "Illegal mask: " + Utils.toHexString(mask) + " for given number of children: " + children.length, this); + + if (entry != null) { + entry.validateCell(); + if (cn == 0) + throw new InvalidDataException("BlobMap with entry and count=" + count + " must have children", this); + } else { + if (cn <= 1) throw new InvalidDataException( + "BlobMap with no entry and count=" + count + " must have two or more children", this); + } + } + + @SuppressWarnings("unchecked") + @Override + public BlobMap empty() { + return (BlobMap) EMPTY; + } + + @Override + public MapEntry entryAt(long ix) { + if (entry != null) { + if (ix == 0L) return entry; + ix -= 1; + } + int n = children.length; + for (int i = 0; i < n; i++) { + ABlobMap c = children[i].getValue(); + long cc = c.count(); + if (ix < cc) return c.entryAt(ix); + ix -= cc; + } + throw new IndexOutOfBoundsException(Errors.badIndex(ix)); + } + + /** + * Removes n leading entries from this BlobMap, in key order. + * + * @param n Number of entries to remove + * @return Updated BlobMap with leading entries removed. + * @throws IndexOutOfBoundsException If there are insufficient entries in the + * BlobMap + */ + public BlobMap removeLeadingEntries(long n) { + // TODO: optimise this + BlobMap bm = this; + for (long i = 0; i < n; i++) { + MapEntry me = bm.entryAt(0); + bm = bm.dissoc(me.getKey()); + } + return bm; + } + + /** + * Checks this BlobMap for equality with another map. + * + * @param a Map to compare with + * @return true if maps are equal, false otherwise. + */ + public boolean equals(AMap a) { + if (this == a) return true; // important optimisation for e.g. hashmap equality + if (a == null) return false; + if (this.getType()!=a.getType()) return false; + // Must be a BlobMap + return equals((BlobMap)a); + } + + /** + * Checks this BlobMap for equality with another BlobMap + * + * @param a BlobMap to compare with + * @return true if maps are equal, false otherwise. + */ + public boolean equals(BlobMap a) { + if (a==null) return false; + long n=this.count(); + if (n != a.count()) return false; + if (this.mask!=a.mask) return false; + + if (!Utils.equals(this.entry, a.entry)) return false; + + Hash h=this.cachedHash(); + if (h!=null) { + Hash ha=a.cachedHash(); + if (ha!=null) return h.equals(ha); + } + return getHash().equals(a.getHash()); + } + + @Override + public byte getTag() { + return Tag.BLOBMAP; + } + + @Override + public ACell toCanonical() { + return this; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/BlobMaps.java b/convex-core/src/main/java/convex/core/data/BlobMaps.java new file mode 100644 index 000000000..0be7cb7f2 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/BlobMaps.java @@ -0,0 +1,38 @@ +package convex.core.data; + +import convex.core.lang.RT; +import convex.core.util.Utils; + +public class BlobMaps { + + /** + * Returns the empty BlobMap. Guaranteed singleton. + * + * @param Type of Blob Map + * @param Key type + * @param Value type + * @return The empty BlobMap + */ + @SuppressWarnings("unchecked") + public static , K extends ABlob, V extends ACell> R empty() { + return (R) BlobMap.EMPTY; + } + + @SuppressWarnings("unchecked") + public static , K extends ABlob, V extends ACell> R create(K k, V v) { + return (R) BlobMap.create(k, v); + } + + @SuppressWarnings("unchecked") + public static , K extends ABlob, V extends ACell> R of(Object... kvs) { + int n = kvs.length; + if (Utils.isOdd(n)) throw new IllegalArgumentException("Even number of key + values required"); + BlobMap result = empty(); + for (int i = 0; i < n; i += 2) { + V value=RT.cvm(kvs[i + 1]); + result = result.assoc((K) kvs[i], value); + } + + return (R) result; + } +} diff --git a/convex-core/src/main/java/convex/core/data/BlobTree.java b/convex-core/src/main/java/convex/core/data/BlobTree.java new file mode 100644 index 000000000..44d1616c8 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/BlobTree.java @@ -0,0 +1,503 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.security.MessageDigest; + +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * Implementation of a large Blob data structure consisting of 2 or more chunks. + * + * Intention is to enable relatively large binary content to be handled without + * too many tree levels, and without too many references in a single tree node + * We choose a branching factor of 16 as a reasonable tradeoff. + * + * Level 1 can hold up to 64k Level 2 can hold up to 1mb Level 3 can hold up to + * 16mb Level 4 can hold up to 256mb ... Level 15 (max) should be big enough for + * the moment + * + * One smart reference is maintained for each child node at each level + * + */ +public class BlobTree extends ABlob { + + public static final int BIT_SHIFT_PER_LEVEL = 4; + public static final int FANOUT = 1 << BIT_SHIFT_PER_LEVEL; + + private final Ref[] children; + private final int shift; + private final long count; + + private BlobTree(Ref[] children, int shift, long count) { + this.children = children; + this.shift = shift; + this.count = count; + } + + /** + * Create a BlobTree from an arbitrary Blob. + * + * Must be of sufficient size to convert to BlobTree + * @param blob Source of BlobTree data + * @return New BolobTree instance + */ + public static BlobTree create(ABlob blob) { + if (blob instanceof BlobTree) return (BlobTree) blob; + + long length = blob.count(); + int chunks = Utils.checkedInt(calcChunks(length)); + Blob[] blobs = new Blob[chunks]; + for (int i = 0; i < chunks; i++) { + int offset = i * Blob.CHUNK_LENGTH; + blobs[i] = blob.slice(offset, Math.min(Blob.CHUNK_LENGTH, length - offset)).toBlob(); + } + return create(blobs); + } + + /** + * Create a BlobTree from an array of children. Each child must be a valid + * chunk. All except the last child must be of the correct chunk size. + * + * @param blobs Blobs to include + * @return New BlobTree + */ + static BlobTree create(Blob... blobs) { + return create(blobs, 0, blobs.length); + } + + /** + * Computes the shift level for a BlobTree with the specified number of chunks + * + * @param chunkCount + * @return Shift value for a BlobTree with the specified number of chunks + */ + static int calcShift(long chunkCount) { + int shift = 0; + while (chunkCount > FANOUT) { + shift += BIT_SHIFT_PER_LEVEL; + chunkCount >>= BIT_SHIFT_PER_LEVEL; + } + return shift; + } + + /** + * Computes the number of chunks for a BlobTree of the given length + * + * @param length The length of the Blob in bytes + * @return Number of chunks needed for a given byte length. + */ + public static long calcChunks(long length) { + return ((length - 1) >> Blobs.CHUNK_SHIFT) + 1; + } + + private static BlobTree createSmall(Blob[] blobs, int offset, int chunkCount) { + long length = 0; + if (chunkCount < 2) throw new IllegalArgumentException("Cannot create BlobTree without at least 2 Blobs"); + @SuppressWarnings("unchecked") + Ref[] children = new Ref[chunkCount]; + for (int i = 0; i < chunkCount; i++) { + Blob blob = blobs[offset + i]; + long childLength = blob.count(); + + if (childLength > Blob.CHUNK_LENGTH) + throw new IllegalArgumentException("BlobTree chunk too large: " + childLength); + if ((i < chunkCount - 1) && (childLength != Blob.CHUNK_LENGTH)) + throw new IllegalArgumentException("Illegal internediate chunk size: " + childLength); + + Ref child = blob.getRef(); + children[i] = child; + length += childLength; + } + return new BlobTree(children, 0, length); + } + + private static BlobTree create(Blob[] blobs, int offset, int chunkCount) { + int shift = calcShift(chunkCount); + if (shift == 0) { + return createSmall(blobs, offset, chunkCount); + } else { + int childSize = 1 << shift; // number of chunks in children + int numChildren = ((chunkCount - 1) >> shift) + 1; + @SuppressWarnings("unchecked") + Ref[] children = new Ref[numChildren]; + + long length = 0; + for (int i = 0; i < numChildren; i++) { + int childOffset = i * childSize; + BlobTree bt = create(blobs, offset + childOffset, Math.min(childSize, chunkCount - childOffset)); + children[i] = bt.getRef(); + length += bt.count; + } + return new BlobTree(children, shift, length); + } + } + +// TODO: better implementation of this +// @Override +// public int compareTo(ABlob o) { +// if (this==o) return 0; +// throw new UnsupportedOperationException(); +// } + + @Override + public boolean isCanonical() { + return count>Blob.CHUNK_LENGTH; + } + + @Override + public final boolean isCVMValue() { + return true; + } + + @Override + public void getBytes(byte[] dest, int destOffset) { + long clen = childLength(); + int n = children.length; + for (int i = 0; i < n; i++) { + getChild(i).getBytes(dest, Utils.checkedInt(destOffset + i * clen)); + } + } + + @Override + public long count() { + return count; + } + + @Override + public ABlob slice(long start, long length) { + if ((start == 0L) && (length == this.count)) return this; + if (start < 0L) throw new IndexOutOfBoundsException(Errors.badIndex(start)); + + long csize = childLength(); + int ci = (int) (start / csize); + if (ci == (start + length - 1) / csize) { + return getChild(ci).slice(start - ci * csize, length); + } + + // FIXME: This looks broken + // TODO: handle big slices more effectively + int alen = Utils.checkedInt(this.count); + byte[] bs = new byte[alen]; + return Blob.wrap(bs, Utils.checkedInt(start), Utils.checkedInt(length)); + } + + private ABlob getChild(int childIndex) { + return children[childIndex].getValue(); + } + + @Override + public Blob toBlob() { + int len = Utils.checkedInt(count()); + byte[] data = new byte[len]; + getBytes(data, 0); + return Blob.wrap(data); + } + + @Override + protected void updateDigest(MessageDigest digest) { + int n = children.length; + for (int i = 0; i < n; i++) { + getChild(i).updateDigest(digest); + } + } + + @Override + public byte getUnchecked(long i) { + int childLength = childLength(); + int ci = (int) (i >> (shift + Blobs.CHUNK_SHIFT)); + return getChild(ci).getUnchecked(i - ci * childLength); + } + + /** + * Gets the length in bytes of each full child of this BlobTree + * @return + */ + private int childLength() { + return 1 << (shift + Blobs.CHUNK_SHIFT); + } + + @Override + public boolean equals(ABlob a) { + if (!(a instanceof BlobTree)) return false; + return equals((BlobTree) a); + } + + public boolean equals(BlobTree b) { + if (b == this) return true; + if (b.count != count) return false; + int n = children.length; + for (int i = 0; i < n; i++) { + if (!children[i].equals(b.children[i])) return false; + } + return true; + } + + @Override + public boolean equalsBytes(byte[] bytes, int byteOffset) { + int clen=childLength(); + for (int i=0; i> shift) + 1); + if ((numChildren < 2) || (numChildren > FANOUT)) { + throw new BadFormatException( + "Invalid number of children [" + numChildren + "] for BlobTree with length: " + count); + } + + @SuppressWarnings("unchecked") + Ref[] children = (Ref[]) new Ref[numChildren]; + for (int i = 0; i < numChildren; i++) { + Ref ref = Format.readRef(bb); + children[i] = ref; + } + + return new BlobTree(children, shift, count); + } + + public static BlobTree read(Blob src, long count) throws BadFormatException { + int headerLength = (1 + Format.getVLCLength(count)); + long chunks = calcChunks(count); + int shift = calcShift(chunks); + int numChildren = Utils.checkedInt(((chunks - 1) >> shift) + 1); + + @SuppressWarnings("unchecked") + Ref[] children = (Ref[]) new Ref[numChildren]; + + ByteBuffer bb=src.getByteBuffer(); + bb.position(headerLength); + for (int i = 0; i < numChildren; i++) { + Ref ref = Format.readRef(bb); + children[i] = ref; + } + + return new BlobTree(children, shift, count); + } + + @Override + public int estimatedEncodingSize() { + return 1 + Format.MAX_VLC_LONG_LENGTH + Ref.INDIRECT_ENCODING_LENGTH * children.length; + } + + @Override + public ABlob append(ABlob d) { + // TODO: optimise + return toBlob().append(d); + } + + @Override + public Blob getChunk(long chunkIndex) { + long childSize = 1 << shift; + int child = Utils.checkedInt(chunkIndex >> shift); + return getChild(child).getChunk(chunkIndex - child * childSize); + } + + @Override + public void validate() throws InvalidDataException { + super.validate(); + int n = children.length; + if ((n < 2) | (n > FANOUT)) throw new InvalidDataException("Illegal number of BlobTree children: " + n, this); + int clen = childLength(); + long total = 0; + + // We need to validate and check the lengths of all child notes. Note that only the last child can + // be shorted than the defined childLength() for this shift level. + for (int i = 0; i < n; i++) { + ABlob child; + child = getChild(i); + child.validate(); + + long cl = child.count(); + total += cl; + if (i == (n - 1)) { + if (cl > clen) throw new InvalidDataException( + "Illegal last child length: " + cl + " expected less than or equal to " + clen, this); + } else { + if (cl != clen) + throw new InvalidDataException("Illegal child length: " + cl + " expected " + clen, this); + } + } + if (total != count) throw new InvalidDataException("Incorrect total child count: " + total, this); + } + + @Override + public ByteBuffer getByteBuffer() { + throw new UnsupportedOperationException("Can't get bytebuffer for " + this.getClass()); + } + + @Override + public String toHexString() { + StringBuilder sb = new StringBuilder(); + toHexString(sb); + return sb.toString(); + } + + @Override + public void toHexString(StringBuilder sb) { + for (int i = 0; i < children.length; i++) { + children[i].getValue().toHexString(sb); + } + } + + @Override + public void validateCell() throws InvalidDataException { + int n = children.length; + if ((n < 2) | (n > FANOUT)) throw new InvalidDataException("Illegal number of BlobTree children: " + n, this); + } + + @Override + public long commonHexPrefixLength(ABlob b) { + long cpl = 0; + long DIGITS_PER_CHUNK = Blob.CHUNK_LENGTH * 2; + for (long i = 0;; i++) { + long cl = getChunk(i).commonHexPrefixLength(b.getChunk(i)); + if (cl < DIGITS_PER_CHUNK) return cpl + cl; + cpl += DIGITS_PER_CHUNK; + } + } + + @Override + public long hexMatchLength(ABlob b, long start, long length) { + long HEX_CHUNK_LENGTH = (Blob.CHUNK_LENGTH * 2); + long end = start + length; + long endChunk = (end - 1) / HEX_CHUNK_LENGTH; + for (long ci = start / HEX_CHUNK_LENGTH; ci < endChunk; ci++) { + long cpos = ci * HEX_CHUNK_LENGTH; // position of chunk + long cs = Math.max(0, start - cpos); // start position within chunk + long ce = Math.min(HEX_CHUNK_LENGTH, end - cpos); // end position within chunk + long clen = ce - cs; // length to check within chunk + long match = getChunk(ci).hexMatchLength(b.getChunk(ci), cs, clen); + if (match < clen) return cpos + cs + match; + } + return length; + } + + @Override + public long longValue() { + if (count != 8) throw new IllegalStateException(Errors.wrongLength(8, count)); + return getChunk(0).longValue(); + } + + @Override + public long toLong() { + return slice(count-8,8).toLong(); + } + + @Override + public int getRefCount() { + return children.length; + } + + @SuppressWarnings("unchecked") + @Override + public Ref getRef(int i) { + return (Ref) children[i]; + } + + @Override + public BlobTree updateRefs(IRefFunction func) { + Ref[] newChildren = Ref.updateRefs(children, func); + return withChildren(newChildren); + } + + private BlobTree withChildren(Ref[] newChildren) { + if (children == newChildren) return this; + return new BlobTree(newChildren, shift, count); + } + + @Override + public boolean isRegularBlob() { + return true; + } + + @Override + public byte getTag() { + return Tag.BLOB; + } + + @Override + public ABlob toCanonical() { + if (isCanonical()) return this; + return Blobs.toCanonical(this); + } + + + +} diff --git a/convex-core/src/main/java/convex/core/data/Blobs.java b/convex-core/src/main/java/convex/core/data/Blobs.java new file mode 100644 index 000000000..71bbd6a98 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/Blobs.java @@ -0,0 +1,96 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.util.Random; + +import convex.core.exceptions.BadFormatException; +import convex.core.util.Utils; + +public class Blobs { + + static final int CHUNK_SHIFT = 12; + + public static final int MAX_ENCODING_LENGTH = Math.max(Blob.MAX_ENCODING_LENGTH, BlobTree.MAX_ENCODING_LENGTH); + + public static T createRandom(long length) { + return createRandom(new Random(),length); + } + + @SuppressWarnings("unchecked") + public static T createRandom(Random r, long length) { + if (length <= Blob.CHUNK_LENGTH) return (T) Blob.createRandom(r, length); + + int numChunks = Utils.checkedInt(((length - 1) >> CHUNK_SHIFT) + 1); + + Blob[] blobs = new Blob[numChunks]; + for (int i = 0; i < numChunks; i++) { + Blob b = Blob.createRandom(r, Math.min(Blob.CHUNK_LENGTH, length - i * Blob.CHUNK_LENGTH)); + blobs[i] = b; + } + return (T) BlobTree.create(blobs); + } + + /** + * Converts any blob to a the correct canonical Blob format + * @param a Any Blob + * @return Canonical version s a Blob or BlobTree + */ + public static ABlob toCanonical(ABlob a) { + long length = a.count(); + if (length <= Blob.CHUNK_LENGTH) return a.toBlob(); + return BlobTree.create(a); + } + + /** + * Creates a blob from a hex string + * @param a Hex String + * @return Blob created, or null if String not valid hex + */ + public static ABlob fromHex(String a) { + long slength = a.length(); + if ((slength & 1) != 0) return null; + Blob fullBlob = Blob.fromHex(a); + + long length = slength / 2; + if (length <= Blob.CHUNK_LENGTH) return fullBlob; + return BlobTree.create(fullBlob); + } + + /** + * Reads a Blob from a ByteBuffer. + * + * @param bb ByteBuffer starting with a blob encoding + * @return Blob read from ByteBuffer + * @throws BadFormatException If format is invalid + */ + public static ABlob read(ByteBuffer bb) throws BadFormatException { + long len = Format.readVLCLong(bb); + if (len < 0L) throw new BadFormatException("Negative blob length?"); + if (len > Blob.CHUNK_LENGTH) return BlobTree.read(bb, len); + byte[] buff = new byte[Utils.checkedInt(len)]; + bb.get(buff); + return Blob.wrap(buff); + // TODO keep byte format representation? + } + + @SuppressWarnings("unchecked") + public static T readFromBlob(Blob source) throws BadFormatException { + int sLen = source.length; + if (sLen < 2) throw new BadFormatException("Trying to read Blob from insufficient source of size " + sLen); + // read length at position 1 (skipping tag) + long len = Format.readVLCLong(source.store, source.offset + 1); + + T result = null; + if (len < 0L) throw new BadFormatException("Negative blob length?"); + if (len > Blob.CHUNK_LENGTH) { + result = (T) BlobTree.read(source, len); + } else { + result = (T) Blob.read(source, len); + } + // we can attach original blob as source at this point + result.attachEncoding(source); + return result; + } + + +} diff --git a/convex-core/src/main/java/convex/core/data/Format.java b/convex-core/src/main/java/convex/core/data/Format.java new file mode 100644 index 000000000..cef96f45f --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/Format.java @@ -0,0 +1,981 @@ +package convex.core.data; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.BufferOverflowException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CodingErrorAction; + +import convex.core.Belief; +import convex.core.Block; +import convex.core.BlockResult; +import convex.core.Order; +import convex.core.Result; +import convex.core.State; +import convex.core.data.prim.CVMBool; +import convex.core.data.prim.CVMByte; +import convex.core.data.prim.CVMChar; +import convex.core.data.prim.CVMDouble; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.lang.AFn; +import convex.core.lang.Core; +import convex.core.lang.Ops; +import convex.core.lang.RT; +import convex.core.lang.impl.Fn; +import convex.core.lang.impl.MultiFn; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Call; +import convex.core.transactions.Invoke; +import convex.core.transactions.Transfer; +import convex.core.util.Utils; + +/** + * Static utility class for message format encoding + * + * "Standards are always out of date. That's what makes them standards." - Alan + * Bennett + */ +public class Format { + + /** + * 8191 byte system-wide limit on the legal length of a data object encoding. + * + * Technical reasons for this choice: + *
    + *
  • This is the max length that can be VLC encoded in a 2 byte message header. This simplifies message encoding and decoding.
  • + *
  • It is big enough to include a 4096-byte Blob
  • + *
+ */ + public static final int LIMIT_ENCODING_LENGTH = 0x1FFF; + + /** + * Maximum length for a VLC encoded Long + */ + public static final int MAX_VLC_LONG_LENGTH = 10; // 70 bits + + /** + * Maximum size in bytes of an embedded value, including tag + */ + public static final int MAX_EMBEDDED_LENGTH=140; // TODO: reconsider + + /** + * Encoded length of a null value + */ + public static final int NULL_ENCODING_LENGTH = 1; + + + /** + * Maximum length in bytes of a Ref encoding (may be an embedded data object) + */ + public static final int MAX_REF_LENGTH = Math.max(Ref.INDIRECT_ENCODING_LENGTH, MAX_EMBEDDED_LENGTH); + + /** + * Gets the length in bytes of VLC encoding for the given long value + * @param x Long value to encode + * @return Length of VLC encoding + */ + public static int getVLCLength(long x) { + if ((x < 64) && (x >= -64)) { + return 1; + } + int bitLength = Utils.bitLength(x); + int blen = (bitLength + 6) / 7; + return blen; + } + + /** + * Puts a VLC encoded long into the specified bytebuffer (with no tag) + * + * Format: + *
    + *
  • MSB of each byte 0=last octet, 1=more octets
  • + *
  • Following MSB, 7 bits of integer representation for each octet
  • + *
  • Second highest bit of first byte is interpreted as the sign
  • + *
+ * @param bb ByteBuffer to write to + * @param x Value to VLC encode + * @return Updated ByteBuffer + */ + public static ByteBuffer writeVLCLong(ByteBuffer bb, long x) { + if ((x < 64) && (x >= -64)) { + // single byte, cleared high bit + byte single = (byte) (x & 0x7F); + return bb.put(single); + } + int bitLength = Utils.bitLength(x); + int blen = (bitLength + 6) / 7; + for (int i = blen - 1; i >= 1; i--) { + byte single = (byte) (0x80 | (x >> (7 * i))); // 7 bits with high bit set + bb = bb.put(single); + } + byte end = (byte) (x & 0x7F); // last 7 bits of long, high bit zero + return bb.put(end); + } + + /** + * Puts a variable length integer into the specified byte array (with no tag) + * + * Format: + *
    + *
  • MSB of each byte 0=last octet, 1=more octets
  • + *
  • Following MSB, 7 bits of integer representation for each octet
  • + *
  • Second highest bit of first byte is interpreted as the sign
  • + *
+ * + * @param bs Byte array to write to + * @param pos Initial position in byte array + * @param x Long value to write + * @return end position in byte array after writing VLC long + */ + public static int writeVLCLong(byte[] bs, int pos, long x) { + if ((x < 64) && (x >= -64)) { + // single byte, cleared high bit + byte single = (byte) (x & 0x7F); + bs[pos++]=single; + return pos; + } + + int bitLength = Utils.bitLength(x); + int blen = (bitLength + 6) / 7; + for (int i = blen - 1; i >= 1; i--) { + byte single = (byte) (0x80 | (x >> (7 * i))); // 7 bits with high bit set + bs[pos++]=single; + } + byte end = (byte) (x & 0x7F); // last 7 bits of long, high bit zero + bs[pos++]=end; + return pos; + } + + /** + * Reads a VLC encoded long from the given ByteBuffer. Assumes no tag + * + * @param bb ByteBuffer from which to read + * @return Long value from ByteBuffer + * @throws BadFormatException If encoding is invalid + */ + public static long readVLCLong(ByteBuffer bb) throws BadFormatException { + byte octet = bb.get(); + long result = vlcSignExtend(octet); // sign extend 7th bit to all bits + int bitsRead = 7; + int sevenBits = octet & 0x7F; + final boolean signOnly = (sevenBits == 0x00) || (sevenBits == 0x7F); // flag for continuation with sign only + while ((octet & 0x80) != 0) { + if (bitsRead > 64) throw new BadFormatException("VLC long encoding too long for long value"); + octet = bb.get(); + sevenBits = octet & 0x7F; + if (signOnly && (bitsRead == 7)) { // only need to test on first iteration + boolean signBit = (sevenBits & 0x40) != 0; // top bit from current 7 bits + boolean resultSignBit = (result < 0L); // sign bit from first octet + if (signBit == resultSignBit) + throw new BadFormatException("VLC long encoding not canonical, excess leading sign byte(s)"); + } + + // continue while high bit of byte set + result = (result << 7) | sevenBits; // shift and set next 7 lowest bits + bitsRead += 7; + } + if ((bitsRead > 63) && !signOnly) { + throw new BadFormatException("VLC long encoding not canonical, non-sign information beyond 63 bits read"); + } + return result; + } + + /** + * Sign extend 7th bit (sign) of a byte to all bits in a long + * + * @param b Byte to extend + * @return The sign-extended byte as a long + */ + public static long vlcSignExtend(byte b) { + return (((long) b) << 57) >> 57; + } + + /** + * Reads a VLC encoded long as a long from the given location in a byte byte + * array. Assumes no tag + * @param data Byte array + * @param pos Position from which to read in byte array + * @return long value from byte array + * @throws BadFormatException If format is invalid, or reading beyond end of + * array + */ + public static long readVLCLong(byte[] data, int pos) throws BadFormatException { + byte octet = data[pos++]; + long result = (((long) octet) << 57) >> 57; // sign extend 7th bit to 64th bit + int bits = 7; + while ((octet & 0x80) != 0) { + if (pos >= data.length) throw new BadFormatException("VLC encoding beyong end of array"); + if (bits > 64) throw new BadFormatException("VLC encoding too long for long value"); + octet = data[pos++]; + // continue while high bit of byte set + result = (result << 7) | (octet & 0x7F); // shift and set next 7 lowest bits + bits += 7; + } + return result; + } + + /** + * Peeks for a VLC encoded message length at the start of a ByteBuffer, which + * must contain at least 1 byte, maximum 2. + * + * Does not move the buffer position. + * + * @param bb ByteBuffer containing a message length + * @return The message length + * @throws BadFormatException If the ByteBuffer does not start with a valid + * message length + */ + public static int peekMessageLength(ByteBuffer bb) throws BadFormatException { + int len = bb.get(0); + + // Zero message length not allowed + if (len == 0) { + throw new BadFormatException( + "Format.peekMessageLength: Zero message length:" + Utils.readBufferData(bb)); + } + + if ((len & 0x40) != 0) { + // sign bit from top byte looks wrong! + String hex = Utils.toHexString((byte) len); + throw new BadFormatException( + "Format.peekMessageLength: Expected positive message length, got first byte [" + hex + "]"); + } + + if ((len & 0x80) == 0) { + // 1 byte header (without high bit set) + return len & 0x3F; + } + + int lsb = bb.get(1); + if ((lsb & 0x80) != 0) { + String hex = Utils.toHexString((byte) len) + Utils.toHexString((byte) lsb); + throw new BadFormatException( + "Format.peekMessageLength: Max 2 bytes allowed in VLC encoded message length, got [" + hex + "]"); + } + len = ((len & 0x3F) << 7) + lsb; + + return len; + } + + /** + * Writes a message length as a VLC encoded long + * + * @param bb ByteBuffer with capacity available for writing + * @param len Length of message to write + * @return The ByteBuffer after writing the message length + */ + public static ByteBuffer writeMessageLength(ByteBuffer bb, int len) { + if ((len <= 0) || (len > LIMIT_ENCODING_LENGTH)) { + throw new IllegalArgumentException("Invalid message length: " + len); + } + return writeVLCLong(bb, len); + } + + public static ByteBuffer writeVLCBigInteger(ByteBuffer bb, BigInteger value) { + int bitLength = value.bitLength() + 1; // bits required including sign bit + if (bitLength <= 64) { + return writeVLCLong(bb, value.longValue()); + } + byte[] bs = value.toByteArray(); + int bslen = bs.length; + int blen = (bitLength + 6) / 7; // number of octets required for encoding + for (int i = blen - 1; i >= 1; i--) { + int bits7 = Utils.extractBits(bs, 7, i * 7); // get 7 bits from source bytes + byte single = (byte) (0x80 | bits7); // 7 bits with high bit set + bb = bb.put(single); + } + byte end = (byte) (bs[bslen - 1] & 0x7F); // last 7 bits of last byte + return bb.put(end); + } + + /** + * Finds the first byte in a bytebuffer which is a VLC terminal byte, starting + * from the current position. + * + * @param bb A ByteBuffer starting with a VLC encoded value. + * @return VLC terminal byte position (relative to position of bytebuffer), or + * -1 if not found + */ + private static int findVLCTerminal(ByteBuffer bb) { + int pos = bb.position(); + int len = bb.remaining(); + for (int i = 0; i < len; i++) { + byte x = bb.get(pos + i); + if ((x & 0x80) == 0) return i; + } + return -1; + } + + /** + * Reads a BigInteger from the ByteBuffer. Assumes tag already read. + * + * @param bb ByteBuffer to read from + * @return A BigInteger + * @throws BadFormatException If format is invalid + */ + public static BigInteger readVLCBigInteger(ByteBuffer bb) throws BadFormatException { + int vlclen = findVLCTerminal(bb) + 1; + if (vlclen == 0) throw new BadFormatException("No terminal byte found for VLC encoding of BigInteger"); + + /** get bytes of VLC encoding */ + byte[] vlc = new byte[vlclen]; + bb.get(vlc); + assert ((vlc[vlclen - 1] & 0x80) == 0); // check for terminal byte in correct position + + /** bytes needed to contain VLC encoding bits */ + int blen = (vlclen * 7 + 7) / 8; + byte[] bs = new byte[blen]; + boolean signBit = (vlc[0] & 0x40) != 0; + boolean signOnly = (vlc[0] == (byte) 0xFF) | (vlc[0] == (byte) 0x80); // continuation with sign + bs[0] = (byte) (signBit ? -1 : 0); // initialise first byte with sign + for (int i = 0; i < vlclen; i++) { // iterate over all bytes in VLC encoding starting from highest + byte bits7 = (byte) (vlc[i] & 0x7F); // 7 bits from VLC byte + + // if the top byte could have been sign extended, need to check for canonical + // encoding on the next highest byte + if (signOnly && (i == 1)) { + boolean thisSign = (bits7 & 0x40) != 0; + if (thisSign == signBit) + throw new BadFormatException("Non-canonical BigInteger with VLC bytes " + Blob.wrap(vlc)); + } + Utils.setBits(bs, 7, 7 * (vlclen - 1 - i), bits7); + } + return new BigInteger(bs); + } + + /** + * Writes a canonical object to a ByteBuffer, preceded by the appropriate tag + * + * @param bb ByteBuffer to write to + * @param cell Cell to write (may be null) + * @return The ByteBuffer after writing the specified object + */ + public static ByteBuffer write(ByteBuffer bb, ACell cell) { + // first check for null + if (cell == null) { + return bb.put(Tag.NULL); + } + // Generic handling for all non-null CVM types + return cell.write(bb); + } + + /** + * Writes a canonical object to a byte array, preceded by the appropriate tag + * + * @param bs Byte array to write to + * @param pos Starting position to write in byte array + * @param cell Cell to write (may be null) + * @return Position in byte array after writing the specified object + */ + public static int write(byte[] bs, int pos, ACell cell) { + if (cell==null) { + bs[pos++]=Tag.NULL; + return pos; + } + return cell.encode(bs,pos); + } + + public static ByteBuffer writeVLCBigDecimal(ByteBuffer bb, BigDecimal value) { + bb = bb.put((byte) value.scale()); + bb = writeVLCBigInteger(bb, value.unscaledValue()); + return bb; + } + + public static BigDecimal readVLCBigDecimal(ByteBuffer bb) throws BadFormatException { + byte scale = bb.get(); + BigInteger value = readVLCBigInteger(bb); + return new BigDecimal(value, scale); + } + + /** + * Writes a UTF-8 String to the byteBuffer. Includes string tag and length + * + * @param bb ByteBuffer to write to + * @param s String to write + * @return ByteBuffer after writing + */ + public static ByteBuffer writeUTF8String(ByteBuffer bb, String s) { + bb = bb.put(Tag.STRING); + return writeRawUTF8String(bb, s); + } + + /** + * Writes a raw string without tag to the byteBuffer. Includes length in bytes + * of UTF-8 representation + * + * @param bb ByteBuffer to write to + * @param s String to write + * @return ByteBuffer after writing + */ + public static ByteBuffer writeRawUTF8String(ByteBuffer bb, String s) { + if (s.length() == 0) { + bb = writeLength(bb, 0); + } else { + byte[] bs = Utils.toByteArray(s); + bb = writeLength(bb, bs.length); + bb = bb.put(bs); + } + return bb; + } + + /** + * Writes a raw string without tag to the byte array. Includes length in bytes + * of UTF-8 representation + * + * @param bs Byte array + * @param pos Starting position to write in byte array + * @param s String to write + * @return Position in byte array after writing + */ + public static int writeRawUTF8String(byte[] bs, int pos, String s) { + if (s.length() == 0) { + // zero length, no string bytes + return writeVLCLong(bs,pos,0); + } + + byte[] sBytes = Utils.toByteArray(s); + int n=sBytes.length; + pos = writeVLCLong(bs,pos, sBytes.length); + System.arraycopy(sBytes, 0, bs, pos, n); + return pos+n; + } + + private static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); + private static final ThreadLocal UTF8_DECODERS = new ThreadLocal() { + @Override + protected CharsetDecoder initialValue() { + CharsetDecoder dec = UTF8_CHARSET.newDecoder(); + dec.onUnmappableCharacter(CodingErrorAction.REPORT); + dec.onMalformedInput(CodingErrorAction.REPORT); + return dec; + } + }; + + private static final ThreadLocal UTF8_ENCODERS = new ThreadLocal() { + @Override + protected CharsetEncoder initialValue() { + CharsetEncoder dec = UTF8_CHARSET.newEncoder(); + dec.onUnmappableCharacter(CodingErrorAction.REPORT); + dec.onMalformedInput(CodingErrorAction.REPORT); + return dec; + } + }; + + + /** + * Reads a UTF-8 String from a ByteBuffer. Assumes the object tag has already been + * read + * + * @param bb ByteBuffer to read from + * @return String from ByteBuffer + * @throws BadFormatException If encoding is invalid + */ + public static String readUTF8String(ByteBuffer bb) throws BadFormatException { + try { + int len = readLength(bb); + if (len == 0) return ""; + + byte[] bs = new byte[len]; + bb.get(bs); + + String s = UTF8_DECODERS.get().decode(ByteBuffer.wrap(bs)).toString(); + return s; + // return new String(bs, StandardCharsets.UTF_8); + } catch (BufferUnderflowException e) { + throw new BadFormatException("Buffer underflow", e); + } catch (CharacterCodingException e) { + throw new BadFormatException("Bad UTF-8 format", e); + } + } + + /** + * Reads a Symbol from a ByteBuffer. Assumes the object tag has already been + * read + * + * @param bb ByteBuffer from which to read a Symbol + * @return Symbol read from ByteBuffer + * @throws BadFormatException If encoding is invalid + */ + public static Symbol readSymbol(ByteBuffer bb) throws BadFormatException { + return Symbol.read(bb); + } + + public static ByteBuffer writeLength(ByteBuffer bb, int i) { + bb = writeVLCLong(bb, i); + return bb; + } + + /** + * Read an int length field (used for Strings etc.) + * + * @param bb ByteBuffer from which to read + * @return Length field + * @throws BadFormatException If encoding is invalid + */ + public static int readLength(ByteBuffer bb) throws BadFormatException { + // our strategy to to read along, then test if it is a valid non-negative int + long l = readVLCLong(bb); + int li = (int) l; + if (l != li) throw new BadFormatException("Bad length, out of integer range: " + l); + if (li < 0) throw new BadFormatException("Negative length: " + li); + return li; + } + + /** + * Writes a 64-bit long as 8 bytes to the ByteBuffer provided + * + * @param bb Destination ByteBuffer + * @param value Value to write + * @return ByteBuffer after writing + */ + public static ByteBuffer writeLong(ByteBuffer bb, long value) { + return bb.putLong(value); + } + + /** + * Reads a 64-bit long as 8 bytes from the ByteBuffer provided + * + * @param bb Destination ByteBuffer + * @return long value + */ + public static long readLong(ByteBuffer bb) { + return bb.getLong(); + } + + /** + * Reads a Ref from the ByteBuffer. + * + * Converts Embedded objects to Refs automatically. + * + * @param Type of referenced value + * @param bb ByteBuffer containing a ref to read + * @return Ref as read from ByteBuffer + * @throws BadFormatException If the data is badly formatted, or a non-embedded + * object is found. + */ + @SuppressWarnings("unchecked") + public static Ref readRef(ByteBuffer bb) throws BadFormatException { + byte tag=bb.get(); + if (tag==Tag.REF) return Ref.readRaw(bb); + ACell cell= Format.read(tag,bb); + if (cell==null) return (Ref) Ref.NULL_VALUE; + return cell.getRef(); + } + + @SuppressWarnings("unchecked") + private static T readDataStructure(ByteBuffer bb, byte tag) throws BadFormatException { + if (tag == Tag.VECTOR) return (T) Vectors.read(bb); + + if (tag == Tag.MAP) return (T) Maps.read(bb); + + if (tag == Tag.SYNTAX) return (T) Syntax.read(bb); + + if (tag == Tag.SET) return (T) Sets.read(bb); + + if (tag == Tag.LIST) return (T) List.read(bb); + + if (tag == Tag.BLOBMAP) return (T) BlobMap.read(bb); + + throw new BadFormatException("Can't read data structure with tag byte: " + tag); + } + + private static ACell readCode(ByteBuffer bb, byte tag) throws BadFormatException { + if (tag == Tag.OP) return Ops.read(bb); + if (tag == Tag.CORE_DEF) { + Symbol sym = Symbol.read(bb); + // TODO: consider if dependency of format on core bad? + ACell o = Core.ENVIRONMENT.get(sym); + if (o == null) throw new BadFormatException("Core definition not found [" + sym + "]"); + return o; + } + + if (tag == Tag.FN_MULTI) { + AFn fn = MultiFn.read(bb); + return fn; + } + + if (tag == Tag.FN) { + AFn fn = Fn.read(bb); + return fn; + } + + throw new BadFormatException("Can't read Op with tag byte: " + Utils.toHexString(tag)); + } + + /** + * Decodes a single Value from a Blob. Assumes the presence of a tag. + * throws an exception if the Blob contents are not fully consumed + * + * @param blob Blob representing the Encoding of the Value + * @return Value read from the blob of encoded data + * @throws BadFormatException In case of encoding error + */ + public static T read(Blob blob) throws BadFormatException { + byte tag = blob.byteAt(0); + return read(tag,blob); + } + + /** + * Read from a Blob with the specified tag + * @param Type of value to read + * @param tag Tag to use for reading + * @param blob Blob to read from + * @return Value decoded + * @throws BadFormatException If encoding is invalid for the given tag + */ + @SuppressWarnings("unchecked") + public static T read(byte tag, Blob blob) throws BadFormatException { + if (tag == Tag.NULL) { + long len=blob.count(); + if (len!=1) throw new BadFormatException("Bad null encoding with length"+len); + return null; + } + if (tag == Tag.BLOB) { + return (T) Blobs.readFromBlob(blob); + } else { + // TODO: maybe refactor to avoid read from byte buffers? + ByteBuffer bb = blob.getByteBuffer().position(1); + T result; + + try { + result = (T) read(tag,bb); + if (bb.hasRemaining()) throw new BadFormatException( + "Blob with type " + Utils.getClass(result) + " has excess bytes: " + bb.remaining()); + } catch (BufferUnderflowException e) { + throw new BadFormatException("Blob has insufficients bytes: " + blob.count(), e); + } + + result.attachEncoding(blob); + return result; + } + } + + /** + * Read a value encoded as a hex string + * @param Type of value to read + * @param hexString A valid hex String + * @return Value read + * @throws BadFormatException If encoding is invalid + */ + public static T read(String hexString) throws BadFormatException { + return read(Blob.fromHex(hexString)); + } + + /** + * Reads a basic type (primitives and numerics) with the given tag + * + * @param bb ByteBuffer to read from + * @param tag Tag byte indicating type to read + * @return Cell value read + + * @throws BadFormatException If encoding is invalid + * @throws BufferUnderflowException if the ByteBuffer contains insufficent bytes for Encoding + */ + @SuppressWarnings("unchecked") + private static T readBasicType(ByteBuffer bb, byte tag) throws BadFormatException, BufferUnderflowException { + try { + if (tag == Tag.NULL) return null; + if (tag == Tag.BYTE) return (T) CVMByte.create(bb.get()); + if (tag == Tag.CHAR) return (T) CVMChar.create(bb.getChar()); + if (tag == Tag.LONG) return (T) CVMLong.create(readVLCLong(bb)); + if (tag == Tag.DOUBLE) return (T) CVMDouble.create(bb.getDouble()); + + throw new BadFormatException("Can't read basic type with tag byte: " + tag); + } catch (IllegalArgumentException e) { + throw new BadFormatException("Format error basic type with tag byte: " + tag); + } + } + + /** + * Reads a Record with the given tag + * + * @param bb ByteBuffer to read from + * @param tag Tag byte indicating type to read + * @return Record value read + * @throws BadFormatException In case of a bad record encoding + */ + @SuppressWarnings("unchecked") + private static T readRecord(ByteBuffer bb, byte tag) throws BadFormatException { + if (tag == Tag.BLOCK) { + return (T) Block.read(bb); + } + if (tag == Tag.STATE) { + return (T) State.read(bb); + } + if (tag == Tag.ORDER) { + return (T) Order.read(bb); + } + if (tag == Tag.BELIEF) { + return (T) Belief.read(bb); + } + + if (tag == Tag.RESULT) { + return (T) Result.read(bb); + } + + if (tag == Tag.BLOCK_RESULT) { + return (T) BlockResult.read(bb); + } + + + throw new BadFormatException("Can't read record type with tag byte: " + tag); + } + + /** + *

+ * Reads one complete Cell from a ByteBuffer. + *

+ * + *

+ * May return any valid Cell (including null) + *

+ * + *

+ * Assumes the presence of an object tag. + *

+ * + * @param bb ByteBuffer from which to read + * @return Value read from the ByteBuffer + * @throws BadFormatException If encoding is invalid + */ + public static T read(ByteBuffer bb) throws BadFormatException { + byte tag = bb.get(); + return read(tag,bb); + } + + @SuppressWarnings("unchecked") + static T read(byte tag,ByteBuffer bb) throws BadFormatException { + try { + if ((tag & 0xF0) == 0x00) return readBasicType(bb, tag); + + if (tag == Tag.STRING) return (T) Strings.read(bb); + if (tag == Tag.BLOB) return (T) Blobs.read(bb); + if (tag == Tag.SYMBOL) return (T) readSymbol(bb); + if (tag == Tag.KEYWORD) return (T) Keyword.read(bb); + + if (tag == Tag.TRUE) return (T) CVMBool.TRUE; + if (tag == Tag.FALSE) return (T) CVMBool.FALSE; + + if (tag == Tag.ADDRESS) return (T) Address.readRaw(bb); + if (tag == Tag.SIGNED_DATA) return (T) SignedData.read(bb); + + if ((tag & 0xF0) == 0x80) return readDataStructure(bb, tag); + + if ((tag & 0xF0) == 0xA0) return (T) readRecord(bb, tag); + + if ((tag & 0xF0) == 0xD0) return (T) readTransaction(bb, tag); + + if (tag == Tag.PEER_STATUS) return (T) PeerStatus.read(bb); + if (tag == Tag.ACCOUNT_STATUS) return (T) AccountStatus.read(bb); + + if ((tag & 0xF0) == 0xC0) return (T) readCode(bb, tag); + } catch (IllegalArgumentException e) { + throw new BadFormatException("Illegal argument reading encoding", e); + } catch (ClassCastException e) { + throw new BadFormatException("Unexpected data type when decoding: "+e.getMessage(), e); + } + + // report error + int pos = bb.position() - 1; + bb.position(0); + ABlob data = Utils.readBufferData(bb); + throw new BadFormatException("Don't recognise tag: " + Utils.toHexString(tag) + " at position " + pos + + " Content: " + data.toHexString()); + } + + static ATransaction readTransaction(ByteBuffer bb, byte tag) throws BadFormatException { + if (tag == Tag.INVOKE) { + return Invoke.read(bb); + } else if (tag == Tag.TRANSFER) { + return Transfer.read(bb); + } else if (tag == Tag.CALL) { + return Call.read(bb); + } + throw new BadFormatException("Can't read Transaction with tag " + Utils.toHexString(tag)); + } + + /** + * Returns true if the object is a canonical data object. Canonical data objects + * can be used as first class decentralised data objects. + * + * @param o Value to test + * @return true if object is canonical, false otherwise. + */ + public static boolean isCanonical(ACell o) { + if (o==null) return true; + return o.isCanonical(); + } + + /** + * Determines if an object should be embedded directly in the encoding rather + * than referenced with a Ref / hash. Defined to be true for most small objects. + * + * @param cell Value to test + * @return true if object is embedded, false otherwise + */ + public static boolean isEmbedded(ACell cell) { + if (cell == null) return true; + return cell.isEmbedded(); + } + + /** + * Gets the encoded Blob for an object in canonical message format + * + * @param o The object to encode + * @return Encoded data as a blob + */ + public static Blob encodedBlob(ACell o) { + if (o==null) return Blob.NULL_ENCODING; + return o.getEncoding(); + } + + /** + * Gets an new encoded ByteBuffer for an Cell in wire format + * + * @param cell The Cell to encode + * @return A ByteBuffer ready to read (i.e. already flipped) + */ + public static ByteBuffer encodedBuffer(ACell cell) { + // estimate size of bytebuffer required, 33 bytes big enough for most small + // stuff + int initialLength; + + if (cell==null) { + return ByteBuffer.wrap(new byte[] {0}).flip(); + } + + ABlob b = cell.cachedEncoding(); + if (b != null) return b.getByteBuffer(); + + initialLength = cell.estimatedEncodingSize(); + + ByteBuffer bb = ByteBuffer.allocate(initialLength); + boolean done = false; + while (!done) { + try { + bb = cell.write(bb); + done = true; + } catch (BufferOverflowException be) { + // retry with larger buffer + bb = ByteBuffer.allocate(bb.capacity() * 2 + 10); + } + } + bb.flip(); + return bb; + } + + /** + * Writes hex digits from digit position start, total length + * + * @param bb ByteBuffer to read from + * @param src Blob containing hex digits + * @param start Start position (in hex digits) + * @param length Length (in hex digits) + * @return ByteBuffer after writing + */ + static ByteBuffer writeHexDigits(ByteBuffer bb, ABlob src, long start, long length) { + bb = Format.writeVLCLong(bb, start); + bb = Format.writeVLCLong(bb, length); + int nBytes = Utils.checkedInt((length + 1) >> 1); + byte[] bs = new byte[nBytes]; + for (int i = 0; i < length; i++) { + Utils.setBits(bs, 4, 4 * ((nBytes * 2) - i - 1), src.getHexDigit(start + i)); + } + bb = bb.put(bs); + return bb; + } + + /** + * Writes hex digits from digit position start, total length. + * + * Fills final hex digit with 0 if length is odd. + * + * @param bs Byte array + * @param pos Position to write into byte array + * @param src Source Blob for hex digits + * @param start Start position in source blob (hex digit number from beginning) + * @param length Number of hex digits to write + * @return position after writing + */ + public static int writeHexDigits(byte[] bs, int pos, ABlob src, long start, long length) { + pos = Format.writeVLCLong(bs,pos, start); + pos = Format.writeVLCLong(bs,pos, length); + int nBytes = Utils.checkedInt((length + 1) >> 1); + byte[] bs2 = new byte[nBytes]; + for (int i = 0; i < nBytes; i++) { + long ix=start+i*2; + int d0=src.getHexDigit(ix); + int d1=((i*2+1)> 1); + byte[] bs = new byte[nBytes]; + bb.get(bs); + if (length < nBytes * 2) { + // test for invalid high bits missing if we have an odd number of digits - + // should be zero + if (Utils.extractBits(bs, 4, 0) != 0) + throw new BadFormatException("Bytes for " + length + " hex digits: " + Utils.toHexString(bs)); + } + + int rBytes = Utils.checkedInt((start + length + 1) >> 1); // bytes covering the specified range completely + byte[] rs = new byte[rBytes]; + + for (int i = 0; i < length; i++) { + int digit = Utils.extractBits(bs, 4, 4 * ((nBytes * 2) - i - 1)); + int di = Utils.checkedInt(4 * ((rBytes * 2) - (start + i) - 1)); + Utils.setBits(rs, 4, di, digit); + } + + return rs; + } + + /** + * Gets a hex String representing an object's encoding + * @param cell Any cell + * @return Hex String + */ + public static String encodedString(ACell cell) { + return encodedBlob(cell).toHexString(); + } + + /** + * Gets a hex String representing an object's encoding. Used in testing only. + * @param o Any object, will be cast to appropriate CVM type + * @return Hex String + */ + public static String encodedString(Object o) { + return encodedString(RT.cvm(o)); + } + + public static int estimateSize(ACell cell) { + if (cell==null) return 1; + return cell.estimatedEncodingSize(); + } + + static boolean canEncodeUFT8(CharSequence s) { + return UTF8_ENCODERS.get().canEncode(s); + } + +} diff --git a/convex-core/src/main/java/convex/core/data/Hash.java b/convex-core/src/main/java/convex/core/data/Hash.java new file mode 100644 index 000000000..fa41e125b --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/Hash.java @@ -0,0 +1,223 @@ +package convex.core.data; + +import java.nio.ByteBuffer; + +import convex.core.Constants; +import convex.core.crypto.Hashing; +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.InvalidDataException; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * Class used to represent an immutable 32-byte Hash value. + * + * The Hash algorithm used may depend on context. + * + * This is intended to help with type safety vs. regular Blob objects and as a + * useful type as a key in relevant data structures. + * + * "Companies spend millions of dollars on firewalls, encryption and secure + * access devices, and it's money wasted, because none of these measures address + * the weakest link in the security chain." - Kevin Mitnick + * + */ +public class Hash extends AArrayBlob { + /** + * Standard length of a Hash in bytes + */ + public static final int LENGTH = Constants.HASH_LENGTH; + + /** + * Type of Hash values + */ + public static final AType TYPE = Types.BLOB; + + private Hash(byte[] hashBytes, int offset) { + super(hashBytes, offset, LENGTH); + } + + private Hash(byte[] hashBytes) { + super(hashBytes, 0, LENGTH); + } + + /* + * Hash of some common constant values These are useful to have pre-calculated + * for efficiency + */ + public static final Hash NULL_HASH = Hashing.sha3(new byte[] { Tag.NULL }); + public static final Hash TRUE_HASH = Hashing.sha3(new byte[] { Tag.TRUE }); + public static final Hash FALSE_HASH = Hashing.sha3(new byte[] { Tag.FALSE }); + public static final Hash EMPTY_HASH = Hashing.sha3(new byte[0]); + + + /** + * Wraps the specified bytes as a Data object Warning: underlying bytes are used + * directly. Use only if no external references to the byte array will be + * retained. + * + * @param hashBytes Bytes to wrap + * @return Hash wrapping the given byte array + */ + public static Hash wrap(byte[] hashBytes) { + return new Hash(hashBytes); + } + + /** + * Wraps the specified blob data as a Hash, sharing the underlying byte array. + * @param data Blob data of correct size for a Hash. Must have at least enough bytes for a Hash + * @return Wrapped data as a Hash + */ + public static Hash wrap(AArrayBlob data) { + if (data instanceof Hash) return (Hash)data; + return wrap(data.getInternalArray(),data.getInternalOffset()); + } + + /** + * Wraps the specified bytes as a Data object Warning: underlying bytes are used + * directly. Use only if no external references to the byte array will be + * retained. + * + * @param hashBytes Byte array containing hash value + * @param offset Offset into byte array for start of hash value + * @return Hash wrapping the given byte array segment + */ + public static Hash wrap(byte[] hashBytes, int offset) { + if ((offset < 0) || (offset + LENGTH > hashBytes.length)) + throw new IllegalArgumentException(Errors.badRange(offset, LENGTH)); + return new Hash(hashBytes, offset); + } + + @Override + public boolean equals(ABlob other) { + if (other==null) return false; + if (other instanceof Hash) return equals((Hash)other); + if (other.count()!=LENGTH) return false; + if (other.getType()!=TYPE) return false; + return other.equalsBytes(this.store, this.offset); + } + + /** + * Tests if the Hash value is precisely equal to another non-null Hash value. + * + * @param other Hash to comapre with + * @return true if Hashes are equal, false otherwise. + */ + public boolean equals(Hash other) { + if (other == this) return true; + return Utils.arrayEquals(other.store, other.offset, this.store, this.offset, LENGTH); + } + + /** + * Get the first 32 bits of this Hash. Used for Java hashCodes + * @return Int representing the first 32 bits + */ + public int firstInt() { + return Utils.readInt(this.store, this.offset); + } + + /** + * Constructs a Hash object from a hex string + * + * @param hexString Hex String + * @return Hash with the given hex string value, or null is String is not valid + */ + public static Hash fromHex(String hexString) { + byte [] bs=Utils.hexToBytes(hexString); + if (bs==null) return null; + if (bs.length!=LENGTH) return null; + return wrap(bs); + } + + public static Hash wrap(AArrayBlob data, int offset, int length) { + return wrap(data.store, data.offset + offset); + } + + /** + * Computes the Hash for any ACell value. + * + * May return a cached Hash if available in memory. + * + * @param value Any Cell + * @return Hash of the encoded data for the given value + */ + public static Hash compute(ACell value) { + if (value == null) return NULL_HASH; + return value.getHash(); + } + + /** + * Reads a Hash from a ByteBuffer Assumes no Tag, i.e. just Hash.LENGTH for the + * hash is read. + * + * @param bb ByteBuffer to read from + * @return Hash object read from ByteBuffer + */ + public static Hash readRaw(ByteBuffer bb) { + byte[] bs = new byte[Hash.LENGTH]; + bb.get(bs); + return Hash.wrap(bs); + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.BLOB; + bs[pos++]=LENGTH; + return encodeRaw(bs,pos); + } + + @Override + public boolean isCanonical() { + // always canonical, since class invariants are maintained + return false; + } + + @Override public final boolean isCVMValue() { + return true; + } + + @Override + public int estimatedEncodingSize() { + // tag plus raw data + return 1 + LENGTH; + } + + @Override + public long getEncodingLength() { + // Always a fixed encoding length, tag plus count plus length + return 2 + LENGTH; + } + + @Override + public Blob getChunk(long i) { + if (i != 0) throw new IndexOutOfBoundsException(Errors.badIndex(i)); + return toBlob(); + } + + @Override + public void validateCell() throws InvalidDataException { + if (length != LENGTH) throw new InvalidDataException("Address length must be 32 bytes = 256 bits", this); + } + + @Override + public boolean isEmbedded() { + // Hashes are always small enough to embed + return true; + } + + @Override + public boolean isRegularBlob() { + return true; + } + + @Override + public byte getTag() { + return Tag.BLOB; + } + + @Override + public Blob toCanonical() { + return toBlob(); + } +} diff --git a/convex-core/src/main/java/convex/core/data/IAssociative.java b/convex-core/src/main/java/convex/core/data/IAssociative.java new file mode 100644 index 000000000..e47afae4e --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/IAssociative.java @@ -0,0 +1,12 @@ +package convex.core.data; + +/** + * Interface for associative data structures + * + * @param Type of associative keys + * @param Type of associative values + */ +public interface IAssociative { + + +} diff --git a/convex-core/src/main/java/convex/core/data/INumeric.java b/convex-core/src/main/java/convex/core/data/INumeric.java new file mode 100644 index 000000000..52b31978a --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/INumeric.java @@ -0,0 +1,33 @@ +package convex.core.data; + +import convex.core.data.prim.APrimitive; +import convex.core.data.prim.CVMDouble; +import convex.core.data.prim.CVMLong; + +/** + * Interface for CVM Numeric types + */ +public interface INumeric { + + + public CVMLong toLong(); + + public CVMDouble toDouble(); + + public double doubleValue(); + + /** + * Gets the numeric type that should be used as for calculations + * @return Double.class or Long.class, or null if not a numeric type + */ + public Class numericType(); + + /** + * Gets the signum of this numerical value. Will be -1, 0 or 1 for Longs, -1.0, 0.0 , 1.0 or ##NaN for doubles. + * @return Signum of the numeric value + */ + public APrimitive signum(); + + public INumeric toStandardNumber(); + +} diff --git a/convex-core/src/main/java/convex/core/data/IRefFunction.java b/convex-core/src/main/java/convex/core/data/IRefFunction.java new file mode 100644 index 000000000..83b85446c --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/IRefFunction.java @@ -0,0 +1,16 @@ +package convex.core.data; + +/** + * Functional interface for operations on Cell Refs that may throw a + * MissingDataException + * + * In general, IRefFunction is used to provide a visitor for data objects containing nested Refs. + */ +@FunctionalInterface +public interface IRefFunction { + + // Note we can't have a generic type parameter in a functional interface. + // So using a wildcard seems the best option? + + public Ref apply(Ref t); +} diff --git a/convex-core/src/main/java/convex/core/data/IValidated.java b/convex-core/src/main/java/convex/core/data/IValidated.java new file mode 100644 index 000000000..4b53f0297 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/IValidated.java @@ -0,0 +1,26 @@ +package convex.core.data; + +import convex.core.exceptions.InvalidDataException; + +/** + * Interface for classes that can be validated + */ +public interface IValidated { + + /** + * Validates the complete structure of this object. + * + * It is necessary to ensure all child Refs are validated, so the general contract for validate is: + * + *
    + *
  1. Call super.validate() - which will indirectly call validateCell()
  2. + *
  3. Call validate() on any contained cells in this class
  4. + *
+ * + * @throws InvalidDataException If the data Valie is invalid in any way + */ + public void validate() throws InvalidDataException; + + + +} diff --git a/convex-core/src/main/java/convex/core/data/IWriteable.java b/convex-core/src/main/java/convex/core/data/IWriteable.java new file mode 100644 index 000000000..7ec54521c --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/IWriteable.java @@ -0,0 +1,31 @@ +package convex.core.data; + +import java.nio.ByteBuffer; + +public interface IWriteable { + /** + * Writes this object to a byte array including an appropriate message tag + * + * @param bs byte array to write this object to + * @param pos position at which to write the value + * @return The updated position + */ + public int encode(byte[] bs, int pos); + + /** + * Writes this object to a ByteBuffer including an appropriate message tag + * + * @param bb ByteBuffer to write to + * @return The updated ByteBuffer + */ + public ByteBuffer write(ByteBuffer bb); + + /** + * Estimate the encoded data size for this Cell. Used for quickly sizing buffers. + * Implementations should try to return a size that is likely to contain the entire object + * when represented in binary format, including the tag byte. + * + * @return The estimated size for the binary representation of this object. + */ + public abstract int estimatedEncodingSize(); +} diff --git a/convex-core/src/main/java/convex/core/data/Keyword.java b/convex-core/src/main/java/convex/core/data/Keyword.java new file mode 100644 index 000000000..daff8d0b6 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/Keyword.java @@ -0,0 +1,150 @@ +package convex.core.data; + +import java.nio.ByteBuffer; + +import convex.core.Constants; +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; + +/** + * Keyword data type. Intended as human-readable map keys, tags and option + * specifiers etc. + * + * Keywords evaluate to themselves, and as such can be considered as literal + * constants. + * + * "Programs must be written for people to read, and only incidentally for + * machines to execute." ― Harold Abelson + */ +public class Keyword extends ASymbolic implements Comparable { + + /** Maximum size of a Keyword in UTF-16 chars representation */ + public static final int MAX_CHARS = Constants.MAX_NAME_LENGTH; + + /** Minimum size of a Keyword in UTF-16 chars representation */ + public static final int MIN_CHARS = 1; + + private Keyword(String name) { + super(name); + } + + public AType getType() { + return Types.KEYWORD; + } + + /** + * Creates a Keyword with the given name + * + * @param name A String to use as the keyword name + * @return The new Keyword, or null if the name is invalid for a Keyword + */ + public static Keyword create(String name) { + if (!validateName(name)) { + return null; + } + return new Keyword(name); + } + + public static Keyword create(AString name) { + if (name==null) return null; + return create(name.toString()); + } + + /** + * Creates a Keyword with the given name, throwing an exception if name is not + * valid + * + * @param aString A String of at least 1 and no more than 64 UTF-8 bytes in length + * @return The new Keyword + */ + public static Keyword createChecked(AString aString) { + Keyword k = create(aString); + if (k == null) throw new IllegalArgumentException("Invalid keyword name: " + aString); + return k; + } + + public static Keyword createChecked(String aString) { + Keyword k = create(aString); + if (k == null) throw new IllegalArgumentException("Invalid keyword name: " + aString); + return k; + } + + + @Override + public boolean isCanonical() { + return true; + } + + + /** + * Reads a Keyword from the given ByteBuffer, assuming tag already consumed + * + * @param bb ByteBuffer source + * @return The Keyword read + * @throws BadFormatException If a Keyword could not be read correctly + */ + public static Keyword read(ByteBuffer bb) throws BadFormatException { + String name=Format.readUTF8String(bb); + Keyword kw = Keyword.create(name); + if (kw == null) throw new BadFormatException("Can't read symbol"); + return kw; + + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.KEYWORD; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + return Format.writeRawUTF8String(bs, pos, name); + } + + @Override + public void print(StringBuilder sb) { + sb.append(':'); + sb.append(name); + } + + @Override + public int estimatedEncodingSize() { + return name.length()*2+3; + } + + @Override + public boolean equals(ACell other) { + if (other == this) return true; + if (!(other instanceof Keyword)) return false; + return name.equals(((Keyword) other).name); + } + + @Override + public int compareTo(Keyword k) { + return name.compareTo(k.name); + } + + @Override + public void validateCell() throws InvalidDataException { + super.validateCell(); + } + + @Override + public int getRefCount() { + return 0; + } + + @Override + public byte getTag() { + return Tag.KEYWORD; + } + + @Override + public ACell toCanonical() { + return this; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/Keywords.java b/convex-core/src/main/java/convex/core/data/Keywords.java new file mode 100644 index 000000000..d20c68ec2 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/Keywords.java @@ -0,0 +1,89 @@ +package convex.core.data; + +/** + * Static Keyword values for configuration maps, records etc. + */ +public class Keywords { + + public static final Keyword STATE = Keyword.create("state"); + public static final Keyword KEYPAIR = Keyword.create("keypair"); + public static final Keyword PORT = Keyword.create("port"); + public static final Keyword ORDERS = Keyword.create("orders"); + public static final Keyword TRANSACTIONS = Keyword.create("transactions"); + public static final Keyword TIMESTAMP = Keyword.create("timestamp"); + public static final Keyword ACCOUNTS = Keyword.create("accounts"); + public static final Keyword PEERS = Keyword.create("peers"); + public static final Keyword BELIEF = Keyword.create("belief"); + public static final Keyword STATES = Keyword.create("states"); + public static final Keyword RESULTS = Keyword.create("results"); + public static final Keyword PERSIST = Keyword.create("persist"); + + + public static final Keyword STORE = Keyword.create("store"); + public static final Keyword RESTORE = Keyword.create("restore"); + + // for testing and suchlike + public static final Keyword FOO = Keyword.create("foo"); + public static final Keyword BAR = Keyword.create("bar"); + public static final Keyword BAZ = Keyword.create("baz"); + + + public static final Keyword SALT = Keyword.create("salt"); + public static final Keyword IV = Keyword.create("iv"); + public static final Keyword ROUNDS = Keyword.create("rounds"); + public static final Keyword CIPHERTEXT = Keyword.create("ciphertext"); + public static final Keyword GLOBALS = Keyword.create("globals"); + public static final Keyword SCHEDULE = Keyword.create("schedule"); + + public static final Keyword NAME = Keyword.create("name"); + + // source info + public static final Keyword START = Keyword.create("start"); + public static final Keyword END = Keyword.create("end"); + public static final Keyword SOURCE = Keyword.create("source"); + public static final Keyword TAG = Keyword.create("tag"); + public static final Keyword DOC = Keyword.create("doc"); + public static final Keyword DESCRIPTION = Keyword.create("description"); + public static final Keyword EXAMPLES = Keyword.create("examples"); + public static final Keyword CODE = Keyword.create("code"); + + public static final Keyword TYPE = Keyword.create("type"); + public static final Keyword SPECIAL_Q = Keyword.create("special?"); + + public static final Keyword PEER = Keyword.create("peer"); + public static final Keyword STAKE = Keyword.create("stake"); + public static final Keyword STAKES = Keyword.create("stakes"); + public static final Keyword DELEGATED_STAKE = Keyword.create("delegated-stake"); + public static final Keyword OWNER = Keyword.create("owner"); + + public static final Keyword HOST = Keyword.create("host"); + public static final Keyword URL = Keyword.create("url"); + + public static final Keyword SEQUENCE = Keyword.create("sequence"); + public static final Keyword BALANCE = Keyword.create("balance"); + public static final Keyword ENVIRONMENT = Keyword.create("environment"); + public static final Keyword HOLDINGS = Keyword.create("holdings"); + public static final Keyword ALLOWANCE = Keyword.create("allowance"); + public static final Keyword CONTROLLER = Keyword.create("controller"); + public static final Keyword KEY = Keyword.create("key"); + + public static final Keyword ID = Keyword.create("id"); + public static final Keyword RESULT = Keyword.create("result"); + public static final Keyword ERROR_CODE = Keyword.create("error-code"); + public static final Keyword TRACE = Keyword.create("trace"); + + public static final Keyword EXPANDER_Q = Keyword.create("expander?"); + public static final Keyword MACRO = Keyword.create("macro"); + + public static final Keyword CALLABLE_Q = Keyword.create("callable?"); + + public static final Keyword VALUE = Keyword.create("value"); + public static final Keyword FUNCTION = Keyword.create("function"); + public static final Keyword METADATA = Keyword.create("metadata"); + + + public static final Keyword OUTGOING_CONNECTIONS = Keyword.create("outgoing-connections"); + public static final Keyword AUTO_MANAGE = Keyword.create("auto-manage"); + public static final Keyword TIMEOUT = Keyword.create("timeout"); + public static final Keyword EVENT_HOOK = Keyword.create("event-hook"); +} diff --git a/convex-core/src/main/java/convex/core/data/List.java b/convex-core/src/main/java/convex/core/data/List.java new file mode 100644 index 000000000..8576e2232 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/List.java @@ -0,0 +1,425 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.ListIterator; +import java.util.function.Consumer; +import java.util.function.Function; + +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * Implementation of a list wrapping a vector. + * + * Note that we embed the vector directly, avoiding going via a Ref. This is + * important for serialisation efficiency / avoiding excess Cells. + * + * "One can even conjecture that Lisp owes its survival specifically to the fact + * that its programs are lists, which everyone, including me, has regarded as a + * disadvantage." - John McCarthy, "Early History of Lisp" + * + * @param Type of List elements + */ +public class List extends AList { + + public static final List EMPTY = wrap(VectorLeaf.EMPTY); + + public static final Ref> EMPTY_REF = EMPTY.getRef(); + + static { + // Set empty Ref flags as internal embedded constant + EMPTY_REF.setFlags(Ref.INTERNAL_FLAGS); + } + + /** + * Wrapped vector containing reversed elements + */ + private AVector data; + + private List(AVector data) { + super(data.count); + this.data = data.toVector(); // ensure canonical, not a mapentry etc. + } + + /** + * Wraps a Vector as a list (will reverse element order) + * @param Type of elements + * @param vector Vector to wrap + * @return New List instance + */ + public static List wrap(AVector vector) { + return new List(vector); + } + + /** + * Creates a List containing the elements of the provided vector in reverse + * order + * + * @param Type of list elements + * @param vector Vector to reverse into a List + * @return Vector representing this list in reverse order + */ + @SuppressWarnings("unchecked") + public static List reverse(AVector vector) { + if (vector.isEmpty()) return (List) Lists.empty(); + return new List(vector); + } + + @SuppressWarnings("unchecked") + public static List of(Object... elements) { + if (elements.length == 0) return (List) Lists.empty(); + Utils.reverse(elements); + return new List(Vectors.of(elements)); + } + + /*** + * Creates a list wrapping the given array. May destructively alter the array + * @param Type of element + * @param args Elements to include + * @return New List + */ + @SuppressWarnings("unchecked") + public static List create(ACell... args) { + if (args.length==0) return (List) Lists.empty(); + Utils.reverse(args); + return new List(Vectors.create(args)); + } + + @Override + public Object[] toArray() { + Object[] arr = data.toArray(); + Utils.reverse(arr, size()); + return arr; + } + + @Override + public V[] toArray(V[] a) { + V[] arr = data.toArray(a); + Utils.reverse(arr, size()); + return arr; + } + + @Override + public T get(long index) { + return data.get(count - 1 - index); + } + + @Override + public Ref getElementRef(long i) { + return data.getElementRef(count - 1 - i); + } + + @SuppressWarnings("unchecked") + @Override + public AList assoc(long i, R value) { + AVector newData; + newData = data.assoc(count - 1 - i, value); + if (data == newData) return (AList) this; + if (newData==null) return null; + return new List<>(newData); + } + + @Override + public int indexOf(Object o) { + int pos = data.lastIndexOf(o); + if (pos < 0) return -1; + return size() - 1 - pos; + } + + @Override + public int lastIndexOf(Object o) { + int pos = data.indexOf(o); + if (pos < 0) return -1; + return size() - 1 - pos; + } + + @Override + public long longIndexOf(Object o) { + long pos = data.longLastIndexOf(o); + if (pos < 0) return -1; + return count - 1 - pos; + } + + @Override + public long longLastIndexOf(Object o) { + long pos = data.longIndexOf(o); + if (pos < 0) return -1; + return count - 1 - pos; + } + + @Override + public ListIterator listIterator() { + return new MyListIterator(0); + } + + @Override + public ListIterator listIterator(int index) { + return new MyListIterator(index); + } + + @Override + public ListIterator listIterator(long index) { + return new MyListIterator(index); + } + + private class MyListIterator implements ListIterator { + + private final ListIterator dataIterator; + + public MyListIterator(long pos) { + this.dataIterator = data.listIterator(count - pos); + } + + @Override + public boolean hasNext() { + return dataIterator.hasPrevious(); + } + + @Override + public T next() { + return dataIterator.previous(); + } + + @Override + public boolean hasPrevious() { + return dataIterator.hasNext(); + } + + @Override + public T previous() { + return dataIterator.next(); + } + + @Override + public int nextIndex() { + return Utils.checkedInt(count - 1 - dataIterator.previousIndex()); + } + + @Override + public int previousIndex() { + return Utils.checkedInt(count - 1 - dataIterator.nextIndex()); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public void set(T e) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public void add(T e) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + } + + @Override + public int getRefCount() { + return data.getRefCount(); + } + + @Override + public Ref getRef(int i) { + return data.getRef(i); + } + + @Override + public List updateRefs(IRefFunction func) { + AVector newData = (AVector) data.updateRefs(func); + if (newData == data) return this; + return new List(newData); + } + + @Override + public boolean isCanonical() { + return true; + } + + @Override public final boolean isCVMValue() { + return data.isCVMValue(); + } + + @Override + public void print(StringBuilder sb) { + sb.append('('); + long n = count; + for (long i = 0; i < n; i++) { + if (i > 0) sb.append(' '); + Utils.print(sb,data.get(n - 1 - i)); + } + sb.append(')'); + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.LIST; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = data.encodeRaw(bs,pos); + return pos; + } + + /** + * Reads a List from the specified bytebuffer. Assumes Tag byte already consumed. + * @param bb ByteBuffer to read from + * @return List instance read from ByteBuffer + * @throws BadFormatException If Encoding is invalid + * + */ + public static List read(ByteBuffer bb) throws BadFormatException { + try { + AVector data = Vectors.read(bb); + if (data == null) throw new BadFormatException("Expected vector but got null in List format"); + return new List(data); + } catch (ClassCastException e) { + throw new BadFormatException("Expected vector in List format", e); + } + } + + @Override + public Iterator iterator() { + return listIterator(); + } + + @Override + public int estimatedEncodingSize() { + return data.estimatedEncodingSize(); + } + + /** + * Prepends an element to the list in first position. + * + * @param value Value to prepend + * @return Updated list + */ + @Override + public List conj(R value) { + return new List((AVector) data.conj(value)); + } + + @Override + public AList cons(T x) { + return new List((AVector) data.conj(x)); + } + + public List conjAll(ACollection xs) { + return reverse(data.conjAll(xs)); + } + + + @SuppressWarnings("unchecked") + @Override + public AVector toVector() { + return (AVector) Vectors.create(toCellArray()); + } + + @Override + public ASequence next() { + if (count <= 1) return null; + return slice(1, count - 1); + } + + @Override + public ASequence slice(long start, long length) { + long end = start + length; + if ((start == 0) && (end == count)) return this; + return reverse(data.slice(count - end, length)); + } + + @Override + public AList map(Function mapper) { + // TODO: reverse map order? + return new List<>(data.map(mapper)); + } + + @SuppressWarnings("unchecked") + @Override + public List concat(ASequence vals) { + AVector rvals; + if (vals instanceof List) { + rvals = ((List) vals).data; + } else { + rvals = Vectors.empty(); + long n = vals.count(); + for (long i = 0; i < n; i++) { + rvals = rvals.conj(vals.get(n - i - 1)); + } + } + return List.reverse(rvals.concat((AVector)data)); + } + + @Override + public void visitElementRefs(Consumer> f) { + data.visitElementRefs(f); + } + + @SuppressWarnings("unchecked") + @Override + public AVector subVector(long start, long length) { + checkRange(start, length); + + // Create using an Object array. Probably fastest? + ACell[] arr = new ACell[Utils.checkedInt(length)]; + for (int i = 0; i < length; i++) { + arr[i] = get(start + i); + } + return (AVector) Vectors.create(arr); + } + + @Override + public void validateCell() throws InvalidDataException { + long dc = data.count(); + if (count != dc) + throw new InvalidDataException("Bad data count " + count + " with underlying data count " + dc, this); + data.validateCell(); + } + + @Override + public void forEach(Consumer action) { + data.forEach(action); + } + + @Override + public AList drop(long n) { + if (n==0) return this; + long newLen=count-n; + if (newLen<0) return null; + if (newLen==0) return Lists.empty(); + return new List(data.subVector(0, newLen)); + } + + @Override + public byte getTag() { + return Tag.LIST; + } + + + @Override + public AVector reverse() { + return data; + } + + @SuppressWarnings("unchecked") + @Override + protected void copyToArray(R[] arr, int offset) { + int n=Utils.checkedInt(count); + for (int i=0; i AList empty() { + return (List) List.EMPTY; + } + + @SuppressWarnings("unchecked") + public static > L create(java.util.List list) { + return (L) List.of(list.toArray()); + } + + @SafeVarargs + public static AList of(Object... vals) { + return List.of(vals); + } +} diff --git a/convex-core/src/main/java/convex/core/data/LongBlob.java b/convex-core/src/main/java/convex/core/data/LongBlob.java new file mode 100644 index 000000000..9ab930f4b --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/LongBlob.java @@ -0,0 +1,146 @@ +package convex.core.data; + +import java.security.MessageDigest; + +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * Wrapper for an 8-byte long blob + * + * We use this for efficient management of indexes using longs in BlobMaps. + * + */ +public final class LongBlob extends ALongBlob { + + private LongBlob(long value) { + super(value); + } + + public static LongBlob create(String string) { + byte[] bs = Utils.hexToBytes(string); + if (bs.length != LENGTH) throw new IllegalArgumentException("Long blob requires a length 8 hex string"); + return new LongBlob(Utils.readLong(bs, 0)); + } + + public static LongBlob create(long value) { + return new LongBlob(value); + } + + + @Override public final boolean isCVMValue() { + return true; + } + + @Override + public void getBytes(byte[] dest, int destOffset) { + Utils.writeLong(dest, destOffset,value); + } + + @Override + public ABlob slice(long start, long length) { + if ((start == 0) && (length == LENGTH)) return this; + + if (start < 0) throw new IndexOutOfBoundsException(Errors.badRange(start, length)); + return getEncoding().slice(start + 2, length); + } + + @Override + public Blob toBlob() { + return getEncoding().slice(2, LENGTH); + } + + @Override + protected void updateDigest(MessageDigest digest) { + byte[] bs = getEncoding().getInternalArray(); + digest.update(bs, 2, (int) LENGTH); + } + + @Override + public int getHexDigit(long i) { + if ((i < 0) || (i >= LENGTH * 2)) throw new IndexOutOfBoundsException(Errors.badIndex(i)); + return 0x0F & (int) (value >> ((LENGTH * 2 - i - 1) * 4)); + } + + @Override + public long commonHexPrefixLength(ABlob b) { + if (b == this) return LENGTH * 2; + + long max = Math.min(LENGTH, b.count()); + for (long i = 0; i < max; i++) { + byte ai = getUnchecked(i); + byte bi = b.getUnchecked(i); + if (ai != bi) return (i * 2) + (Utils.firstDigitMatch(ai, bi) ? 1 : 0); + } + return max * 2; + } + + @Override + public boolean equals(ABlob a) { + if (a instanceof LongBlob) return (((LongBlob) a).value == value); + if (a instanceof Blob) { + Blob b=(Blob)a; + return ((b.count()==LENGTH)&& (b.longValue()== value)); + } + return false; + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.BLOB; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + bs[pos++]=((byte) 8); + Utils.writeLong(bs, pos, value); + return pos+8; + } + + @Override + public int estimatedEncodingSize() { + return (int) (2 + LENGTH); + } + + @Override + public long longValue() { + return value; + } + + @Override + public long hexMatchLength(ABlob b, long start, long length) { + if (b == this) return length; + long end = start + length; + for (long i = start; i < end; i++) { + if (!(getHexDigit(i) == b.getHexDigit(i))) return i - start; + } + return length; + } + + @Override + public boolean isRegularBlob() { + return true; + } + + @Override + public byte getTag() { + return Tag.BLOB; + } + + @Override + public boolean equalsBytes(byte[] bytes, int byteOffset) { + return value==Utils.readLong(bytes, byteOffset); + } + + @Override + public boolean isCanonical() { + return false; + } + + @Override + public Blob toCanonical() { + return toBlob(); + } + +} diff --git a/convex-core/src/main/java/convex/core/data/MapEntry.java b/convex-core/src/main/java/convex/core/data/MapEntry.java new file mode 100644 index 000000000..ea5b77dd4 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/MapEntry.java @@ -0,0 +1,345 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.RT; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * Map.Entry implementation for persistent maps. + * + * Contains exactly 2 Refs, one for key and one for value + * + * Implements Comparable using the hash value of keys. + * + * @param The type of keys + * @param The type of values + */ +public class MapEntry extends AMapEntry implements Comparable> { + + private final Ref keyRef; + private final Ref valueRef; + + private MapEntry(Ref key, Ref value) { + super(2); + this.keyRef = key; + this.valueRef = value; + } + + @Override + public AType getType() { + return Types.VECTOR; + } + + @SuppressWarnings("unchecked") + public static MapEntry createRef(Ref keyRef, Ref valueRef) { + // ensure we have a hash at least + return new MapEntry((Ref) keyRef, (Ref) valueRef); + } + + /** + * Creates a new MapEntry with the provided key and value + * @param Type of Key + * @param Type of value + * @param key Key to use for MapEntry + * @param value Value to use for MapEntry + * @return New MapEntry instance + */ + public static MapEntry create(K key, V value) { + return createRef(Ref.get(key), Ref.get(value)); + } + + /** + * Create a map entry, converting key and value to correct CVM types. + * @param Type of Keys + * @param Type of Values + * @param key Key to use for map entry + * @param value Value to use for map entry + * @return New MapEntry + */ + public static MapEntry of(Object key, Object value) { + return create(RT.cvm(key),RT.cvm(value)); + } + + @Override + public MapEntry withValue(V value) { + if (value == getValue()) return this; + return new MapEntry(keyRef, Ref.get(value)); + } + + @SuppressWarnings("unchecked") + @Override + public AVector assoc(long i, R a) { + if (i == 0) return (AVector) withKey((K) a); + if (i == 1) return (AVector) withValue((V) a); + return null; + } + + @Override + protected MapEntry withKey(K key) { + if (key == getKey()) return this; + return new MapEntry(Ref.get(key), valueRef); + } + + @Override + public K getKey() { + return keyRef.getValue(); + } + + @Override + public AVector map(Function mapper) { + return Vectors.of(mapper.apply(getKey()), mapper.apply(getValue())); + } + + @Override + public R reduce(BiFunction func, R value) { + R result = func.apply(value, getKey()); + result = func.apply(result, getKey()); + return result; + } + + @SuppressWarnings("unchecked") + @Override + protected void copyToArray(R[] arr, int offset) { + arr[offset] = (R) getKey(); + arr[offset + 1] = (R) getValue(); + } + + /** + * Gets the hash of the key for this MapEntry + * + * @return the Hash of the Key + */ + public Hash getKeyHash() { + return getKeyRef().getHash(); + } + + @Override + public V getValue() { + return valueRef.getValue(); + } + + public Ref getKeyRef() { + return keyRef; + } + + public Ref getValueRef() { + return valueRef; + } + + /** + * Reads a MapEntry from a ByteBuffer. Assumes Tag already handled. + * @param bb ByteBuffer to read from + * @return MapEntry instance + * @throws BadFormatException If encoding is invalid + */ + public static MapEntry read(ByteBuffer bb) throws BadFormatException { + Ref kr = Format.readRef(bb); + Ref vr = Format.readRef(bb); + return new MapEntry(kr, vr); + } + + /** + * Reads a MapEntry or null from a ByteBuffer. Assumes no Tag. + * @param bb ByteBuffer to read from + * @return MapEntry instance, or null + * @throws BadFormatException If encoding is invalid + */ + public static MapEntry readCompressed(ByteBuffer bb) throws BadFormatException { + byte b=bb.get(); + if (b==Tag.NULL) return null; + if (b!=Tag.VECTOR) throw new BadFormatException("Bad header byte for compressed MapEntry: "+Utils.toHexString(b)); + Ref kr = Format.readRef(bb); + Ref vr = Format.readRef(bb); + return new MapEntry(kr, vr); + } + + @Override + public int compareTo(MapEntry o) { + if (this == o) return 0; + return keyRef.compareTo(o.keyRef); + } + + @Override + public int getRefCount() { + return 2; + } + + @SuppressWarnings("unchecked") + @Override + public Ref getRef(int i) { + if ((i >> 1) != 0) throw new IndexOutOfBoundsException(i); + return (Ref) (((i & 1) == 0) ? keyRef : valueRef); + } + + @SuppressWarnings("unchecked") + @Override + public MapEntry updateRefs(IRefFunction func) { + Ref newKeyRef = (Ref) func.apply(keyRef); + Ref newValueRef = (Ref) func.apply(valueRef); + if ((keyRef == newKeyRef) && (valueRef == newValueRef)) return this; + return new MapEntry(newKeyRef, newValueRef); + } + + @Override + public boolean equals(ACell o) { + if (o==null) return false; + if (o.getTag()!=Tag.VECTOR) return false; + AVector v=(AVector) o; + if (v.count()!=2) return false; + return getEncoding().equals(o.getEncoding()); + } + + public boolean equals(MapEntry b) { + if (this == b) return true; + return keyRef.equals(b.keyRef) && valueRef.equals(b.valueRef); + } + + /** + * Checks if the keys of two map entries are equal + * + * @param b MapEntry to compare with this MapEntry + * @return true if this entry's key equals that of the other entry, false + * otherwise. + */ + public boolean keyEquals(MapEntry b) { + return keyRef.equals(b.keyRef); + } + + @Override + @SuppressWarnings("unchecked") + public AVector toVector() { + return new VectorLeaf(new Ref[] { keyRef, valueRef }); + } + + @Override + public boolean contains(Object o) { + return (Utils.equals(o, getKey()) || Utils.equals(o, getValue())); + } + + @Override + public ACell get(long i) { + if (i == 0) return getKey(); + if (i == 1) return getValue(); + throw new IndexOutOfBoundsException(Errors.badIndex(i)); + } + + @SuppressWarnings("unchecked") + @Override + public Ref getElementRef(long i) { + if (i == 0) return (Ref) keyRef; + if (i == 1) return (Ref) valueRef; + throw new IndexOutOfBoundsException(Errors.badIndex(i)); + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.VECTOR; + pos = Format.writeVLCLong(bs,pos, 2); // Size of 2, to match VectorLeaf encoding + return encodeRaw(bs,pos); + } + + /** + * Writes the raw MapEntry content. Puts the key and value Refs onto the given + * ByteBuffer + * + * @param bs Byte array to write to + * @return Updated position after writing + */ + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = keyRef.encode(bs,pos); + pos = valueRef.encode(bs,pos); + return pos; + } + + /** + * Writes a MapEntry or null content in compressed format (no count). Useful for + * embedding an optional MapEntry inside a larger Encoding + * + * @param me MapEntry to encode + * @param bs Byte array to write to + * @param pos Starting position for encoding in byte array + * @return Updated position after writing + */ + public static int encodeCompressed(MapEntry me,byte[] bs, int pos) { + if (me==null) { + bs[pos++]=Tag.NULL; + } else { + bs[pos++]=Tag.VECTOR; + pos = me.encodeRaw(bs,pos); + } + return pos; + } + + + @Override public final boolean isCVMValue() { + return true; + } + + @Override + public int estimatedEncodingSize() { + return 1+Format.MAX_EMBEDDED_LENGTH*2; // header plus two embedded objects + } + + @SuppressWarnings("unchecked") + @Override + public void visitElementRefs(Consumer> f) { + f.accept((Ref) keyRef); + f.accept((Ref) valueRef); + } + + @Override + public AVector concat(ASequence b) { + return toVector().concat(b); + } + + @Override + public AVector subVector(long start, long length) { + AVector vec=toVector(); + return vec.subVector(start, length); + } + + @Override + public void validate() throws InvalidDataException { + super.validate(); + keyRef.validate(); + valueRef.validate(); + } + + + @Override + public void validateCell() throws InvalidDataException { + // TODO: is there really Nothing to do? + } + + @Override + public byte getTag() { + return Tag.VECTOR; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static MapEntry convertOrNull(AVector v) { + if (v.count()!=2) return null; + return createRef(v.getElementRef(0),v.getElementRef(1)); + } + + @Override + public boolean isCanonical() { + return false; + } + + @Override + public ACell toCanonical() { + // Vector is the canonical form of a MapEntry + return toVector(); + } +} diff --git a/convex-core/src/main/java/convex/core/data/MapLeaf.java b/convex-core/src/main/java/convex/core/data/MapLeaf.java new file mode 100644 index 000000000..ef8bdd5b4 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/MapLeaf.java @@ -0,0 +1,757 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; + +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.exceptions.TODOException; +import convex.core.util.MergeFunction; +import convex.core.util.Utils; + +/** + * Limited size Persistent Merkle Map implemented as a small sorted list of + * Key/Value pairs + * + * Must be sorted by Key hash value to ensure uniqueness of representation + * + * @param Type of keys + * @param Type of values + */ +public class MapLeaf extends AHashMap { + /** + * Maximum number of entries in a MapLeaf + */ + public static final int MAX_ENTRIES = 8; + + static final MapEntry[] EMPTY_ENTRIES = new MapEntry[0]; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static final MapLeaf EMPTY = new MapLeaf(EMPTY_ENTRIES); + + private final MapEntry[] entries; + + private MapLeaf(MapEntry[] items) { + super(items.length); + entries = items; + } + + /** + * Creates a ListMap with the specified entries. Entries must have distinct keys + * but may otherwise be specified in any order. + * + * Null entries are ignored/removed. + * + * @param entries Entries for map + * @return New ListMap + */ + public static MapLeaf create(MapEntry[] entries) { + return create(entries, 0, entries.length); + } + + /** + * Creates a ListMap with the specified entries. Null entries are + * ignored/removed. + * + * @param entries + * @param offset Offset into entries array + * @param length Number of entries to take from entries array, starting at + * offset + * @return A new ListMap + */ + protected static MapLeaf create(MapEntry[] entries, int offset, int length) { + if (length == 0) return emptyMap(); + if (length > MAX_ENTRIES) throw new IllegalArgumentException("Too many entries: " + entries.length); + MapEntry[] sorted = Utils.copyOfRangeExcludeNulls(entries, offset, offset + length); + if (sorted.length == 0) return emptyMap(); + Arrays.sort(sorted); + return new MapLeaf(sorted); + } + + @SuppressWarnings("unchecked") + public static MapLeaf create(MapEntry item) { + return new MapLeaf((MapEntry[]) new MapEntry[] { item }); + } + + @SuppressWarnings("unchecked") + @Override + public boolean containsKey(ACell key) { + return getEntry((K) key) != null; + } + + @Override + public MapEntry getEntry(ACell k) { + // if we have an ACell instance, use (or compute) the cached hash. Probably + // faster. + if (k instanceof ACell) return getEntryByHash(((ACell) k).getHash()); + + int len = size(); + for (int i = 0; i < len; i++) { + MapEntry e = entries[i]; + if (Utils.equals(k, e.getKey())) return e; + } + return null; + } + + @Override + public MapEntry getKeyRefEntry(Ref ref) { + int len = size(); + for (int i = 0; i < len; i++) { + MapEntry e = entries[i]; + if (ref.equals(e.getKeyRef())) return e; + } + return null; + } + + @Override + protected MapEntry getEntryByHash(Hash hash) { + int len = size(); + for (int i = 0; i < len; i++) { + MapEntry e = entries[i]; + if (hash.equals(e.getKeyHash())) return e; + } + return null; + } + + @Override + public boolean containsValue(Object value) { + int len = size(); + for (int i = 0; i < len; i++) { + if (Utils.equals(value, entries[i].getValue())) return true; + } + return false; + } + + @SuppressWarnings("unchecked") + @Override + public V get(ACell key) { + MapEntry me = getEntry((K) key); + return (me == null) ? null : me.getValue(); + } + + /** + * Gets the index of key k in the internal array, or -1 if not found + * + * @param key + * @return + */ + private int seek(K key) { + int len = size(); + for (int i = 0; i < len; i++) { + if (Utils.equals(key, entries[i].getKey())) return i; + } + return -1; + } + + private int seekKeyRef(Ref key) { + int len = size(); + for (int i = 0; i < len; i++) { + if (Utils.equals(key, entries[i].getKeyRef())) return i; + } + return -1; + } + + @SuppressWarnings("unchecked") + @Override + public MapLeaf dissoc(ACell key) { + int i = seek((K)key); + if (i < 0) return this; // not found + return dissocEntry(i); + } + + @Override + public MapLeaf dissocRef(Ref key) { + int i = seekKeyRef(key); + if (i < 0) return this; // not found + return dissocEntry(i); + } + + @SuppressWarnings("unchecked") + private MapLeaf dissocEntry(int internalIndex) { + int len = size(); + if (len == 1) return emptyMap(); + MapEntry[] newEntries = (MapEntry[]) new MapEntry[len - 1]; + System.arraycopy(entries, 0, newEntries, 0, internalIndex); + System.arraycopy(entries, internalIndex + 1, newEntries, internalIndex, len - internalIndex - 1); + return new MapLeaf(newEntries); + } + + @Override + public AHashMap assocEntry(MapEntry e) { + return assocEntry(e, 0); + } + + @Override + public AHashMap assocEntry(MapEntry e, int shift) { + int len = size(); + + // first check for update with existing key + for (int i = 0; i < len; i++) { + MapEntry me = entries[i]; + if (e.equals(me)) return this; + if (me.getKeyRef().equals(e.getKeyRef())) { + // replace current entry + MapEntry[] newEntries = entries.clone(); + newEntries[i] = e; + return new MapLeaf(newEntries); + } + } + + // need to extend array, use new shift if promoting to TreeMap + int newLen = len + 1; + @SuppressWarnings("unchecked") + MapEntry[] newEntries = (MapEntry[]) new MapEntry[newLen]; + System.arraycopy(entries, 0, newEntries, 0, len); + newEntries[newLen - 1] = e; + if (newLen <= MAX_ENTRIES) { + // new size implies a ListMap + Arrays.sort(newEntries); + return new MapLeaf(newEntries); + } else { + // new size implies a TreeMap with the current given shift + return MapTree.create(newEntries, shift); + } + } + + @SuppressWarnings("unchecked") + @Override + public AHashMap assoc(ACell key, ACell value) { + return assoc((K)key, (V) value, 0); + } + + protected AHashMap assoc(K key, V value, int shift) { + int len = size(); + + // first check for update with existing key + for (int i = 0; i < len; i++) { + MapEntry me = entries[i]; + if (Utils.equals(me.getKey(), key)) { + if (Utils.equals(me.getValue(), value)) return this; + MapEntry newEntry = me.withValue(value); + if (me == newEntry) return this; + + // need to clone and update array + MapEntry[] newEntries = entries.clone(); + newEntries[i] = newEntry; + return new MapLeaf(newEntries); + } + } + + // Key not found, so need to extend array + @SuppressWarnings("unchecked") + MapEntry[] newEntries = (MapEntry[]) new MapEntry[len + 1]; + System.arraycopy(entries, 0, newEntries, 0, len); + newEntries[len] = MapEntry.create(key, value); + if (len + 1 <= MAX_ENTRIES) { + // new size should be a ListMap + Arrays.sort(newEntries); + return new MapLeaf(newEntries); + } else { + // new Size should be a TreeMap with current shift + return MapTree.create(newEntries, shift); + } + } + + @Override + protected AHashMap assocRef(Ref keyRef, V value, int shift) { + return assoc(keyRef.getValue(), value, shift); + } + + @Override + public AHashMap assocRef(Ref keyRef, V value) { + return assocRef(keyRef, value, 0); + } + + @Override + public Set keySet() { + int len = size(); + HashSet h = new HashSet(len); + ; + for (int i = 0; i < len; i++) { + MapEntry me = entries[i]; + h.add(me.getKey()); + } + return h; + } + + @Override + protected void accumulateKeySet(HashSet h) { + for (int i = 0; i < entries.length; i++) { + MapEntry me = entries[i]; + h.add(me.getKey()); + } + } + + @Override + protected void accumulateValues(ArrayList al) { + for (int i = 0; i < entries.length; i++) { + MapEntry me = entries[i]; + al.add(me.getValue()); + } + } + + @Override + public Set> entrySet() { + int len = size(); + HashSet> h = new HashSet>(len); + ; + accumulateEntrySet(h); + return h; + } + + @Override + protected void accumulateEntrySet(HashSet> h) { + for (int i = 0; i < entries.length; i++) { + MapEntry me = entries[i]; + h.add(me); + } + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.MAP; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = Format.writeVLCLong(bs,pos, count); + + for (int i = 0; i < count; i++) { + pos = entries[i].encodeRaw(bs,pos); + } + return pos; + } + + @Override + public int estimatedEncodingSize() { + // allow space for header, size byte, 2 refs per entry + return 2 + 2* Format.MAX_EMBEDDED_LENGTH * size(); + } + + public static int MAX_ENCODING_LENGTH= 2 + 2 * MAX_ENTRIES * Format.MAX_EMBEDDED_LENGTH; + + /** + * Reads a MapLeaf from the provided ByteBuffer Assumes the header byte is + * already read. + * + * @param bb ByteBuffer to read from + * @param count Count of map elements + * @return A Map as deserialised from the provided ByteBuffer + * @throws BadFormatException If encoding is invalid + */ + @SuppressWarnings("unchecked") + public static MapLeaf read(ByteBuffer bb, long count) throws BadFormatException { + if (count == 0) return (MapLeaf) EMPTY; + if (count < 0) throw new BadFormatException("Negative count of map elements!"); + if (count > MAX_ENTRIES) throw new BadFormatException("MapLeaf too big: " + count); + + MapEntry[] items = (MapEntry[]) new MapEntry[(int) count]; + for (int i = 0; i < count; i++) { + items[i] = MapEntry.read(bb); + } + + if (!isValidOrder(items)) { + throw new BadFormatException("Bad ordering of keys!"); + } + + return new MapLeaf(items); + } + + + @SuppressWarnings("unchecked") + public static MapLeaf emptyMap() { + return (MapLeaf) EMPTY; + } + + @Override + public void forEach(BiConsumer action) { + for (MapEntry e : entries) { + action.accept(e.getKey(), e.getValue()); + } + } + + @Override + public boolean isCanonical() { + return true; + } + + @Override public final boolean isCVMValue() { + return true; + } + + private static boolean isValidOrder(MapEntry[] entries) { + long count = entries.length; + for (int i = 0; i < count - 1; i++) { + Hash a = entries[i].getKeyHash(); + Hash b = entries[i + 1].getKeyHash(); + if (a.compareTo(b) >= 0) { + return false; + } + } + return true; + } + + @Override + public int getRefCount() { + return 2 * entries.length; + } + + @SuppressWarnings("unchecked") + @Override + public Ref getRef(int i) { + MapEntry e = entries[i >> 1]; // IndexOutOfBoundsException if out of range + if ((i & 1) == 0) { + return (Ref) e.getKeyRef(); + } else { + return (Ref) e.getValueRef(); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public MapLeaf updateRefs(IRefFunction func) { + int n = entries.length; + if (n == 0) return this; + MapEntry[] newEntries = entries; + for (int i = 0; i < n; i++) { + MapEntry e = newEntries[i]; + MapEntry newEntry = e.updateRefs(func); + if (e!=newEntry) { + if (newEntries==entries) newEntries=entries.clone(); + newEntries[i]=newEntry; + } + } + if (newEntries==entries) return this; + // Note: we assume no key hashes have changed + return new MapLeaf(newEntries); + } + + /** + * Filters this ListMap to contain only key hashes with the hex digits specified + * in the given Mask + * + * @param digitPos Position of the hex digit to filter + * @param mask Mask of digits to include + * @return Filtered ListMap + */ + public MapLeaf filterHexDigits(int digitPos, int mask) { + mask = mask & 0xFFFF; + if (mask == 0) return emptyMap(); + if (mask == 0xFFFF) return this; + int sel = 0; + int n = size(); + for (int i = 0; i < n; i++) { + Hash h = entries[i].getKeyHash(); + if ((mask & (1 << h.getHexDigit(digitPos))) != 0) { + sel = sel | (1 << i); // include this index in selection + } + } + if (sel == 0) return emptyMap(); // no entries selected + return filterEntries(sel); + } + + /** + * Filters entries using the given bit mask + * + * @param selection + * @return + */ + @SuppressWarnings("unchecked") + private MapLeaf filterEntries(int selection) { + if (selection == 0) return emptyMap(); // no items selected + int n = size(); + if (selection == ((1 << n) - 1)) return this; // all items selected + MapEntry[] newEntries = new MapEntry[Integer.bitCount(selection)]; + int ix = 0; + for (int i = 0; i < n; i++) { + if ((selection & (1 << i)) != 0) { + newEntries[ix++] = entries[i]; + } + } + assert (ix == Integer.bitCount(selection)); + return new MapLeaf(newEntries); + } + + @Override + public MapEntry entryAt(long i) { + return entries[Utils.checkedInt(i)]; + } + + @Override + public AHashMap mergeWith(AHashMap b, MergeFunction func) { + return mergeWith(b, func, 0); + } + + @Override + protected AHashMap mergeWith(AHashMap b, MergeFunction func, int shift) { + if (b instanceof MapLeaf) return mergeWith((MapLeaf) b, func, shift); + if (b instanceof MapTree) return ((MapTree) b).mergeWith(this, func.reverse()); + throw new TODOException("Unhandled map type: " + b.getClass()); + } + + private AHashMap mergeWith(MapLeaf b, MergeFunction func, int shift) { + int al = this.size(); + int bl = b.size(); + int ai = 0; + int bi = 0; + // Complexity to manage: + // 1. Must step through two ListMaps in order, comparing for key hashes + // 2. nulls can be produced to remove entries + // 3. We use the creation of a results ArrayList to signal a change from + // original value + ArrayList> results = null; + while ((ai < al) || (bi < bl)) { + MapEntry ae = (ai < al) ? this.entries[ai] : null; + MapEntry be = (bi < bl) ? b.entries[bi] : null; + + // comparison + int c = (ae == null) ? 1 : ((be == null) ? -1 : ae.getKeyHash().compareTo(be.getKeyHash())); + + // new entry + MapEntry newE = null; + if (c < 0) { + V r = func.merge(ae.getValue(), null); + if (r != null) newE = ae.withValue(r); + } else if (c > 0) { + V r = func.merge(null, be.getValue()); + if (r != null) newE = be.withValue(r); + } else { + // we have matched keys + V r = func.merge(ae.getValue(), be.getValue()); + if (r != null) newE = ae.withValue(r); + } + if ((results == null) && (newE != ((c <= 0) ? ae : null))) { + // create new results array if difference detected + results = new ArrayList<>(16); + for (int i = 0; i < ai; i++) { // copy previous values in this map, up to ai + results.add(entries[i]); + } + } + if (c <= 0) ai++; // inc ai if we used ae + if (c >= 0) bi++; // inc bi if we used be + if ((results != null) && (newE != null)) results.add(newE); + } + if (results == null) return this; // no change detected + return Maps.createWithShift(shift, results); + } + + @Override + public AHashMap mergeDifferences(AHashMap b, MergeFunction func) { + return mergeDifferences(b,func,0); + } + + @Override + protected AHashMap mergeDifferences(AHashMap b, MergeFunction func, int shift) { + if (b instanceof MapLeaf) return mergeDifferences((MapLeaf) b, func,shift); + if (b instanceof MapTree) return b.mergeWith(this, func.reverse()); + throw new TODOException("Unhandled map type: " + b.getClass()); + } + + public AHashMap mergeDifferences(MapLeaf b, MergeFunction func,int shift) { + if (this.equals(b)) return this; // no change in identical case + int al = this.size(); + int bl = b.size(); + int ai = 0; + int bi = 0; + ArrayList> results = null; + while ((ai < al) || (bi < bl)) { + MapEntry ae = (ai < al) ? this.entries[ai] : null; + MapEntry be = (bi < bl) ? b.entries[bi] : null; + int c = (ae == null) ? 1 : ((be == null) ? -1 : ae.getKeyHash().compareTo(be.getKeyHash())); + MapEntry newE = null; + if (c < 0) { + // lowest key in this map only + V r = func.merge(ae.getValue(), null); + if (r != null) newE = ae.withValue(r); + } else if (c > 0) { + // lowest key in other map b only + V r = func.merge(null, be.getValue()); + if (r != null) newE = be.withValue(r); + } else { + // keys are equal (i.e. value in both maps) + V av = ae.getValue(); + V bv = be.getValue(); + V r = (Utils.equals(av, bv)) ? av : func.merge(ae.getValue(), be.getValue()); + if (r != null) newE = ae.withValue(r); + } + if ((results == null) && (newE != ((c <= 0) ? ae : null))) { + // create new results array if difference detected + results = new ArrayList<>(16); + for (int i = 0; i < ai; i++) { + results.add(entries[i]); + } + } + if (c <= 0) ai++; // inc ai if we used ae + if (c >= 0) bi++; // inc bi if we used be + if ((results != null) && (newE != null)) results.add(newE); + } + if (results == null) return this; + return Maps.createWithShift(shift,results); + } + + @Override + public R reduceValues(BiFunction func, R initial) { + int n = size(); + R result = initial; + for (int i = 0; i < n; i++) { + result = func.apply(result, entries[i].getValue()); + } + return result; + } + + @Override + public R reduceEntries(BiFunction, ? extends R> func, R initial) { + int n = size(); + R result = initial; + for (int i = 0; i < n; i++) { + result = func.apply(result, entries[i]); + } + return result; + } + + @Override + public boolean equalsKeys(AMap a) { + if (a instanceof MapLeaf) return equalsKeys((MapLeaf) a); + // different map type cannot possibly be equal + return false; + } + + /** + * Returns true if this map has all keys equal to the other Map + * + * @param a A map to compare keys with + * @return Boolean true if the two maps have the same keys + */ + public boolean equalsKeys(MapLeaf a) { + if (this == a) return true; + int n = this.size(); + if (n != a.size()) return false; + for (int i = 0; i < n; i++) { + if (!this.entries[i].keyEquals(a.entries[i])) return false; + } + return true; + } + + @Override + public boolean equals(AMap a) { + if (!(a instanceof MapLeaf)) return false; + return equals((MapLeaf) a); + } + + public boolean equals(MapLeaf a) { + if (this == a) return true; + int n = size(); + if (n != a.size()) return false; + for (int i = 0; i < n; i++) { + if (!entries[i].equals(a.entries[i])) return false; + } + return true; + } + + @Override + public MapLeaf mapEntries(Function, MapEntry> func) { + MapEntry[] newEntries = entries; + for (int i = 0; i < entries.length; i++) { + MapEntry e = entries[i]; + MapEntry newE = func.apply(e); + if (e != newE) { + if ((newE != null) && (!(e.keyEquals(newE)))) + throw new IllegalArgumentException("Function changed Key: " + e.getKey()); + if (newEntries == entries) { + newEntries = entries.clone(); + } + newEntries[i] = newE; + } + } + if (newEntries == entries) return this; + return create(newEntries); + } + + @Override + protected void validateWithPrefix(String prefix) throws InvalidDataException { + validate(); + for (int i = 0; i < entries.length; i++) { + MapEntry e = entries[i]; + Hash h = e.getKeyRef().getHash(); + if (!h.toHexString().startsWith(prefix)) { + throw new InvalidDataException("Prefix " + prefix + " invalid for map entry: " + e + " with hash: " + h, + this); + } + e.validate(); + } + } + + @Override + public void validateCell() throws InvalidDataException { + if ((count == 0) && (this != EMPTY)) { + throw new InvalidDataException("Empty map not using canonical instance", this); + } + + if (count > MAX_ENTRIES) { + throw new InvalidDataException("Too many items in list map: " + entries.length, this); + } + + // validates both key uniqueness and sort order + if (!isValidOrder(entries)) { + throw new InvalidDataException("Invalid key ordering", this); + } + } + + @Override + public boolean containsAllKeys(AHashMap b) { + if (this==b) return true; + + // if map is too big, can't possibly contain all keys + if (b.count()>count) return false; + + // must be a mapleaf if this size or smaller + return containsAllKeys((MapLeaf)b); + } + + protected boolean containsAllKeys(MapLeaf b) { + int ix=0; + for (MapEntry meb:b.entries) { + Hash bh=meb.getKeyHash(); + + if (ix>=count) return false; // no remaining entries in this + while (ix mea=entries[ix]; + Hash ah=mea.getKeyHash(); + int c=ah.compareTo(bh); + if (c<0) { + // ah is smaller than bh + // need to advance ix and try next entry + ix++; + if (ix>=count) return false; // not found + continue; + } else if (c>0) { + return false; // didn't contain the key entry + } else { + // found it, so advance to next entry in b and update ix + ix++; + break; + } + } + } + + return true; + } + + @Override + public byte getTag() { + return Tag.MAP; + } + + @Override + public ACell toCanonical() { + return this; + } +} diff --git a/convex-core/src/main/java/convex/core/data/MapTree.java b/convex-core/src/main/java/convex/core/data/MapTree.java new file mode 100644 index 000000000..44cdb926a --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/MapTree.java @@ -0,0 +1,880 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; + +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.exceptions.TODOException; +import convex.core.util.Bits; +import convex.core.util.MergeFunction; +import convex.core.util.Utils; + +/** + * Persistent Map for large hash maps requiring tree structure. + * + * Internally implemented as a radix tree, indexed by key hash. Uses an array of + * child Maps, with a bitmap mask indicating which hex digits are present, i.e. + * have non-empty children. + * + * @param Type of map keys + * @param Type of map values + */ +public class MapTree extends AHashMap { + /** + * Child maps, one for each present bit in the mask, max 16 + */ + private final Ref>[] children; + + /** + * Shift position of this treemap node in number of hex digits + */ + private final int shift; + + /** + * Mask indicating which hex digits are present in the child array e.g. 0x0001 + * indicates all children are in the '0' digit. e.g. 0xFFFF indicates there are + * children for every digit. + */ + private final short mask; + + private MapTree(Ref>[] blocks, int shift, short mask, long count) { + super(count); + this.children = blocks; + this.shift = shift; + this.mask = mask; + } + + /** + * Computes the total count from an array of Refs to maps Ignores null Refs in + * child array + * + * @param children + * @return The total count of all child maps + */ + private static long computeCount(Ref>[] children) { + long n = 0; + for (Ref> cref : children) { + if (cref == null) continue; + AMap m = cref.getValue(); + n += m.count(); + } + return n; + } + + @SuppressWarnings("unchecked") + public static MapTree create(MapEntry[] newEntries, int shift) { + int n = newEntries.length; + if (n <= MapLeaf.MAX_ENTRIES) { + throw new IllegalArgumentException( + "Insufficient distinct entries for TreeMap construction: " + newEntries.length); + } + + // construct full child array + Ref>[] children = new Ref[16]; + for (int i = 0; i < n; i++) { + MapEntry e = newEntries[i]; + int ix = e.getKeyHash().getHexDigit(shift); + Ref> ref = children[ix]; + if (ref == null) { + children[ix] = MapLeaf.create(e).getRef(); + } else { + AHashMap newChild=ref.getValue().assocEntry(e, shift + 1); + children[ix] = newChild.getRef(); + } + } + return (MapTree) createFull(children, shift); + } + + /** + * Creates a Tree map given child refs for each digit + * + * @param children An array of children, may refer to nulls or empty maps which + * will be filtered out + * @return + */ + private static AHashMap createFull(Ref>[] children, int shift, long count) { + if (children.length != 16) throw new IllegalArgumentException("16 children required!"); + Ref>[] newChildren = Utils.filterArray(children, a -> { + if (a == null) return false; + AMap m = a.getValue(); + return ((m != null) && !m.isEmpty()); + }); + + if (children != newChildren) { + return create(newChildren, shift, Utils.computeMask(children, newChildren), count); + } else { + return create(children, shift, (short) 0xFFFF, count); + } + } + + /** + * Create a MapTree with a full compliment of children. + * @param + * @param + * @param newChildren + * @param shift + * @return + */ + private static AHashMap createFull(Ref>[] newChildren, int shift) { + return createFull(newChildren, shift, computeCount(newChildren)); + } + + /** + * Creates a Map with the specified child map Refs. Removes empty maps passed as + * children. + * + * Returns a ListMap for small maps. + * + * @param children Array of Refs to child maps for each bit in mask + * @param shift Shift position (hex digit of key hashes for this map) + * @param mask Mask specifying the hex digits included in the child array at + * this shift position + * @return A new map as specified @ + */ + @SuppressWarnings("unchecked") + private static AHashMap create(Ref>[] children, int shift, short mask, long count) { + int cLen = children.length; + if (Integer.bitCount(mask & 0xFFFF) != cLen) { + throw new IllegalArgumentException( + "Invalid child array length " + cLen + " for bit mask " + Utils.toHexString(mask)); + } + + // compress small counts to ListMap + if (count <= MapLeaf.MAX_ENTRIES) { + MapEntry[] entries = new MapEntry[Utils.checkedInt(count)]; + int ix = 0; + for (Ref> childRef : children) { + AMap child = childRef.getValue(); + long cc = child.count(); + for (long i = 0; i < cc; i++) { + entries[ix++] = child.entryAt(i); + } + } + assert (ix == count); + return MapLeaf.create(entries); + } + int sel = (1 << cLen) - 1; + short newMask = mask; + for (int i = 0; i < cLen; i++) { + AMap child = children[i].getValue(); + if (child.isEmpty()) { + newMask = (short) (newMask & ~(1 << digitForIndex(i, mask))); // remove from mask + sel = sel & ~(1 << i); // remove from selection + } + } + if (mask != newMask) { + return new MapTree(Utils.filterSmallArray(children, sel), shift, newMask, count); + } + return new MapTree(children, shift, mask, count); + } + + @Override + public boolean containsKey(ACell key) { + return containsKeyRef(Ref.get(key)); + } + + @Override + public MapEntry getEntry(ACell k) { + return getKeyRefEntry(Ref.get(k)); + } + + @Override + public MapEntry getKeyRefEntry(Ref ref) { + int digit = Utils.extractDigit(ref.getHash(), shift); + int i = Bits.indexForDigit(digit, mask); + if (i < 0) return null; // -1 case indicates not found + return children[i].getValue().getKeyRefEntry(ref); + } + + @Override + public boolean containsValue(Object value) { + for (Ref> b : children) { + if (b.getValue().containsValue(value)) return true; + } + return false; + } + + @Override + public V get(ACell key) { + MapEntry me = getKeyRefEntry(Ref.get(key)); + if (me == null) return null; + return me.getValue(); + } + + @Override + public MapEntry entryAt(long i) { + long pos = i; + for (Ref> c : children) { + AHashMap child = c.getValue(); + long cc = child.count(); + if (pos < cc) return child.entryAt(pos); + pos -= cc; + } + throw new IndexOutOfBoundsException("Entry index: " + i); + } + + @Override + protected MapEntry getEntryByHash(Hash hash) { + int digit = Utils.extractDigit(hash, shift); + int i = Bits.indexForDigit(digit, mask); + if (i < 0) return null; // not present + return children[i].getValue().getEntryByHash(hash); + } + + @SuppressWarnings("unchecked") + @Override + public AHashMap dissoc(ACell key) { + return dissocRef((Ref) Ref.get(key)); + } + + @Override + @SuppressWarnings("unchecked") + public AHashMap dissocRef(Ref keyRef) { + int digit = Utils.extractDigit(keyRef.getHash(), shift); + int i = Bits.indexForDigit(digit, mask); + if (i < 0) return this; // not present + + // dissoc entry from child + AHashMap child = children[i].getValue(); + AHashMap newChild = child.dissocRef(keyRef); + if (child == newChild) return this; // no removal, no change + + if (count - 1 == MapLeaf.MAX_ENTRIES) { + // reduce to a ListMap + HashSet> eset = entrySet(); + boolean removed = eset.removeIf(e -> Utils.equals(((MapEntry) e).getKeyRef(), keyRef)); + if (!removed) throw new Error("Expected to remove at least one entry!"); + return MapLeaf.create(eset.toArray((MapEntry[]) MapLeaf.EMPTY_ENTRIES)); + } else { + // replace child + if (newChild.isEmpty()) return dissocChild(i); + return replaceChild(i, newChild.getRef()); + } + } + + @SuppressWarnings("unchecked") + private AHashMap dissocChild(int i) { + int bsize = children.length; + AHashMap child = children[i].getValue(); + Ref>[] newBlocks = (Ref>[]) new Ref[bsize - 1]; + System.arraycopy(children, 0, newBlocks, 0, i); + System.arraycopy(children, i + 1, newBlocks, i, bsize - i - 1); + short newMask = (short) (mask & (~(1 << digitForIndex(i, mask)))); + long newCount = count - child.count(); + return create(newBlocks, shift, newMask, newCount); + } + + @SuppressWarnings("unchecked") + private MapTree insertChild(int digit, Ref> newChild) { + int bsize = children.length; + int i = Bits.positionForDigit(digit, mask); + short newMask = (short) (mask | (1 << digit)); + if (mask == newMask) throw new Error("Digit already present!"); + + Ref>[] newChildren = (Ref>[]) new Ref[bsize + 1]; + System.arraycopy(children, 0, newChildren, 0, i); + System.arraycopy(children, i, newChildren, i + 1, bsize - i); + newChildren[i] = newChild; + long newCount = count + newChild.getValue().count(); + return (MapTree) create(newChildren, shift, newMask, newCount); + } + + /** + * Replaces the child ref at a given index position. Will return the same + * TreeMap if no change + * + * @param i + * @param newChild + * @return @ + */ + private MapTree replaceChild(int i, Ref> newChild) { + if (children[i] == newChild) return this; + AHashMap oldChild = children[i].getValue(); + Ref>[] newChildren = children.clone(); + newChildren[i] = newChild; + long newCount = count + newChild.getValue().count() - oldChild.count(); + return (MapTree) create(newChildren, shift, mask, newCount); + } + + public static int digitForIndex(int index, short mask) { + // scan mask for specified index + int found = 0; + for (int i = 0; i < 16; i++) { + if ((mask & (1 << i)) != 0) { + if (found++ == index) return i; + } + } + throw new IllegalArgumentException("Index " + index + " not available in mask map: " + Utils.toHexString(mask)); + } + + @SuppressWarnings("unchecked") + @Override + public MapTree assoc(ACell key, ACell value) { + K k= (K)key; + Ref keyRef = Ref.get(k); + return assocRef(keyRef, (V) value, shift); + } + + @Override + public MapTree assocRef(Ref keyRef, V value) { + return assocRef(keyRef, value, shift); + } + + @Override + protected MapTree assocRef(Ref keyRef, V value, int shift) { + if (this.shift != shift) { + throw new Error("Invalid shift!"); + } + int digit = Utils.extractDigit(keyRef.getHash(), shift); + int i = Bits.indexForDigit(digit, mask); + if (i < 0) { + // location not present, need to insert new child + AHashMap newChild = MapLeaf.create(MapEntry.createRef(keyRef, Ref.get(value))); + return insertChild(digit, newChild.getRef()); + } else { + // child exists, so assoc in new ref at lower shift level + AHashMap child = children[i].getValue(); + AHashMap newChild = child.assocRef(keyRef, value, shift + 1); + return replaceChild(i, newChild.getRef()); + } + } + + @Override + public AHashMap assocEntry(MapEntry e) { + assert (this.shift == 0); // should never call this on a different shift + return assocEntry(e, 0); + } + + @Override + public MapTree assocEntry(MapEntry e, int shift) { + assert (this.shift == shift); // should always be correct shift + Ref keyRef = e.getKeyRef(); + int digit = Utils.extractDigit(keyRef.getHash(), shift); + int i = Bits.indexForDigit(digit, mask); + if (i < 0) { + // location not present + AHashMap newChild = MapLeaf.create(e); + return insertChild(digit, newChild.getRef()); + } else { + // location needs update + AHashMap child = children[i].getValue(); + AHashMap newChild = child.assocEntry(e, shift + 1); + if (child == newChild) return this; + return replaceChild(i, newChild.getRef()); + } + } + + @Override + public Set keySet() { + int len = size(); + HashSet h = new HashSet(len); + accumulateKeySet(h); + return h; + } + + @Override + protected void accumulateKeySet(HashSet h) { + for (Ref> mr : children) { + mr.getValue().accumulateKeySet(h); + } + } + + @Override + protected void accumulateValues(ArrayList al) { + for (Ref> mr : children) { + mr.getValue().accumulateValues(al); + } + } + + @Override + public HashSet> entrySet() { + int len = size(); + HashSet> h = new HashSet>(len); + accumulateEntrySet(h); + return h; + } + + @Override + protected void accumulateEntrySet(HashSet> h) { + for (Ref> mr : children) { + mr.getValue().accumulateEntrySet(h); + } + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.MAP; + return encodeRaw(bs,pos); + } + + + @Override + public int encodeRaw(byte[] bs, int pos) { + int ilength = children.length; + pos = Format.writeVLCLong(bs,pos, count); + + bs[pos++] = (byte) shift; + pos = Utils.writeShort(bs, pos,mask); + + for (int i = 0; i < ilength; i++) { + pos = children[i].encode(bs,pos); + } + return pos; + } + + @Override + public int estimatedEncodingSize() { + // allow space for tag, shift byte byte, 2 byte mask, embedded child refs + return 4 + Format.MAX_EMBEDDED_LENGTH * children.length; + } + + public static int MAX_ENCODING_LENGTH = 4 + Format.MAX_EMBEDDED_LENGTH * 16; + + /** + * Reads a ListMap from the provided ByteBuffer Assumes the header byte and count is + * already read. + * + * @param bb ByteBuffer to read from + * @param count Count of map entries + * @return TreeMap instance as read from ByteBuffer + * @throws BadFormatException If encoding is invalid + */ + @SuppressWarnings("unchecked") + public static MapTree read(ByteBuffer bb, long count) throws BadFormatException { + int shift = bb.get(); + short mask = bb.getShort(); + + int ilength = Integer.bitCount(mask & 0xFFFF); + Ref>[] blocks = (Ref>[]) new Ref[ilength]; + + for (int i = 0; i < ilength; i++) { + // need to read as a Ref + Ref> ref = Format.readRef(bb); + blocks[i] = ref; + } + // create directly, we have all values + MapTree result = new MapTree(blocks, shift, mask, count); + if (!result.isValidStructure()) throw new BadFormatException("Problem with TreeMap invariants"); + return result; + } + + @Override + public void forEach(BiConsumer action) { + for (Ref> sub : children) { + sub.getValue().forEach(action); + } + } + + @Override + public boolean isCanonical() { + if (count <= MapLeaf.MAX_ENTRIES) return false; + return true; + } + + @Override public final boolean isCVMValue() { + return shift==0; + } + + @Override + public int getRefCount() { + return children.length; + } + + @SuppressWarnings("unchecked") + @Override + public Ref getRef(int i) { + return (Ref) children[i]; + } + + @SuppressWarnings("unchecked") + @Override + public MapTree updateRefs(IRefFunction func) { + int n = children.length; + if (n == 0) return this; + Ref>[] newChildren = children; + for (int i = 0; i < n; i++) { + Ref> child = children[i]; + Ref> newChild = (Ref>) func.apply(child); + if (child != newChild) { + if (children == newChildren) { + newChildren = children.clone(); + } + newChildren[i] = newChild; + } + } + if (newChildren == children) return this; + // Note: we assume no key hashes have changed, so structure is the same + return new MapTree<>(newChildren, shift, mask, count); + } + + @Override + public AHashMap mergeWith(AHashMap b, MergeFunction func) { + return mergeWith(b, func, this.shift); + } + + @Override + protected AHashMap mergeWith(AHashMap b, MergeFunction func, int shift) { + if ((b instanceof MapTree)) { + MapTree bt = (MapTree) b; + if (this.shift != bt.shift) throw new Error("Misaligned shifts!"); + return mergeWith(bt, func, shift); + } + if ((b instanceof MapLeaf)) return mergeWith((MapLeaf) b, func, shift); + throw new Error("Unrecognised map type: " + b.getClass()); + } + + @SuppressWarnings("unchecked") + private AHashMap mergeWith(MapTree b, MergeFunction func, int shift) { + // assume two TreeMaps with identical prefix and shift + assert (b.shift == shift); + int fullMask = mask | b.mask; + // We are going to build full child list only if needed + Ref>[] newChildren = null; + for (int digit = 0; digit < 16; digit++) { + int bitMask = 1 << digit; + if ((fullMask & bitMask) == 0) continue; // nothing to merge at this index + AHashMap ac = childForDigit(digit).getValue(); + AHashMap bc = b.childForDigit(digit).getValue(); + AHashMap rc = ac.mergeWith(bc, func, shift + 1); + if (ac != rc) { + if (newChildren == null) { + newChildren = (Ref>[]) new Ref[16]; + for (int ii = 0; ii < digit; ii++) { // copy existing children up to this point + int chi = Bits.indexForDigit(ii, mask); + if (chi >= 0) newChildren[ii] = children[chi]; + } + } + } + if (newChildren != null) newChildren[digit] = rc.getRef(); + } + if (newChildren == null) return this; + return createFull(newChildren, shift); + } + + @SuppressWarnings("unchecked") + private AHashMap mergeWith(MapLeaf b, MergeFunction func, int shift) { + Ref>[] newChildren = null; + int ix = 0; + for (int i = 0; i < 16; i++) { + int imask = (1 << i); // mask for this digit + if ((mask & imask) == 0) continue; + Ref> cref = children[ix++]; + AHashMap child = cref.getValue(); + MapLeaf bSubset = b.filterHexDigits(shift, imask); // filter only relevant elements in b + AHashMap newChild = child.mergeWith(bSubset, func, shift + 1); + if (child != newChild) { + if (newChildren == null) { + newChildren = (Ref>[]) new Ref[16]; + for (int ii = 0; ii < children.length; ii++) { // copy existing children + int chi = digitForIndex(ii, mask); + newChildren[chi] = children[ii]; + } + } + } + if (newChildren != null) { + newChildren[i] = newChild.getRef(); + } + } + assert (ix == children.length); + // if any new children created, create a new Map, else use this + AHashMap result = (newChildren == null) ? this : createFull(newChildren, shift); + + MapLeaf extras = b.filterHexDigits(shift, ~mask); + int en = extras.size(); + for (int i = 0; i < en; i++) { + MapEntry e = extras.entryAt(i); + V value = func.merge(null, e.getValue()); + if (value != null) { + // include only new keys where function result is not null. Re-use existing + // entry if possible. + result = result.assocEntry(e.withValue(value), shift); + } + } + return result; + } + + @Override + public AHashMap mergeDifferences(AHashMap b, MergeFunction func) { + return mergeDifferences(b, func,0); + } + + @Override + protected AHashMap mergeDifferences(AHashMap b, MergeFunction func,int shift) { + if ((b instanceof MapTree)) { + MapTree bt = (MapTree) b; + // this is OK, top levels should both have shift 0 and be aligned down the tree. + if (this.shift != bt.shift) throw new Error("Misaligned shifts!"); + return mergeDifferences(bt, func,shift); + } else { + // must be ListMap + return mergeDifferences((MapLeaf) b, func,shift); + } + } + + @SuppressWarnings("unchecked") + private AHashMap mergeDifferences(MapTree b, MergeFunction func, int shift) { + // assume two treemaps with identical prefix and shift + if (this.equals(b)) return this; // no differences to merge + int fullMask = mask | b.mask; + Ref>[] newChildren = null; // going to build new full child list if needed + for (int i = 0; i < 16; i++) { + int bitMask = 1 << i; + if ((fullMask & bitMask) == 0) continue; // nothing to merge at this index + Ref> aref = childForDigit(i); + Ref> bref = b.childForDigit(i); + if (aref.equalsValue(bref)) continue; // identical children, no differences + AHashMap ac = aref.getValue(); + AHashMap bc = bref.getValue(); + AHashMap newChild = ac.mergeDifferences(bc, func,shift+1); + if (newChild != ac) { + if (newChildren == null) { + newChildren = (Ref>[]) new Ref[16]; + for (int ii = 0; ii < 16; ii++) { // copy existing children + int chi = Bits.indexForDigit(ii, mask); + if (chi >= 0) newChildren[ii] = children[chi]; + } + } + } + if (newChildren != null) newChildren[i] = (newChild == bc) ? bref : newChild.getRef(); + } + if (newChildren == null) return this; + return createFull(newChildren, shift); + } + + @SuppressWarnings("unchecked") + private AHashMap mergeDifferences(MapLeaf b, MergeFunction func, int shift) { + Ref>[] newChildren = null; + int ix = 0; + for (int i = 0; i < 16; i++) { + int imask = (1 << i); // mask for this digit + if ((mask & imask) == 0) continue; + Ref> cref = children[ix++]; + AHashMap child = cref.getValue(); + MapLeaf bSubset = b.filterHexDigits(shift, imask); // filter only relevant elements in b + AHashMap newChild = child.mergeDifferences(bSubset, func,shift+1); + if (child != newChild) { + if (newChildren == null) { + newChildren = (Ref>[]) new Ref[16]; + for (int ii = 0; ii < children.length; ii++) { // copy existing children + int chi = digitForIndex(ii, mask); + newChildren[chi] = children[ii]; + } + } + } + if (newChildren != null) newChildren[i] = newChild.getRef(); + } + assert (ix == children.length); + AHashMap result = (newChildren == null) ? this : createFull(newChildren, shift); + + MapLeaf extras = b.filterHexDigits(shift, ~mask); + int en = extras.size(); + for (int i = 0; i < en; i++) { + MapEntry e = extras.entryAt(i); + V value = func.merge(null, e.getValue()); + if (value != null) { + // include only new keys where function result is not null. Re-use existing + // entry if possible. + result = result.assocEntry(e.withValue(value), shift); + } + } + return result; + } + + /** + * Gets the Ref for the child at the given digit, or an empty map if not found + * + * @param digit The hex digit to query at this TreeMap's shift position + * @return The child map for this digit, or an empty map if the child does not + * exist + */ + private Ref> childForDigit(int digit) { + int ix = Bits.indexForDigit(digit, mask); + if (ix < 0) return Maps.emptyRef(); + return children[ix]; + } + + @Override + public R reduceValues(BiFunction func, R initial) { + int n = children.length; + R result = initial; + for (int i = 0; i < n; i++) { + result = children[i].getValue().reduceValues(func, result); + } + return result; + } + + @Override + public R reduceEntries(BiFunction, ? extends R> func, R initial) { + int n = children.length; + R result = initial; + for (int i = 0; i < n; i++) { + result = children[i].getValue().reduceEntries(func, result); + } + return result; + } + + @Override + public boolean equalsKeys(AMap a) { + if (a instanceof MapTree) return equalsKeys((MapTree) a); + // different map type cannot possibly be equal + return false; + } + + boolean equalsKeys(MapTree a) { + if (this == a) return true; + if (this.count != a.count) return false; + if (this.mask != a.mask) return false; + int n = children.length; + for (int i = 0; i < n; i++) { + if (!children[i].getValue().equalsKeys(a.children[i].getValue())) return false; + } + return true; + } + + @Override + public boolean equals(AMap a) { + if (!(a instanceof MapTree)) return false; + return equals((MapTree) a); + } + + boolean equals(MapTree b) { + if (this == b) return true; + long n = count; + if (n != b.count) return false; + if (mask != b.mask) return false; + if (shift != b.shift) return false; + + // Fall back to comparing hashes. Probably most efficient in general. + if (getHash().equals(b.getHash())) return true; + return false; + } + + @Override + public AHashMap mapEntries(Function, MapEntry> func) { + int n = children.length; + if (n == 0) return this; + Ref>[] newChildren = children; + for (int i = 0; i < n; i++) { + AHashMap child = children[i].getValue(); + AHashMap newChild = child.mapEntries(func); + if (child != newChild) { + if (children == newChildren) { + newChildren = children.clone(); + } + newChildren[i] = newChild.getRef(); + } + } + if (newChildren == children) return this; + + // Note: creation should remove any empty children. Need to recompute count + // since + // entries may have been removed. + return create(newChildren, shift, mask, computeCount(newChildren)); + } + + @Override + public void validate() throws InvalidDataException { + super.validate(); + // Perform validation for this tree position + validateWithPrefix(""); + } + + @Override + protected void validateWithPrefix(String prefix) throws InvalidDataException { + if (mask == 0) throw new InvalidDataException("TreeMap must have children!", this); + if (shift != prefix.length()) { + throw new InvalidDataException("Invalid prefix [" + prefix + "] for TreeMap with shift=" + shift, this); + } + int bsize = children.length; + + long childCount=0;; + for (int i = 0; i < bsize; i++) { + if (children[i] == null) + throw new InvalidDataException("Null child ref at " + prefix + Utils.toHexChar(digitForIndex(i, mask)), + this); + ACell o = children[i].getValue(); + if (!(o instanceof AHashMap)) { + throw new InvalidDataException( + "Expected map child at " + prefix + Utils.toHexChar(digitForIndex(i, mask)), this); + } + @SuppressWarnings("unchecked") + AHashMap child = (AHashMap) o; + if (child.isEmpty()) + throw new InvalidDataException("Empty child at " + prefix + Utils.toHexChar(digitForIndex(i, mask)), + this); + int d = digitForIndex(i, mask); + child.validateWithPrefix(prefix + Utils.toHexChar(d)); + + childCount += child.count(); + } + + if (count != childCount) { + throw new InvalidDataException("Bad child count, expected " + count + " but children had: " + childCount, this); + } + } + + private boolean isValidStructure() { + if (count <= MapLeaf.MAX_ENTRIES) return false; + if (children.length != Integer.bitCount(mask & 0xFFFF)) return false; + for (int i = 0; i < children.length; i++) { + if (children[i] == null) return false; + } + return true; + } + + @Override + public void validateCell() throws InvalidDataException { + if (!isValidStructure()) throw new InvalidDataException("Bad structure", this); + } + + @SuppressWarnings("unchecked") + @Override + public boolean containsAllKeys(AHashMap map) { + if (map instanceof MapTree) { + return containsAllKeys((MapTree)map); + } + // must be a MapLeaf + long n=map.count; + for (long i=0; i me=map.entryAt(i); + if (!this.containsKeyRef((Ref) me.getKeyRef())) return false; + } + return true; + } + + protected boolean containsAllKeys(MapTree map) { + // fist check this mask contains all of target mask + if ((this.mask|map.mask)!=this.mask) return false; + + for (int i=0; i<16; i++) { + Ref> child=this.childForDigit(i); + if (child==null) continue; + + Ref> mchild=map.childForDigit(i); + if (mchild==null) continue; + + if (!(child.getValue().containsAllKeys(mchild.getValue()))) return false; + } + return true; + } + + @Override + public byte getTag() { + return Tag.MAP; + } + + @Override + public AHashMap toCanonical() { + if (count > MapLeaf.MAX_ENTRIES) return this; + // shouldn't be possible? + throw new TODOException(); + } + +} diff --git a/convex-core/src/main/java/convex/core/data/Maps.java b/convex-core/src/main/java/convex/core/data/Maps.java new file mode 100644 index 000000000..6c2e298f8 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/Maps.java @@ -0,0 +1,156 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.util.HashMap; + +import convex.core.exceptions.BadFormatException; +import convex.core.lang.RT; + +/** + * Utility class for map functions + * + */ +public class Maps { + + private static final AMap EMPTY_MAP = MapLeaf.emptyMap(); + + static { + // Set empty Ref flags as internal embedded constant + EMPTY_MAP.getRef().setFlags(Ref.INTERNAL_FLAGS); + } + + private static final Ref EMPTY_REF = EMPTY_MAP.getRef(); + + @SuppressWarnings("unchecked") + public static > R empty() { + return (R) EMPTY_MAP; + } + + @SuppressWarnings("unchecked") + public static > Ref emptyRef() { + return (Ref) EMPTY_REF; + } + + public static MapLeaf create(K k, V v) { + return MapLeaf.create(MapEntry.create(k, v)); + } + + /** + * Constructs a map with the given keys and values. If keys are repreated, later keys will + * overwrite earlier ones. Performs conversion to CVM types. + * @param Map type + * @param Key type + * @param Value type + * @param keysAndValues Keys and values to include + * @return Map with given keys and values + */ + @SuppressWarnings("unchecked") + public static , K extends ACell, V extends ACell> R of(Object... keysAndValues) { + int n = keysAndValues.length >> 1; + if (keysAndValues.length != n * 2) + throw new IllegalArgumentException("Even number of values need for key-value pairs"); + + AMap result = Maps.empty(); + for (int i = 0; i < n; i++) { + K key = (K) RT.cvm(keysAndValues[i * 2]); + V value = (V) RT.cvm(keysAndValues[i * 2 + 1]); + result = result.assoc(key, value); + } + return (R) result; + } + + /** + * Constructs a map with the given keys and values. If keys are repreated, later keys will + * overwrite earlier ones. Performs conversion to CVM types. + * @param Map type + * @param Key type + * @param Value type + * @param keysAndValues Keys and values to include + * @return Map with given keys and values + */ + @SuppressWarnings("unchecked") + public static , K extends ACell, V extends ACell> R create(ACell[] keysAndValues) { + int n = keysAndValues.length >> 1; + if (keysAndValues.length != n * 2) + throw new IllegalArgumentException("Even number of values need for key-value pairs"); + + AMap result = Maps.empty(); + for (int i = 0; i < n; i++) { + K key = (K) keysAndValues[i * 2]; + V value = (V) keysAndValues[i * 2 + 1]; + result = result.assoc(key, value); + } + return (R) result; + } + + @SuppressWarnings("unchecked") + public static HashMap hashMapOf(Object... keysAndValues) { + int n = keysAndValues.length >> 1; + HashMap result = new HashMap<>(n); + if (keysAndValues.length != n * 2) + throw new IllegalArgumentException("Even number of values need for key-value pairs"); + for (int i = 0; i < n; i++) { + K key = (K) keysAndValues[i * 2]; + V value = (V) keysAndValues[i * 2 + 1]; + result.put(key, value); + } + return result; + } + + /** + * Create a map with a collection of entries. + * + * @param Key type + * @param Value type + * @param entries Entries to include + * @return AHashMap instance + */ + public static AHashMap create(java.util.List> entries) { + return createWithShift(0, entries); + } + + /** + * Create a hashmap with the correct shift and given entries. + * + * @param Key type + * @param Value type + * @param shift Shift level of map + * @param entries Entries to include + * @return AHashMap instance + */ + public static AHashMap createWithShift(int shift, java.util.List> entries) { + int n = entries.size(); + if (n == 0) return empty(); + AHashMap result = Maps.empty(); + for (MapEntry e : entries) { + result = result.assocEntry(e, shift); + } + return result; + } + + @SuppressWarnings("unchecked") + public static > R coerce(AMap m) { + return (R) m; + } + + /** + * Read a Hashmap from a ByteBuffer. Assumes tag byte already read. + * @param Key type + * @param Value type + * @param bb ByteBuffer to read from + * @return Map instance + * @throws BadFormatException If encoding is invalid + */ + public static AHashMap read(ByteBuffer bb) throws BadFormatException { + long count = Format.readVLCLong(bb); + if (count <= MapLeaf.MAX_ENTRIES) { + return MapLeaf.read(bb, count); + } else { + return MapTree.read(bb, count); + } + } + + public static int MAX_ENCODING_SIZE = Math.max(MapTree.MAX_ENCODING_LENGTH, MapLeaf.MAX_ENCODING_LENGTH); + + +} diff --git a/convex-core/src/main/java/convex/core/data/PeerStatus.java b/convex-core/src/main/java/convex/core/data/PeerStatus.java new file mode 100644 index 000000000..bcdbe0ccc --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/PeerStatus.java @@ -0,0 +1,283 @@ +package convex.core.data; + +import java.nio.ByteBuffer; + +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.RT; +import convex.core.lang.impl.RecordFormat; +import convex.core.util.Utils; + +/** + * Class describing the on-chain state of a Peer declared on the network. + * + * State includes: - Stake placed by this Peer - A host address for peer + * connections / client requests + * + */ +public class PeerStatus extends ARecord { + private static final Keyword[] PEER_KEYS = new Keyword[] { Keywords.CONTROLLER, Keywords.STAKE, Keywords.STAKES,Keywords.DELEGATED_STAKE, + Keywords.METADATA}; + + private static final RecordFormat FORMAT = RecordFormat.of(PEER_KEYS); + + private final Address controller; + private final long stake; + private final long delegatedStake; + + private final ABlobMap stakes; + + /** + * Metadata for the Peer. Can be null internally, which is interpreted as an empty Map. + */ + private final AHashMap metadata; + + private PeerStatus(Address controller, long stake, ABlobMap stakes, long delegatedStake, AHashMap metadata) { + super(FORMAT); + this.controller = controller; + this.stake = stake; + this.delegatedStake = delegatedStake; + this.metadata = metadata; + this.stakes = stakes; + } + + public static PeerStatus create(Address controller, long stake) { + return create(controller, stake, null); + } + + public static PeerStatus create(Address controller, long stake, AHashMap metadata) { + return new PeerStatus(controller, stake, BlobMaps.empty(), 0L, metadata); + } + /** + * Gets the stake of this peer + * + * @return Total stake, including own stake + delegated stake + */ + public long getTotalStake() { + // TODO: include rewards? + return stake + delegatedStake; + } + + /** + * Gets the self-owned stake of this peer + * + * @return Own stake, excluding delegated stake + */ + public long getPeerStake() { + // TODO: include rewards? + return stake; + } + + /** + * Gets the controller of this peer + * + * @return The controller of this peer + */ + public Address getController() { + return controller; + } + + /** + * Gets the delegated stake of this peer + * + * @return Total of delegated stake + */ + public long getDelegatedStake() { + return delegatedStake; + } + + /** + * Gets the String representation of the hostname set for the current Peer status, + * or null if not specified. + * + * @return Hostname String + */ + public AString getHostname() { + if (metadata == null) return null; + return RT.ensureString(metadata.get(Keywords.URL)); + } + + /** + * Gets the Metadata of this Peer + * + * @return Host String + */ + public AHashMap getMetadata() { + return metadata==null?Maps.empty():metadata; + } + + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.PEER_STATUS; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = Format.write(bs,pos, controller); + pos = Format.writeVLCLong(bs,pos, stake); + pos = Format.write(bs,pos, stakes); + pos = Format.writeVLCLong(bs,pos, delegatedStake); + pos = Format.write(bs,pos, metadata); + return pos; + } + + public static PeerStatus read(ByteBuffer bb) throws BadFormatException { + Address owner = Format.read(bb); + long stake = Format.readVLCLong(bb); + ABlobMap stakes = Format.read(bb); + long delegatedStake = Format.readVLCLong(bb); + + AHashMap metadata = Format.read(bb); + + return new PeerStatus(owner, stake,stakes,delegatedStake,metadata); + } + + @Override + public int estimatedEncodingSize() { + return 100; + } + + @Override + public boolean isCanonical() { + return true; + } + + + /** + * Gets the delegated stake on this peer for the given delegator. + * + * Returns 0 if the delegator has no stake. + * + * @param delegator Address of delegator + * @return Value of delegated stake + */ + public long getDelegatedStake(Address delegator) { + // TODO: include rewards? + + CVMLong a = stakes.get(delegator); + if (a == null) return 0; + return a.longValue(); + } + + /** + * Sets the delegated stake on this peer for the given delegator. + * + * A value of 0 will remove the delegator's stake entirely + * + * @param delegator Address of delegator + * @param newStake New Delegated stake for the given Address + * @return Value of delegated stake + */ + public PeerStatus withDelegatedStake(Address delegator, long newStake) { + long oldStake = getDelegatedStake(delegator); + if (oldStake == newStake) return this; + + // compute adjustment to total delegated stake + long newDelegatedStake = delegatedStake + newStake - oldStake; + + ABlobMap newStakes = (newStake == 0L) ? stakes.dissoc(delegator) + : stakes.assoc(delegator, CVMLong.create(newStake)); + return new PeerStatus(controller, stake, newStakes, newDelegatedStake, metadata); + } + + /** + * Sets the Peer Stake on this peer for the given delegator. + * + * A value of 0 will remove the Peer stake entirely + * + * @param newStake New Delegated stake for the given Address + * @return Value of delegated stake + */ + public PeerStatus withPeerStake(long newStake) { + if (stake == newStake) return this; + + return new PeerStatus(controller, newStake, stakes, delegatedStake, metadata); + } + + public PeerStatus withHostname(AString newHostname) { + AHashMap newMeta=metadata; + if (newMeta==null) { + newMeta=Maps.create(Keywords.URL, newHostname); + } else { + newMeta=newMeta.assoc(Keywords.URL, newHostname); + } + if (metadata==newMeta) return this; + + return new PeerStatus(controller, stake, stakes, delegatedStake, newMeta); + } + + @Override + public void validateCell() throws InvalidDataException { + // TODO: Nothing? + } + + @Override + public ACell get(ACell key) { + if (Keywords.CONTROLLER.equals(key)) return controller; + if (Keywords.STAKE.equals(key)) return CVMLong.create(stake); + if (Keywords.STAKES.equals(key)) return stakes; + if (Keywords.DELEGATED_STAKE.equals(key)) return CVMLong.create(delegatedStake); + if (Keywords.METADATA.equals(key)) return metadata; + + return null; + } + + @Override + public byte getTag() { + return Tag.PEER_STATUS; + } + + @SuppressWarnings("unchecked") + @Override + protected PeerStatus updateAll(ACell[] newVals) { + Address newOwner = (Address) newVals[0]; + long newStake = ((CVMLong) newVals[1]).longValue(); + ABlobMap newStakes = (ABlobMap) newVals[2]; + long newDelStake = ((CVMLong) newVals[3]).longValue(); + AHashMap newMeta = (AHashMap) newVals[4]; + + if ((this.stake==newStake)&&(this.stakes==newStakes) + &&(this.metadata==newMeta)&&(this.delegatedStake==newDelStake)) { + return this; + } + return new PeerStatus(newOwner, newStake, newStakes, newDelStake, newMeta); + } + + protected static long computeDelegatedStake(ABlobMap stakes) { + long ds = stakes.reduceValues((acc, e)->acc+e.longValue(), 0L); + return ds; + } + + @Override + public boolean equals(AMap a) { + if (this == a) return true; // important optimisation for e.g. hashmap equality + if (a == null) return false; + if (a.getTag()!=getTag()) return false; + PeerStatus as=(PeerStatus)a; + return equals(as); + } + + /** + * Tests if this PeerStatus is equal to another + * @param a PeerStatus to compare with + * @return true if equal, false otherwise + */ + public boolean equals(PeerStatus a) { + if (a == null) return false; + Hash h=this.cachedHash(); + if (h!=null) { + Hash ha=a.cachedHash(); + if (ha!=null) return Utils.equals(h, ha); + } + + if (stake!=a.stake) return false; + if (delegatedStake!=a.delegatedStake) return false; + if (!(Utils.equals(metadata, a.metadata))) return false; + if (!(Utils.equals(stakes, a.stakes))) return false; + if (!(Utils.equals(controller, a.controller))) return false; + return true; + } +} diff --git a/convex-core/src/main/java/convex/core/data/Ref.java b/convex-core/src/main/java/convex/core/data/Ref.java new file mode 100644 index 000000000..2ae779599 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/Ref.java @@ -0,0 +1,687 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.function.Consumer; + +import convex.core.data.prim.CVMBool; +import convex.core.exceptions.InvalidDataException; +import convex.core.exceptions.MissingDataException; +import convex.core.lang.RT; +import convex.core.store.AStore; +import convex.core.store.Stores; +import convex.core.util.Utils; + +/** + * Class representing a smart reference to a decentralised data object. + * + * "The greatest trick the Devil ever pulled was convincing the world he didn’t + * exist." - The Usual Suspects + * + * A Ref itself is not a cell, but may be contained within a cell, in which case + * the cell class must implement IRefContainer in order to persist and update + * contained Refs correctly + * + * Refs include a status that indicates the level of validation proven. It is + * important not to rely on the value of a Ref until it has a sufficient status + * - e.g. a minimum status of PERSISTED is required to be able to guarantee + * walking an entire nested data structure. + * + * Guarantees: - O(1) access to the Hash value, cached on first access - O(1) + * access to the referenced object (though may required hitting storage if not + * cached) - Indirectly referenced values may be collected by the garbage + * collector, with the assumption that they can be retrieved from storage if + * required + * + * @param Type of stored value + */ +public abstract class Ref extends AObject implements Comparable>, IWriteable, IValidated { + + /** + * Ref status indicating the status of this Ref is unknown. This is the default + * for new Refs + */ + public static final int UNKNOWN = 0; + + /** + * Ref status indicating the Ref has been shallowly persisted in long term + * storage. The Ref can be made soft, and retrieved from storage if needed. No + * guarantee about the existence / status of any child objects. + */ + public static final int STORED = 1; + + /** + * Ref status indicating the Ref has been deeply persisted in long term storage. + * The Ref and its children can be assumed to be accessible for the life of the + * storage subsystem execution. + */ + public static final int PERSISTED = 2; + + /** + * Ref status indicating the Ref has been both persisted and validated as genuine + * valid CVM data. + */ + public static final int VALIDATED = 3; + + /** + * Ref status indicating the Ref has been shared by this peer in an announced + * Belief. This means that the Peer has a commitment to maintain this data + */ + public static final int ANNOUNCED = 4; + + /** + * Ref status indicating the Ref is an internal embedded value that can be + * encoded and used independency of any given store state + */ + public static final int INTERNAL = 5; + + /** + * Maximum Ref status + */ + public static final int MAX_STATUS = INTERNAL; + + /** + * MAsk for Ref flag bits representing the Status + */ + public static final int STATUS_MASK = 0x0F; + + /** + * Mask bit for a proven embedded value + */ + public static final int KNOWN_EMBEDDED_MASK = 0x10; + + /** + * Mask bit for a proven non-embedded value + */ + public static final int NON_EMBEDDED_MASK = 0x20; + + /** + * Mask for embedding status + */ + public static final int EMBEDDING_MASK = KNOWN_EMBEDDED_MASK | NON_EMBEDDED_MASK; + + /** + * Mask bit for verified data, especially signatures + */ + public static final int VERIFIED_MASK = 0x40; + + /** + * Mask bit for bad data, especially signatures proved invalid + */ + public static final int BAD_MASK = 0x80; + + /** + * Mask bit for bad data, especially signatures proved invalid + */ + public static final int VERIFICATION_MASK = VERIFIED_MASK | BAD_MASK; + + /** + * Flags for internal constant values + */ + public static final int INTERNAL_FLAGS=INTERNAL|KNOWN_EMBEDDED_MASK|VERIFIED_MASK; + + /** + * Ref status indicating that the Ref refers to data that has been proven to be invalid + */ + public static final int INVALID = -1; + + /** + * Ref for null value. Important because we can't persist this, since null + * collides with the result of an empty soft reference. + */ + public static final RefDirect NULL_VALUE = RefDirect.create(null, Hash.NULL_HASH, INTERNAL_FLAGS); + + public static final RefDirect TRUE_VALUE = RefDirect.create(CVMBool.TRUE, Hash.TRUE_HASH, INTERNAL_FLAGS); + public static final RefDirect FALSE_VALUE = RefDirect.create(CVMBool.FALSE, Hash.FALSE_HASH, INTERNAL_FLAGS); + + /** + * Length of an external Reference encoding. Will be a tag byte plus the Hash length + */ + public static final int INDIRECT_ENCODING_LENGTH = 1+Hash.LENGTH; + + + + /** + * Hash of the serialised representation of the value Computed and stored upon + * demand. + */ + protected Hash hash; + + /** + * Flag values including Status of this Ref. See public Ref status constants. + * + * May be incremented atomically in the event of validation, proven storage. + */ + protected int flags; + + protected Ref(Hash hash, int flags) { + this.hash = hash; + this.flags = flags; + } + + /** + * Gets the status of this Ref + * + * @return UNKNOWN, PERSISTED, VERIFIED, ACCOUNCED or INVALID Ref status + * constants + */ + public int getStatus() { + return flags&STATUS_MASK; + } + + /** + * Gets flags with an updated status + * @param newStatus New status to apply to flags + * @return Updated flags (does not change this Ref) + */ + public int flagsWithStatus(int newStatus) { + return (flags&~STATUS_MASK)|(newStatus&STATUS_MASK); + } + + /** + * Gets the flags for this Ref + * + * @return flag int value + */ + public int getFlags() { + return flags; + } + + /** + * Ensures the Ref has the given status, at minimum + * + * Assumes any necessary changes to storage will be made separately. + * SECURITY: Dangerous if misused since may invalidate storage assumptions + * @param newStatus New status to apply to Ref + * @return Updated Ref + */ + public Ref withMinimumStatus(int newStatus) { + newStatus&=STATUS_MASK; + int status=getStatus(); + if (status >= newStatus) return this; + if (status > MAX_STATUS) { + throw new IllegalArgumentException("Ref status not recognised: " + newStatus); + } + int newFlags=(flags&(~STATUS_MASK))|newStatus; + return withFlags(newFlags); + } + + /** + * Create a new Ref of the same type with updated flags + * @param newFlags New flags to set + * @return Updated Ref + */ + public abstract Ref withFlags(int newFlags); + + /** + * Gets the value from this Ref. + * + * Important notes: - May throw a MissingDataException if the data does not + * exist in available storage - Will return null if and only if the Ref refers + * to the null value + * + * @return The value contained in this Ref + */ + public abstract T getValue(); + + @Override + public int hashCode() { + return getHash().hashCode(); + } + + @Override + public void print(StringBuilder sb) { + sb.append("#ref {:hash #hash "); + sb.append((hash==null)?"nil":hash.toString()); + sb.append(", :flags "); + sb.append(flags); + sb.append("}"); + } + + @Override + public String toString() { + // TODO. Why protected by a try-catch? Looks like it will never throw. + StringBuilder sb = new StringBuilder(); + try { + print(sb); + } catch (MissingDataException e) { + throw Utils.sneakyThrow(e); + } + return sb.toString(); + } + + @SuppressWarnings("unchecked") + @Override + public boolean equals(Object o) { + if (!(o instanceof Ref)) return false; + return equalsValue((Ref) o); + } + + /** + * Checks if two Ref Values are equal. Equality is defined as referring to the + * same data, i.e. have an identical hash. + * + * @param a The Ref to compare with + * @return true if Refs have the same value, false otherwise + */ + public abstract boolean equalsValue(Ref a); + + @Override + public int compareTo(Ref a) { + if (this == a) return 0; + return getHash().compareTo(a.getHash()); + } + + /** + * Gets the Hash of this ref's value. + * + * @return Hash of the value + */ + public abstract Hash getHash(); + + /** + * Gets the Hash of this ref's value, or null if not yet computed + * + * @return Hash of the value + */ + public final Hash cachedHash() { + return hash; + } + + /** + * Returns a direct Ref wrapping the given value. Does not perform any Ref + * lookup in stores etc. + * + * @param value Value to wrap in the Ref + * @return New Ref wrapping the given value. + */ + @SuppressWarnings("unchecked") + public static Ref get(T value) { + if (value==null) return (Ref) NULL_VALUE; + return value.getRef(); + } + + @SuppressWarnings("unchecked") + public static Ref get(Object value) { + if (value==null) return (Ref) NULL_VALUE; + if (value instanceof ACell) return ((ACell)value).getRef(); + return RT.cvm(value).getRef(); + } + + /** + * Creates a RefSoft using a specific Hash. Fetches the actual value lazily from the + * store on demand. + * + * Internal soft reference may be initially empty: This Ref might not have + * available data in the store, in which case calls to getValue() may result in + * a MissingDataException + * + * WARNING: Does not mark as either embedded or non-embedded, as this might be a top level + * entry in the store. isEmbedded() will query the store to determine status. + * + * @param hash The hash value for this Ref to refer to + * @return Ref for the specific hash. + */ + public static RefSoft forHash(Hash hash) { + return RefSoft.createForHash(hash); + } + + public Ref markEmbedded(boolean isEmbedded) { + int newFlags=mergeFlags(flags,(isEmbedded?KNOWN_EMBEDDED_MASK:NON_EMBEDDED_MASK)); + flags=newFlags; + return this; + } + + /** + * Sets the Flags for this Ref. WARNING: caller must have performed any necessary validation + * @param newFlags Flags to set + * @return Updated Ref + */ + public Ref setFlags(int newFlags) { + flags=newFlags; + return this; + } + + /** + * Reads a ref from the given ByteBuffer. Assumes no tag. + * + * Marks as non-embedded + * + * @param data ByteBuffer containing the data to read at the current position + * @return Ref read from ByteBuffer + */ + public static Ref readRaw(ByteBuffer data) { + Hash h = Hash.readRaw(data); + Ref ref=Ref.forHash(h); + return ref.markEmbedded(false); + } + + public void validate() throws InvalidDataException { + if (hash != null) hash.validate(); + // TODO is this sane? + if (getStatus() < VALIDATED) { + T o = getValue(); + o.validate(); + } + } + + /** + * Return true if this Ref is a direct reference, i.e. the value is pinned in + * memory and cannot be garbage collected + * + * @return true if this Ref is direct, false otherwise + */ + public abstract boolean isDirect(); + + /** + * Return true if this Ref's status indicates it has definitely been persisted + * to storage. + * + * May return false negatives, e.g. the object could be in the store but this + * Ref instance still has a status of "UNKNOWN". + * + * @return true if this Ref has a status of PERSISTED or above, false otherwise + */ + public boolean isPersisted() { + return getStatus() >= PERSISTED; + } + + /** + * Persists this Ref in the current store if not embedded and not already + * persisted. + * + * This may convert the Ref from a direct reference to a soft reference. + * + * If the persisted Ref represents novelty, will trigger the specified novelty + * handler + * + * @param noveltyHandler Novelty handler to call (may be null) + * @return the persisted Ref + * @throws MissingDataException If the Ref's value does not exist or has been + * garbage collected before being persisted + */ + @SuppressWarnings("unchecked") + public Ref persist(Consumer> noveltyHandler) { + int status = getStatus(); + if (status >= PERSISTED) return (Ref) this; // already persisted in some form + AStore store=Stores.current(); + return (Ref) store.storeRef(this, Ref.PERSISTED,noveltyHandler); + } + + /** + * Persists this Ref in the current store if not embedded and not already + * persisted. Resulting status will be PERSISTED or higher. + * + * This may convert the Ref from a direct reference to a soft reference. + * + * @throws MissingDataException if the Ref cannot be fully persisted. + * @return the persisted Ref + */ + public Ref persist() { + return persist(null); + } + + /** + * Accumulates the set of all unique Refs in the given object. + * + * Might stack overflow if nesting is too deep - not for use in on-chain code. + * + * @param a Ref or Cell + * @return Set containing all unique refs (accoumulated recursively) within the + * given object + */ + public static java.util.Set> accumulateRefSet(Object a) { + HashSet> hs = new HashSet<>(); + accumulateRefSet(a, hs); + return hs; + } + + private static void accumulateRefSet(Object a, HashSet> hs) { + if (a instanceof Ref) { + Ref ref = (Ref) a; + if (hs.contains(ref)) return; + hs.add(ref); + accumulateRefSet(ref.getValue(), hs); + } else if (a instanceof ACell) { + ACell rc = (ACell) a; + rc.updateRefs(r -> { + accumulateRefSet(r, hs); + return r; + }); + } + } + + + + /** + * Updates an array of Refs with the given function. + * + * Returns the original array unchanged if no refs were changed, otherwise + * returns a new array. + * + * @param refs Array of Refs to update + * @param func Ref update function + * @return Array of updated Refs + */ + public static Ref[] updateRefs(Ref[] refs, IRefFunction func) { + Ref[] newRefs = refs; + int n = refs.length; + for (int i = 0; i < n; i++) { + Ref ref = refs[i]; + @SuppressWarnings("unchecked") + Ref newRef = (Ref) func.apply(ref); + if (ref != newRef) { + // Ensure newRefs is a new copy since we are making at least one change + if (newRefs == refs) { + newRefs = refs.clone(); + } + newRefs[i] = newRef; + } + } + return newRefs; + } + + @SuppressWarnings("unchecked") + public static Ref[] createArray(T[] values) { + int n = values.length; + Ref[] refs = new Ref[n]; + for (int i = 0; i < n; i++) { + refs[i] = Ref.get(values[i]); + } + return refs; + } + + /** + * Adds the value of this Ref and all non-embedded child values to a given set. + * + * Logically, provides the guarantee that the set will contain all cells needed + * to recreate the complete value of this Ref. + * + * @param store Store to add to + * @return Set containing this Ref and all direct or indirect child refs + */ + @SuppressWarnings("unchecked") + public ASet addAllToSet(ASet store) { + store = store.includeRef((Ref) this); + ACell rc = getValue(); + + int n = rc.getRefCount(); + for (int i = 0; i < n; i++) { + Ref rr = rc.getRef(i); + if (rr.isEmbedded()) continue; + store = rr.addAllToSet(store); + } + return store; + } + + /** + * Check if the Ref's value is embedded. + * + * If false, the value must be an ACell instance. + * + * @return true if embedded, false otherwise + */ + public final boolean isEmbedded() { + if ((flags&KNOWN_EMBEDDED_MASK)!=0) return true; + if ((flags&NON_EMBEDDED_MASK)!=0) return false; + boolean em= Format.isEmbedded(getValue()); + flags=flags|(em?KNOWN_EMBEDDED_MASK:NON_EMBEDDED_MASK); + return em; + } + + /** + * Converts this Ref to a RefDirect + * @return Direct Ref + */ + public Ref toDirect() { + return RefDirect.create(getValue(), hash, flags); + } + + /** + * Persists a Ref shallowly in the current store. + * + * Status will be updated to STORED or higher. + * + * @return Ref with status of STORED or above + */ + public Ref persistShallow() { + return persistShallow(null); + } + + /** + * Persists a Ref shallowly in the current store. + * + * Status will be updated STORED or higher. Novelty handler will be called exactly once if and only if + * the ref was not previously stored + * + * @param noveltyHandler Novelty handler to call (may be null) + * @return Ref with status of STORED or above + */ + @SuppressWarnings("unchecked") + public Ref persistShallow(Consumer> noveltyHandler) { + AStore store=Stores.current(); + return (Ref) store.storeTopRef((Ref)this, Ref.STORED, noveltyHandler); + } + + /** + * Updates the value stored within this Ref. New value must be equal in value to the old value + * (identical hash), but may have updated internal refs etc. + * + * @param newValue New value + * @return Updated Ref + */ + public abstract Ref withValue(T newValue); + + /** + * Writes the ref to a byte array. Embeds embedded values as necessary. + * @param bs Byte array to encode to + * @return Updated position + */ + @Override + public final int encode(byte[] bs, int pos) { + if (isEmbedded()) { + T value=getValue(); + if (value==null) { + bs[pos++]=Tag.NULL; + return pos; + } + return value.encode(bs, pos); + } else { + bs[pos++]=Tag.REF; + return getHash().encodeRaw(bs, pos); + } + } + + @Override + public final ByteBuffer write(ByteBuffer bb) { + if (isEmbedded()) { + return Format.write(bb, getValue()); + } else { + bb=bb.put(Tag.REF); + return getHash().writeToBuffer(bb); + } + } + + @Override + protected Blob createEncoding() { + if (isEmbedded()) { + return Format.encodedBlob(getValue()); + } + + byte[] bs=new byte[Ref.INDIRECT_ENCODING_LENGTH]; + Hash h=getHash(); + int pos=0; + bs[pos++]=Tag.REF; + pos=h.encodeRaw(bs, pos); + return Blob.wrap(bs,0,pos); + } + + /** + * Gets the encoding length for writing this Ref. Will be equal to the encoding length + * of the Ref's value if embedded, otherwise INDIRECT_ENCODING_LENGTH + * + * @return Exact length of encoding + */ + public final long getEncodingLength() { + if (isEmbedded()) { + T value=getValue(); + if (value==null) return 1; + return value.getEncodingLength(); + } else { + return Ref.INDIRECT_ENCODING_LENGTH; + } + } + + /** + * Gets the indirect memory size for this Ref + * @return 0 for fully embedded values with no child refs, memory size of referred value otherwise + */ + public long getMemorySize() { + T value=getValue(); + if (value==null) return 0; + return value.getMemorySize(); + } + + /** + * Finds all instances of missing data in this Ref, and adds them to the missing set + * @param missingSet Set to add missing instances to + */ + public void findMissing(HashSet missingSet) { + if (getStatus()>=Ref.PERSISTED) return; + if (isMissing()) { + missingSet.add(getHash()); + } else { + // Should be OK to get value, since non-missing! + T val=getValue(); + + // TODO: maybe needs to be non-stack-consuming? + // recursively scan for missing children + int n=val.getRefCount(); + for (int i=0; i r=val.getRef(i); + r.findMissing(missingSet); + } + } + } + + /** + * Checks if this Ref refers to missing data, i.e. a Cell that does not exist in the + * currect store. + * + * @return true if this specific Ref has missing data, false otherwise. + */ + public abstract boolean isMissing(); + + /** + * Merges flags in an idempotent way. Assume flags are valid + * @param a First set of flags + * @param b Second set of flags + * @return Merged flags + */ + public static int mergeFlags(int a, int b) { + return ((a|b)&~STATUS_MASK)|Math.max(a&STATUS_MASK, b& STATUS_MASK); + } + + + +} diff --git a/convex-core/src/main/java/convex/core/data/RefDirect.java b/convex-core/src/main/java/convex/core/data/RefDirect.java new file mode 100644 index 000000000..907c553c3 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/RefDirect.java @@ -0,0 +1,130 @@ +package convex.core.data; + +import convex.core.exceptions.InvalidDataException; +import convex.core.util.Utils; + +/** + * Ref subclass for direct in-memory references. + * + * Direct Refs store the underlying value directly with a regular Java strong reference. + * + *

Care must be taken to ensure recursive structures do not exceed reasonable memory bounds. + * In smart contract execution, juice limits serve this purpose.

+ * + * @param Type of Value referenced + */ +public class RefDirect extends Ref { + /** + * Direct value of this Ref + */ + private final T value; + + private RefDirect(T value, Hash hash, int flags) { + super(hash, flags); + + this.value = value; + } + + /** + * Construction function for a Direct Ref + * @param Type of value + * @param value Value for the Ref + * @param hash Hash (may be null) + * @param status Status for the Ref + * @return New Direct Ref + */ + public static RefDirect create(T value, Hash hash, int status) { + int flags=status&Ref.STATUS_MASK; + return new RefDirect(value, hash, flags); + } + + /** + * Creates a direct Ref to the given value + * @param Type of value + * @param value Any value (may be embedded or otherwise, but should not be null) + * @param hash Hash of value's encoding, or null if not known + * @return Direct Ref to Value + */ + public static RefDirect create(T value, Hash hash) { + return create(value, hash, UNKNOWN); + } + + /** + * Creates a new Direct ref to the given value. Does not compute hash. + * @param Type of Value + * @param value Value + * @return Direct Ref to Value + */ + @SuppressWarnings("unchecked") + public static RefDirect create(T value) { + if (value==null) return (RefDirect) Ref.NULL_VALUE; + return create(value, null, UNKNOWN); + } + + public T getValue() { + return value; + } + + @Override + public boolean isDirect() { + return true; + } + + @Override + public Hash getHash() { + if (hash!=null) return hash; + Hash newHash=(value==null)?Hash.NULL_HASH:value.getHash(); + hash=newHash; + return newHash; + } + + @Override + public Ref toDirect() { + return this; + } + + @Override + public boolean equalsValue(Ref a) { + if (a == this) return true; + if (this.hash != null) { + // use hash if available + if (a.hash != null) return this.hash.equals(a.hash); + } + return Utils.equals(this.value, a.getValue()); + } + + @Override + public void validate() throws InvalidDataException { + super.validate(); + if (isEmbedded() != Format.isEmbedded(value)) throw new InvalidDataException("Embedded flag is wrong!", this); + if (value == null) { + if (this != Ref.NULL_VALUE) throw new InvalidDataException("Null ref not singleton!", this); + } + } + + @Override + public Ref withValue(T newValue) { + if (newValue!=value) return new RefDirect(newValue,hash,flags); + return this; + } + + @Override + public int estimatedEncodingSize() { + if(value==null) return Format.NULL_ENCODING_LENGTH; + return isEmbedded()?value.estimatedEncodingSize():Ref.INDIRECT_ENCODING_LENGTH; + } + + @Override + public boolean isMissing() { + // Never missing, since we have the value at hand + return false; + } + + @Override + public RefDirect withFlags(int newFlags) { + return new RefDirect(value,hash,newFlags); + } + + + +} diff --git a/convex-core/src/main/java/convex/core/data/RefSoft.java b/convex-core/src/main/java/convex/core/data/RefSoft.java new file mode 100644 index 000000000..9b0dcecce --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/RefSoft.java @@ -0,0 +1,151 @@ +package convex.core.data; + +import java.lang.ref.SoftReference; + +import convex.core.exceptions.InvalidDataException; +import convex.core.exceptions.MissingDataException; +import convex.core.store.Stores; +import convex.core.util.Utils; + +/** + * Reference class implemented via a soft reference and store lookup. + * + * Ref makes use of a soft reference to values, allowing memory to be reclaimed + * by the garbage collector when not required. A MissingDataException will occur + * with any attempt to deference this Ref when the value is not present + * and not stored in the current store. + * + * Instances of this class should usually be be STORED, otherwise data loss + * may occur due to garbage collection. However UNKNOWN RefSoft may exist temporarily + * (e.g. reading Refs from external messages) + * + * SoftRef must always have a non-null hash, to ensure lookup capability in + * store. + * + * @param Type of referenced Cell + */ +public class RefSoft extends Ref { + + /** + * SoftReference to value. Might get updated to a fresh instance. + */ + protected SoftReference softRef; + + protected RefSoft(SoftReference ref, Hash hash, int flags) { + super(hash, flags); + this.softRef = ref; + } + + protected RefSoft(T value, Hash hash, int flags) { + this(new SoftReference(value), hash, flags); + } + + protected RefSoft(Hash hash) { + // We don't know anything about this Ref. + this(new SoftReference(null), hash, UNKNOWN); + } + + + @Override + public RefSoft withFlags(int newFlags) { + return new RefSoft(softRef,hash,newFlags); + } + + public static RefSoft create(T value, int flags) { + Hash hash=Hash.compute(value); + return new RefSoft(value, hash, flags); + } + + /** + * Create a RefSoft with a Hash reference. + * + * Attempts to get the value will trigger a store lookup, which may in turn + * cause a MissingDataException if not found. + * + * @param Type of value + * @param hash Hash ID of value. + * @return New RefSoft instance + */ + public static RefSoft createForHash(Hash hash) { + return new RefSoft(hash); + } + + @Override + public T getValue() { + T result = softRef.get(); + if (result == null) { + Ref storeRef = Stores.current().refForHash(hash); + if (storeRef == null) throw Utils.sneakyThrow(new MissingDataException(hash)); + result = storeRef.getValue(); + + if (storeRef instanceof RefSoft) { + // Update soft reference to the fresh version. No point keeping old one.... + this.softRef = ((RefSoft) storeRef).softRef; + } else { + this.softRef = new SoftReference(result); + } + } + return result; + } + + @Override + public boolean isMissing() { + T result = softRef.get(); + if (result != null) return false; // still in memory, so not missing + + // check store + Ref storeRef = Stores.current().refForHash(hash); + if (storeRef == null) return true; // must be missing, couldn't find in store + + // We know we have in store. + // Update soft reference to the fresh version. No point keeping old one.... + if (storeRef instanceof RefSoft) { + this.softRef = ((RefSoft) storeRef).softRef; + } else { + this.softRef = new SoftReference(storeRef.getValue()); + } + return false; + } + + @Override + public boolean equalsValue(Ref a) { + if (a.hash!=null) { + return hash.equals(a.hash); + } + // compare by value + return Utils.equals(getValue(),a.getValue()); + } + + @Override + public boolean isDirect() { + return false; + } + + @Override + public Hash getHash() { + return hash; + } + + + @Override + public void validate() throws InvalidDataException { + super.validate(); + if (hash == null) throw new InvalidDataException("Hash should never be null in soft ref", this); + ACell val = softRef.get(); + boolean embedded=isEmbedded(); + if (embedded!=Format.isEmbedded(val)) { + throw new InvalidDataException("Embedded flag ["+embedded+"] inconsistent with value", this); + } + } + + @Override + public Ref withValue(T newValue) { + if (softRef.get()!=newValue) return new RefSoft(newValue,hash,flags); + return this; + } + + @Override + public int estimatedEncodingSize() { + return isEmbedded()?Format.MAX_EMBEDDED_LENGTH: INDIRECT_ENCODING_LENGTH; + } +} diff --git a/convex-core/src/main/java/convex/core/data/SetLeaf.java b/convex-core/src/main/java/convex/core/data/SetLeaf.java new file mode 100644 index 000000000..a83ac8409 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/SetLeaf.java @@ -0,0 +1,535 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.function.BiFunction; + +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.exceptions.TODOException; +import convex.core.util.Utils; + +/** + * Limited size Persistent Merkle Set implemented as a small sorted list of + * Values + * + * Must be sorted by Key hash value to ensure uniqueness of representation + * + * @param Type of values + */ +public class SetLeaf extends AHashSet { + /** + * Maximum number of entries in a SetLeaf + */ + public static final int MAX_ENTRIES = 16; + + private final Ref[] entries; + + SetLeaf(Ref[] items) { + super(items.length); + entries = items; + } + + /** + * Creates a SetLeaf with the specified elements. + * + * Null entries are ignored/removed. + * + * @param elements Refs of Elements to include + * @return New ListMap + */ + @SafeVarargs + public static SetLeaf create(Ref... elements) { + return create(elements, 0, elements.length); + } + + /** + * Creates a SetLeaf with the specified elements. Null references are + * ignored/removed. + * + * @param entries Refs to elements to include (some may be null) + * @param offset Offset into entries array + * @param length Number of entries to take from entries array, starting at + * offset + * @return A new ListMap + */ + protected static SetLeaf create(Ref[] entries, int offset, int length) { + if (length == 0) return Sets.empty(); + if (length > MAX_ENTRIES) throw new IllegalArgumentException("Too many elements: " + entries.length); + Ref[] sorted = Utils.copyOfRangeExcludeNulls(entries, offset, offset + length); + if (sorted.length == 0) return Sets.empty(); + Arrays.sort(sorted); + return new SetLeaf(sorted); + } + + @SuppressWarnings("unchecked") + public static SetLeaf create(V item) { + return new SetLeaf(new Ref[] { Ref.get(item) }); + } + + @Override + public Ref getValueRef(ACell k) { + // Use cached hash if available + Hash h=(k==null)?Hash.NULL_HASH:k.cachedHash(); + if (h!=null) return getRefByHash(h); + + int len = size(); + for (int i = 0; i < len; i++) { + Ref e = entries[i]; + if (Utils.equals(k, e.getValue())) return e; + } + return null; + } + + @Override + public boolean containsHash(Hash hash) { + return getRefByHash(hash) != null; + } + + @Override + protected Ref getRefByHash(Hash hash) { + int len = size(); + for (int i = 0; i < len; i++) { + Ref e = entries[i]; + if (hash.equals(e.getHash())) return e; + } + return null; + } + + /** + * Gets the index of key k in the internal array, or -1 if not found + * + * @param key + * @return + */ + private int seek(T key) { + int len = size(); + for (int i = 0; i < len; i++) { + if (Utils.equals(key, entries[i].getValue())) return i; + } + return -1; + } + + /** + * Gets the index of key k in the internal array, or -1 if not found + * + * @param key + * @return + */ + private int seekKeyRef(Ref key) { + Hash h=key.getHash(); + int len = size(); + for (int i = 0; i < len; i++) { + if (h.compareTo(entries[i].getHash())==0) return i; + } + return -1; + } + + @SuppressWarnings("unchecked") + @Override + public SetLeaf exclude(ACell key) { + int i = seek((T)key); + if (i < 0) return this; // not found + return excludeAt(i); + } + + @Override + public SetLeaf excludeRef(Ref key) { + int i = seekKeyRef(key); + if (i < 0) return this; // not found + return excludeAt(i); + } + + @SuppressWarnings("unchecked") + private SetLeaf excludeAt(int index) { + int len = size(); + if (len == 1) return Sets.empty(); + Ref[] newEntries = (Ref[]) new Ref[len - 1]; + System.arraycopy(entries, 0, newEntries, 0, index); + System.arraycopy(entries, index + 1, newEntries, index, len - index - 1); + return new SetLeaf(newEntries); + } + + protected void accumulateValues(ArrayList al) { + for (int i = 0; i < entries.length; i++) { + Ref me = entries[i]; + al.add(me.getValue()); + } + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.SET; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + long n=count(); + pos = Format.writeVLCLong(bs,pos, n); + + for (int i = 0; i < n; i++) { + pos = entries[i].encode(bs, pos);; + } + return pos; + } + + @Override + public int estimatedEncodingSize() { + // allow space for header, size byte, 2 refs per entry + return 2 + Format.MAX_EMBEDDED_LENGTH * size(); + } + + public static int MAX_ENCODING_LENGTH= 2 + MAX_ENTRIES * Format.MAX_EMBEDDED_LENGTH; + + /** + * Reads a MapLeaf from the provided ByteBuffer Assumes the header byte is + * already read. + * + * @param bb ByteBuffer to read from + * @param count Count of map elements + * @return A Map as deserialised from the provided ByteBuffer + * @throws BadFormatException If encoding is invalid + */ + @SuppressWarnings("unchecked") + public static SetLeaf read(ByteBuffer bb, long count) throws BadFormatException { + if (count == 0) return Sets.empty(); + if (count < 0) throw new BadFormatException("Negative count of map elements!"); + if (count > MAX_ENTRIES) throw new BadFormatException("MapLeaf too big: " + count); + + Ref[] items = (Ref[]) new Ref[(int) count]; + for (int i = 0; i < count; i++) { + Ref ref=Format.readRef(bb); + items[i]=ref; + } + + if (!isValidOrder(items)) { + throw new BadFormatException("Bad ordering of keys!"); + } + + return new SetLeaf(items); + } + + + @SuppressWarnings("unchecked") + public static SetLeaf emptySet() { + return (SetLeaf) Sets.EMPTY; + } + + @Override + public boolean isCanonical() { + // validation for both key uniqueness and sort order + return isValidOrder(entries); + } + + @Override public final boolean isCVMValue() { + return true; + } + + private static boolean isValidOrder(Ref[] entries) { + long count = entries.length; + for (int i = 0; i < count - 1; i++) { + Hash a = entries[i].getHash(); + Hash b = entries[i + 1].getHash(); + if (a.compareTo(b) >= 0) { + return false; + } + } + return true; + } + + @Override + public int getRefCount() { + return entries.length; + } + + @SuppressWarnings("unchecked") + @Override + public Ref getRef(int i) { + Ref e = entries[i]; // IndexOutOfBoundsException if out of range + return e; + } + + @Override + public Ref getElementRef(long i) { + Ref e = entries[Utils.checkedInt(i)]; // Exception if out of range + return e; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public SetLeaf updateRefs(IRefFunction func) { + int n = entries.length; + if (n == 0) return this; + Ref[] newEntries = entries; + for (int i = 0; i < n; i++) { + Ref e = newEntries[i]; + Ref newEntry = (Ref) func.apply(e); + if (e!=newEntry) { + if (newEntries==entries) newEntries=entries.clone(); + newEntries[i]=newEntry; + } + } + if (newEntries==entries) return this; + // Note: we assume no key hashes have changed + return new SetLeaf(newEntries); + } + + /** + * Filters this ListMap to contain only key hashes with the hex digits specified + * in the given Mask + * + * @param digitPos Position of the hex digit to filter + * @param mask Mask of digits to include + * @return Filtered ListMap + */ + public SetLeaf filterHexDigits(int digitPos, int mask) { + mask = mask & 0xFFFF; + if (mask == 0) return Sets.empty(); + if (mask == 0xFFFF) return this; + int sel = 0; + int n = size(); + for (int i = 0; i < n; i++) { + Hash h = entries[i].getHash(); + if ((mask & (1 << h.getHexDigit(digitPos))) != 0) { + sel = sel | (1 << i); // include this index in selection + } + } + if (sel == 0) return Sets.empty(); // no entries selected + return filterEntries(sel); + } + + /** + * Filters entries using the given bit mask + * + * @param selection + * @return + */ + @SuppressWarnings("unchecked") + private SetLeaf filterEntries(int selection) { + if (selection == 0) return Sets.empty(); // no items selected + int n = size(); + if (selection == ((1 << n) - 1)) return this; // all items selected + Ref[] newEntries = new Ref[Integer.bitCount(selection)]; + int ix = 0; + for (int i = 0; i < n; i++) { + if ((selection & (1 << i)) != 0) { + newEntries[ix++] = entries[i]; + } + } + assert (ix == Integer.bitCount(selection)); + return new SetLeaf(newEntries); + } + + @Override + public AHashSet mergeWith(AHashSet b, int setOp) { + return mergeWith(b,setOp,0); + } + + @Override + protected AHashSet mergeWith(AHashSet b, int setOp, int shift) { + if (b instanceof SetLeaf) return mergeWith((SetLeaf) b, setOp,shift); + if (b instanceof SetTree) return b.mergeWith(this, reverseOp(setOp),shift); + throw new TODOException("Unhandled map type: " + b.getClass()); + } + + public AHashSet mergeWith(SetLeaf b, int setOp,int shift) { + if (this.equals(b)) return applySelf(setOp); // no change in identical case + int al = this.size(); + int bl = b.size(); + int ai = 0; + int bi = 0; + ArrayList> results = null; + while ((ai < al) || (bi < bl)) { + Ref ae = (ai < al) ? this.entries[ai] : null; + Ref be = (bi < bl) ? b.entries[bi] : null; + int c = (ae == null) ? 1 : ((be == null) ? -1 : ae.getHash().compareTo(be.getHash())); + + Ref newE; + if (c==0) { + newE= applyOp(setOp,ae,be); + } else if (c<0) { + // apply to a + newE= applyOp(setOp,ae,null); + } else { + // apply to a + newE= applyOp(setOp,null,be); + } + + // Create results arraylist if any difference from this + if ((results == null) && (newE != ((c <= 0) ? ae : null))) { + // create new results array if difference detected + results = new ArrayList<>(2*MAX_ENTRIES); // space for all if needed + // include new entries + for (int i = 0; i < ai; i++) { + results.add(entries[i]); + } + } + if (c <= 0) ai++; // inc ai if we used ae + if (c >= 0) bi++; // inc bi if we used be + if ((results != null) && (newE != null)) results.add(newE); + } + if (results == null) return this; + return Sets.createWithShift(shift,results); + } + + public R reduceValues(BiFunction func, R initial) { + int n = size(); + R result = initial; + for (int i = 0; i < n; i++) { + result = func.apply(result, entries[i].getValue()); + } + return result; + } + + @Override + public boolean equals(ASet a) { + if (!(a instanceof SetLeaf)) return false; + return equals((SetLeaf) a); + } + + public boolean equals(SetLeaf a) { + if (this == a) return true; + int n = size(); + if (n != a.size()) return false; + for (int i = 0; i < n; i++) { + if (!entries[i].equals(a.entries[i])) return false; + } + return true; + } + + @Override + protected void validateWithPrefix(Hash prefix, int digit, int shift) throws InvalidDataException { + for (int i = 0; i < entries.length; i++) { + Ref e = entries[i]; + Hash h = e.getHash(); + long match=h.commonHexPrefixLength(prefix); + if (match<(shift-1)) { + throw new InvalidDataException("Parent prefix did not match",this); + } + int mydigit=h.getHexDigit(shift); + if (mydigit!=digit) { + throw new InvalidDataException("Bad hex digit at position: "+shift,this); + } + e.validate(); + } + } + + @Override + public void validateCell() throws InvalidDataException { + if ((count == 0) && (this != Sets.EMPTY)) { + throw new InvalidDataException("Empty map not using canonical instance", this); + } + + if (count > MAX_ENTRIES) { + throw new InvalidDataException("Too many items in list map: " + entries.length, this); + } + + // validates both key uniqueness and sort order + if (!isCanonical()) { + throw new InvalidDataException("Non-canonical key ordering", this); + } + } + + @Override + public boolean containsAll(ASet b) { + if (this==b) return true; + + // if set is too big, can't possibly contain all keys + if (b.count()>count) return false; + + // must be a setleaf if this size or smaller + return containsAll((SetLeaf)b); + } + + @Override + public boolean isSubset(ASet b) { + return b.containsAll(this); + } + + protected boolean containsAll(SetLeaf b) { + int ix=0; + for (Ref meb:b.entries) { + Hash bh=meb.getHash(); + + if (ix>=count) return false; // no remaining entries in this + while (ix mea=entries[ix]; + Hash ah=mea.getHash(); + int c=ah.compareTo(bh); + if (c<0) { + // ah is smaller than bh + // need to advance ix and try next entry + ix++; + if (ix>=count) return false; // not found + continue; + } else if (c>0) { + return false; // didn't contain the key entry + } else { + // found it, so advance to next entry in b and update ix + ix++; + break; + } + } + } + + return true; + } + + @Override + public AHashSet includeRef(Ref ref) { + return includeRef(ref,0); + } + + @Override + protected AHashSet includeRef(Ref e, int shift) { + int n=entries.length; + Hash h=e.getHash(); + int pos=0; + for (; pos iref=entries[pos]; + int c=h.compareTo(iref.getHash()); + if (c==0) return this; + if (c<0) break; // need to add at this position + } + + // New element must be added at pos + @SuppressWarnings("unchecked") + Ref[] newEntries=new Ref[n+1]; + System.arraycopy(entries, 0, newEntries, 0, pos); + System.arraycopy(entries, pos, newEntries, pos+1, n-pos); + newEntries[pos]=e; + + if (n(newEntries); + } else { + // expand to tree + return SetTree.create(newEntries, shift); + } + } + + @SuppressWarnings("unchecked") + @Override + protected void copyToArray(R[] arr, int offset) { + for (int i=0; i toCanonical() { + if (count<=MAX_ENTRIES) return this; + return SetTree.create(entries, 0); + } + + + + +} diff --git a/convex-core/src/main/java/convex/core/data/SetTree.java b/convex-core/src/main/java/convex/core/data/SetTree.java new file mode 100644 index 000000000..640dd6962 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/SetTree.java @@ -0,0 +1,655 @@ +package convex.core.data; + +import java.nio.ByteBuffer; + +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.util.Bits; +import convex.core.util.Utils; + +/** + * Persistent Set for large hash sets requiring tree structure. + * + * Internally implemented as a radix tree, indexed by key hash. Uses an array of + * child Maps, with a bitmap mask indicating which hex digits are present, i.e. + * have non-empty children. + * + * @param Type of set elemets + */ +public class SetTree extends AHashSet { + + /** + * Child maps, one for each present bit in the mask, max 16 + */ + private final Ref>[] children; + + /** + * Shift position of this treemap node in number of hex digits + */ + private final int shift; + + /** + * Mask indicating which hex digits are present in the child array e.g. 0x0001 + * indicates all children are in the '0' digit. e.g. 0xFFFF indicates there are + * children for every digit. + */ + private final short mask; + + private SetTree(Ref>[] blocks, int shift, short mask, long count) { + super(count); + this.children = blocks; + this.shift = shift; + this.mask = mask; + } + + /** + * Computes the total count from an array of Refs to sets. Ignores null Refs in + * child array + * + * @param children + * @return The total count of all child maps + */ + private static long computeCount(Ref>[] children) { + long n = 0; + for (Ref> cref : children) { + if (cref == null) continue; + ASet m = cref.getValue(); + n += m.count(); + } + return n; + } + + @SuppressWarnings("unchecked") + public static SetTree create(Ref[] newEntries, int shift) { + int n = newEntries.length; + if (n <= SetLeaf.MAX_ENTRIES) { + throw new IllegalArgumentException( + "Insufficient distinct entries for TreeMap construction: " + newEntries.length); + } + + // construct full child array + Ref>[] children = new Ref[16]; + for (int i = 0; i < n; i++) { + Ref e = newEntries[i]; + int ix = e.getHash().getHexDigit(shift); + Ref> ref = children[ix]; + if (ref == null) { + children[ix] = SetLeaf.create(e).getRef(); + } else { + AHashSet newChild=ref.getValue().includeRef(e,shift+1); + children[ix] = newChild.getRef(); + } + } + return (SetTree) createFull(children, shift); + } + + /** + * Creates a Tree map given child refs for each digit + * + * @param children An array of children, may refer to nulls or empty maps which + * will be filtered out + * @return + */ + private static AHashSet createFull(Ref>[] children, int shift, long count) { + if (children.length != 16) throw new IllegalArgumentException("16 children required!"); + Ref>[] newChildren = Utils.filterArray(children, a -> { + if (a == null) return false; + AHashSet m = a.getValue(); + return ((m != null) && !m.isEmpty()); + }); + + if (children != newChildren) { + return create(newChildren, shift, Utils.computeMask(children, newChildren), count); + } else { + return create(children, shift, (short) 0xFFFF, count); + } + } + + /** + * Create a MapTree with a full compliment of children. + * @param + * @param + * @param newChildren + * @param shift Shift for child node + * @return + */ + private static AHashSet createFull(Ref>[] newChildren, int shift) { + long count=computeCount(newChildren); + return createFull(newChildren, shift, count); + } + + /** + * Creates a Map with the specified child map Refs. Removes empty maps passed as + * children. + * + * Returns a ListMap for small maps. + * + * @param children Array of Refs to child maps for each bit in mask + * @param shift Shift position (hex digit of key hashes for this map) + * @param mask Mask specifying the hex digits included in the child array at + * this shift position + * @return A new map as specified @ + */ + @SuppressWarnings("unchecked") + private static AHashSet create(Ref>[] children, int shift, short mask, long count) { + int cLen = children.length; + if (Integer.bitCount(mask & 0xFFFF) != cLen) { + throw new IllegalArgumentException( + "Invalid child array length " + cLen + " for bit mask " + Utils.toHexString(mask)); + } + + // compress small counts to SetLeaf + if (count <= SetLeaf.MAX_ENTRIES) { + Ref[] entries = new Ref[Utils.checkedInt(count)]; + int ix = 0; + for (Ref> childRef : children) { + AHashSet child = childRef.getValue(); + long cc = child.count(); + for (long i = 0; i < cc; i++) { + entries[ix++] = child.getElementRef(i); + } + } + assert (ix == count); + return SetLeaf.create(entries); + } + int sel = (1 << cLen) - 1; + short newMask = mask; + for (int i = 0; i < cLen; i++) { + AHashSet child = children[i].getValue(); + if (child.isEmpty()) { + newMask = (short) (newMask & ~(1 << digitForIndex(i, mask))); // remove from mask + sel = sel & ~(1 << i); // remove from selection + } + } + if (mask != newMask) { + return new SetTree(Utils.filterSmallArray(children, sel), shift, newMask, count); + } + return new SetTree(children, shift, mask, count); + } + + @Override + public Ref getElementRef(long i) { + long pos = i; + for (Ref> c : children) { + AHashSet child = c.getValue(); + long cc = child.count(); + if (pos < cc) return child.getElementRef(pos); + pos -= cc; + } + throw new IndexOutOfBoundsException("Entry index: " + i); + } + + @Override + protected Ref getRefByHash(Hash hash) { + int digit = Utils.extractDigit(hash, shift); + int i = Bits.indexForDigit(digit, mask); + if (i < 0) return null; // not present + return children[i].getValue().getRefByHash(hash); + } + + @SuppressWarnings("unchecked") + @Override + public AHashSet exclude(ACell key) { + return excludeRef((Ref) Ref.get(key)); + } + + @Override + public AHashSet excludeRef(Ref keyRef) { + int digit = Utils.extractDigit(keyRef.getHash(), shift); + int i = Bits.indexForDigit(digit, mask); + if (i < 0) return this; // not present + + // dissoc entry from child + AHashSet child = children[i].getValue(); + AHashSet newChild = child.excludeRef(keyRef); + if (child == newChild) return this; // no removal, no change + + AHashSet result=(newChild.isEmpty())?dissocChild(i):replaceChild(i, newChild.getRef()); + return result.toCanonical(); + } + + public AHashSet toCanonical() { + if (count>SetLeaf.MAX_ENTRIES) return this; + int n=Utils.checkedInt(count); + @SuppressWarnings("unchecked") + Ref[] newEntries=new Ref[n]; + for (int i=0; i(newEntries); + } + + @SuppressWarnings("unchecked") + private AHashSet dissocChild(int i) { + int bsize = children.length; + AHashSet child = children[i].getValue(); + Ref>[] newBlocks = (Ref>[]) new Ref[bsize - 1]; + System.arraycopy(children, 0, newBlocks, 0, i); + System.arraycopy(children, i + 1, newBlocks, i, bsize - i - 1); + short newMask = (short) (mask & (~(1 << digitForIndex(i, mask)))); + long newCount = count - child.count(); + return create(newBlocks, shift, newMask, newCount); + } + + @SuppressWarnings("unchecked") + private SetTree insertChild(int digit, Ref> newChild) { + int bsize = children.length; + int i = Bits.positionForDigit(digit, mask); + short newMask = (short) (mask | (1 << digit)); + if (mask == newMask) throw new Error("Digit already present!"); + + Ref>[] newChildren = (Ref>[]) new Ref[bsize + 1]; + System.arraycopy(children, 0, newChildren, 0, i); + System.arraycopy(children, i, newChildren, i + 1, bsize - i); + newChildren[i] = newChild; + long newCount = count + newChild.getValue().count(); + return (SetTree) create(newChildren, shift, newMask, newCount); + } + + /** + * Replaces the child ref at a given index position. Will return this if no change + * + * @param i + * @param newChild + * @return @ + */ + private AHashSet replaceChild(int i, Ref> newChild) { + if (children[i] == newChild) return this; + AHashSet oldChild = children[i].getValue(); + Ref>[] newChildren = children.clone(); + newChildren[i] = newChild; + long newCount = count + newChild.getValue().count() - oldChild.count(); + return create(newChildren, shift, mask, newCount); + } + + public static int digitForIndex(int index, short mask) { + // scan mask for specified index + int found = 0; + for (int i = 0; i < 16; i++) { + if ((mask & (1 << i)) != 0) { + if (found++ == index) return i; + } + } + throw new IllegalArgumentException("Index " + index + " not available in mask map: " + Utils.toHexString(mask)); + } + + @SuppressWarnings("unchecked") + @Override + public SetTree include(ACell value) { + Ref keyRef = (Ref) Ref.get(value); + return includeRef(keyRef, shift); + } + + @Override + protected SetTree includeRef(Ref e, int shift) { + if (this.shift != shift) { + throw new Error("Invalid shift!"); + } + Ref keyRef = e; + int digit = Utils.extractDigit(keyRef.getHash(), shift); + int i = Bits.indexForDigit(digit, mask); + if (i < 0) { + // location not present + AHashSet newChild = SetLeaf.create(e); + return insertChild(digit, newChild.getRef()); + } else { + // location needs update + AHashSet child = children[i].getValue(); + AHashSet newChild = child.includeRef(e, shift + 1); + if (child == newChild) return this; + return (SetTree) replaceChild(i, newChild.getRef()); + } + } + + @Override + public AHashSet includeRef(Ref ref) { + return includeRef(ref,shift); + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.SET; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = Format.writeVLCLong(bs,pos, count); + + bs[pos++] = (byte) shift; + pos = Utils.writeShort(bs, pos,mask); + + int ilength = children.length; + for (int i = 0; i < ilength; i++) { + pos = children[i].encode(bs,pos); + } + return pos; + } + + @Override + public int estimatedEncodingSize() { + // allow space for tag, shift byte byte, 2 byte mask, embedded child refs + return 4 + Format.MAX_EMBEDDED_LENGTH * children.length; + } + + public static int MAX_ENCODING_LENGTH = 4 + Format.MAX_EMBEDDED_LENGTH * 16; + + /** + * Reads a SetTree from the provided ByteBuffer Assumes the header byte and count is + * already read. + * + * @param bb ByteBuffer to read from + * @param count Number of elements + * @return TreeMap instance as read from ByteBuffer + * @throws BadFormatException If encoding is invalid + */ + @SuppressWarnings("unchecked") + public static SetTree read(ByteBuffer bb, long count) throws BadFormatException { + int shift = bb.get(); + short mask = bb.getShort(); + + int ilength = Integer.bitCount(mask & 0xFFFF); + Ref>[] blocks = (Ref>[]) new Ref[ilength]; + + for (int i = 0; i < ilength; i++) { + // need to read as a Ref + Ref> ref = Format.readRef(bb); + blocks[i] = ref; + } + // create directly, we have all values + SetTree result = new SetTree(blocks, shift, mask, count); + if (!result.isValidStructure()) throw new BadFormatException("Problem with TreeMap invariants"); + return result; + } + + @Override + public boolean isCanonical() { + if (count <= MapLeaf.MAX_ENTRIES) return false; + return true; + } + + @Override public final boolean isCVMValue() { + return shift==0; + } + + @Override + public int getRefCount() { + return children.length; + } + + @SuppressWarnings("unchecked") + @Override + public Ref getRef(int i) { + return (Ref) children[i]; + } + + @SuppressWarnings("unchecked") + @Override + public SetTree updateRefs(IRefFunction func) { + int n = children.length; + if (n == 0) return this; + Ref>[] newChildren = children; + for (int i = 0; i < n; i++) { + Ref> child = children[i]; + Ref> newChild = (Ref>) func.apply(child); + if (child != newChild) { + if (children == newChildren) { + newChildren = children.clone(); + } + newChildren[i] = newChild; + } + } + if (newChildren == children) return this; + // Note: we assume no key hashes have changed, so structure is the same + return new SetTree<>(newChildren, shift, mask, count); + } + + @Override + public AHashSet mergeWith(AHashSet b, int setOp) { + return mergeWith(b, setOp, this.shift); + } + + @Override + protected AHashSet mergeWith(AHashSet b, int setOp, int shift) { + if ((b instanceof SetTree)) { + SetTree bt = (SetTree) b; + if (this.shift != bt.shift) throw new Error("Misaligned shifts!"); + return mergeWith(bt, setOp, shift); + } + if ((b instanceof SetLeaf)) return mergeWith((SetLeaf) b, setOp, shift); + throw new Error("Unrecognised map type: " + b.getClass()); + } + + @SuppressWarnings("unchecked") + private AHashSet mergeWith(SetTree b, int setOp, int shift) { + // assume two TreeMaps with identical prefix and shift + assert (b.shift == shift); + int fullMask = mask | b.mask; + // We are going to build full child list only if needed + Ref>[] newChildren = null; + for (int digit = 0; digit < 16; digit++) { + int bitMask = 1 << digit; + if ((fullMask & bitMask) == 0) continue; // nothing to merge at this index + AHashSet ac = childForDigit(digit).getValue(); + AHashSet bc = b.childForDigit(digit).getValue(); + AHashSet rc = ac.mergeWith(bc, setOp, shift + 1); + if (ac != rc) { + if (newChildren == null) { + newChildren = (Ref>[]) new Ref[16]; + for (int ii = 0; ii < digit; ii++) { // copy existing children up to this point + int chi = Bits.indexForDigit(ii, mask); + if (chi >= 0) newChildren[ii] = children[chi]; + } + } + } + if (newChildren != null) newChildren[digit] = rc.getRef(); + } + if (newChildren == null) return this; + return createFull(newChildren, shift); + } + + @SuppressWarnings("unchecked") + private AHashSet mergeWith(SetLeaf b, int setOp, int shift) { + Ref>[] newChildren = null; + int ix = 0; + for (int i = 0; i < 16; i++) { + int imask = (1 << i); // mask for this digit + if ((mask & imask) == 0) continue; + Ref> cref = children[ix++]; + AHashSet child = cref.getValue(); + SetLeaf bSubset = b.filterHexDigits(shift, imask); // filter only relevant elements in b + AHashSet newChild = child.mergeWith(bSubset, setOp, shift + 1); + if (child != newChild) { + if (newChildren == null) { + newChildren = (Ref>[]) new Ref[16]; + for (int ii = 0; ii < children.length; ii++) { // copy existing children + int chi = digitForIndex(ii, mask); + newChildren[chi] = children[ii]; + } + } + } + if (newChildren != null) { + newChildren[i] = newChild.getRef(); + } + } + assert (ix == children.length); + // if any new children created, create a new Map, else use this + AHashSet result = (newChildren == null) ? this : createFull(newChildren, shift); + + SetLeaf extras = b.filterHexDigits(shift, ~mask); + int en = extras.size(); + for (int i = 0; i < en; i++) { + Ref e = extras.getRef(i); + Ref newE = applyOp(setOp,null,e); + if (newE != null) { + // include only new keys where function result is not null. Re-use existing + // entry if possible. + result = result.includeRef(newE, shift); + } + } + return result; + } + + + + /** + * Gets the Ref for the child at the given digit, or an empty map if not found + * + * @param digit The hex digit to query at this TreeMap's shift position + * @return The child map for this digit, or an empty map if the child does not + * exist + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Ref> childForDigit(int digit) { + int ix = Bits.indexForDigit(digit, mask); + if (ix < 0) return (Ref)Sets.emptyRef(); + return children[ix]; + } + + @Override + public boolean equals(ASet a) { + if (!(a instanceof SetTree)) return false; + return equals((SetTree) a); + } + + boolean equals(SetTree b) { + if (this == b) return true; + long n = count; + if (n != b.count) return false; + if (mask != b.mask) return false; + if (shift != b.shift) return false; + + // Fall back to comparing hashes. Probably most efficient in general. + if (getHash().equals(b.getHash())) return true; + return false; + } + + @Override + public void validate() throws InvalidDataException { + super.validate(); + + if (mask == 0) throw new InvalidDataException("TreeMap must have children!", this); + if ((shift <0)||(shift>MAX_SHIFT)) { + throw new InvalidDataException("Invalid shift for SetTree", this); + } + + if (count<=SetLeaf.MAX_ENTRIES) { + throw new InvalidDataException("Count too small [" + count + "] for SetTree", this); + } + + Hash firstHash; + try { + firstHash=getElementRef(0).getHash(); + } catch (ClassCastException e) { + throw new InvalidDataException("Bad child type:" +e.getMessage(), this); + } + + int bsize = children.length; + + long childCount=0;; + for (int i = 0; i < bsize; i++) { + if (children[i] == null) { + throw new InvalidDataException("Null child ref at index " + i,this); + } + + ACell o = children[i].getValue(); + if (!(o instanceof AHashMap)) { + throw new InvalidDataException( + "Expected AHashSet child at index " + i +" but got "+Utils.getClassName(o), this); + } + @SuppressWarnings("unchecked") + AHashSet child = (AHashSet) o; + if (child.isEmpty()) + throw new InvalidDataException("Empty child at index " + i,this); + + if (child instanceof SetTree) { + SetTree childTree=(SetTree) child; + int expectedShift=shift+1; + if (childTree.shift!=expectedShift) { + throw new InvalidDataException("Wrong child shift ["+childTree.shift+"], expected ["+expectedShift+"]",this); + } + } + + Hash childHash=child.getElementRef(0).getHash(); + long pmatch=firstHash.commonHexPrefixLength(childHash); + if (pmatch b) { + if (b instanceof SetTree) { + return containsAll((SetTree)b); + } + // must be a SetLeaf + long n=b.count; + for (long i=0; i me=b.getElementRef(i); + if (!this.containsHash(me.getHash())) return false; + } + return true; + } + + protected boolean containsAll(SetTree map) { + // fist check this mask contains all of target mask + if ((this.mask|map.mask)!=this.mask) return false; + + for (int i=0; i<16; i++) { + Ref> child=this.childForDigit(i); + if (child==null) continue; + + Ref> mchild=map.childForDigit(i); + if (mchild==null) continue; + + if (!(child.getValue().containsAll(mchild.getValue()))) return false; + } + return true; + } + + @Override + public Ref getValueRef(ACell k) { + return getRefByHash(Hash.compute(k)); + } + + @Override + protected void copyToArray(R[] arr, int offset) { + for (int i=0; i child=children[i].getValue(); + child.copyToArray(arr,offset); + offset=Utils.checkedInt(offset+child.count()); + } + } + + @Override + public boolean containsHash(Hash hash) { + return getRefByHash(hash)!=null; + } +} diff --git a/convex-core/src/main/java/convex/core/data/Sets.java b/convex-core/src/main/java/convex/core/data/Sets.java new file mode 100644 index 000000000..f032713cc --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/Sets.java @@ -0,0 +1,106 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; + +import convex.core.exceptions.BadFormatException; +import convex.core.lang.RT; +import convex.core.util.Utils; + +public class Sets { + + static final Ref[] EMPTY_ENTRIES = new Ref[0]; + + @SuppressWarnings({ "rawtypes", "unchecked" }) + static final SetLeaf EMPTY = new SetLeaf(EMPTY_ENTRIES); + + static { + // Set empty Ref flags as internal embedded constant + EMPTY.getRef().setFlags(Ref.INTERNAL_FLAGS); + } + + @SuppressWarnings("rawtypes") + public static final Ref EMPTY_REF = EMPTY.getRef(); + + @SuppressWarnings("unchecked") + public static SetLeaf empty() { + return (SetLeaf) EMPTY; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static Ref> emptyRef() { + return (Ref)EMPTY_REF; + } + + @SuppressWarnings("unchecked") + @SafeVarargs + public static ASet of(Object... elements) { + int n=elements.length; + ASet result=empty(); + for (int i=0; i) result.conj(RT.cvm(elements[i])); + } + return result; + } + + @SuppressWarnings("unchecked") + @SafeVarargs + public static ASet of(ACell... elements) { + int n=elements.length; + ASet result=empty(); + for (int i=0; i) result.conj(elements[i]); + } + return result; + } + + /** + * Creates a set of all the elements in the given data structure + * + * @param Type of elements + * @param source Source for elements + * @return A Set + */ + @SuppressWarnings("unchecked") + public static ASet create(ADataStructure source) { + if (source instanceof ASet) return (ASet) source; + + if (source instanceof AMap) { + ASequence seq = RT.sequence(source); // should always be non-null + return Sets.create(seq); + } + if (source instanceof ACollection) return Sets.fromCollection((Collection) source); + throw new IllegalArgumentException("Unexpected type!" + Utils.getClass(source)); + } + + /** + * Creates a set of all the elements in the given data structure + * + * @param Type of elements + * @param source Source for elements + * @return A Set + */ + public static ASet fromCollection(Collection source) { + return Sets.of(source.toArray()); + } + + public static ASet read(ByteBuffer bb) throws BadFormatException { + long count = Format.readVLCLong(bb); + if (count <= SetLeaf.MAX_ENTRIES) { + return SetLeaf.read(bb, count); + } else { + return SetTree.read(bb, count); + } + } + + public static AHashSet createWithShift(int shift, ArrayList> values) { + AHashSet result=Sets.empty(); + for (Ref v: values) { + result=result.includeRef(v, shift); + } + return result; + } + + +} diff --git a/convex-core/src/main/java/convex/core/data/SignedData.java b/convex-core/src/main/java/convex/core/data/SignedData.java new file mode 100644 index 000000000..a5b5f4025 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/SignedData.java @@ -0,0 +1,306 @@ +package convex.core.data; + +import java.nio.ByteBuffer; + +import convex.core.crypto.AKeyPair; +import convex.core.crypto.ASignature; +import convex.core.crypto.Ed25519Signature; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.BadSignatureException; +import convex.core.exceptions.InvalidDataException; +import convex.core.transactions.ATransaction; + +/** + * Node representing a signed data object. + * + * A signed data object encapsulates: + *
    + *
  • An Address that identifies the signer
  • + *
  • A digital signature
  • + *
  • An underlying Cell that has been signed.
  • + *
+ * + * The SignedData instance is considered valid if the signature can be successfully validated for + * the given Address and data value, and if so can be taken as a cryptographic proof that the signature + * was created by someone in possession of the corresponding private key. + * + * Note we currently go via a Ref here for a few reasons: - It guarantees we + * have a hash for signing - It makes the SignedData object + * implementation/representation independent of the value type - It creates a + * possibility of structural sharing for transaction values excluding signatures + * + * Binary representation: + *
    + *
  1. 1 byte - Tag.SIGNED_DATA tag
  2. + *
  3. 32 bytes - Public Key of signer
  4. + *
  5. 64 bytes - raw Signature data
  6. + *
  7. 1+ bytes - Data Value Ref (may be embedded)
  8. + *
+ * + * SECURITY: signing requires presence of a local keypair TODO: SECURITY: any + * persistence forces validation of Signature?? + * + * @param The type of the signed object + */ +public class SignedData extends ACell { + // Encoded fields + private final AccountKey publicKey; + private final ASignature signature; + private final Ref valueRef; + + private SignedData(Ref refToValue, AccountKey address, ASignature sig) { + this.valueRef = refToValue; + this.publicKey = address; + signature = sig; + } + + /** + * Signs a data value Ref with the given keypair. + * + * SECURITY: Marks as already validated, since we just signed it. + * + * @param keyPair The public/private key pair of the signer. + * @param ref Ref to the data to sign + * @return SignedData object signed with the given key-pair + */ + public static SignedData createWithRef(AKeyPair keyPair, Ref ref) { + ASignature sig = keyPair.sign(ref.getHash()); + SignedData sd = new SignedData(ref, keyPair.getAccountKey(), sig); + sd.markValidated(); + return sd; + } + + /** + * Mark this SignedData as already verified as good - cache in Ref + */ + private void markValidated() { + Ref ref=getRef(); + int flags=ref.getFlags(); + if ((flags&Ref.VERIFIED_MASK)!=0) return; // already done + cachedRef=ref.withFlags(flags|Ref.VERIFIED_MASK); + } + + /** + * Mark this SignedData as a bad signature - cache in Ref + */ + private void markBadSignature() { + Ref ref=getRef(); + int flags=ref.getFlags(); + if ((flags&Ref.BAD_MASK)!=0) return; // already done + cachedRef=ref.withFlags(flags|Ref.BAD_MASK); + } + + + public static SignedData create(AKeyPair keyPair, T value2) { + return createWithRef(keyPair, Ref.get(value2)); + } + + /** + * Creates a SignedData object with the given parameters. + * + * SECURITY: Not assumed to be valid. + * + * @param address Public Address of the signer + * @param sig Signature of the supplied data + * @param ref Ref to the data that has been signed + * @return A new SignedData object + */ + public static SignedData create(AccountKey address, ASignature sig, Ref ref) { + // boolean check=Sign.verify(ref.getHash(), sig, address); + // if (!check) throw new ValidationException("Invalid signature: "+sig); + return new SignedData(ref, address, sig); + } + + + public static SignedData create(AKeyPair kp, ASignature sig, Ref ref) { + + return create(kp.getAccountKey(),sig,ref); + } + + + /** + * Gets the signed value object encapsulated by this SignedData object. + * + * Does not check Signature. + * + * @return Data value that has been signed + */ + public T getValue() { + return valueRef.getValue(); + } + + + /** + * Gets the public key of the signer. If the signature is valid, this + * represents a cryptographic proof that the signer was in possession of the + * private key of this address. + * + * @return Public Key of signer. + */ + public AccountKey getAccountKey() { + return publicKey; + } + + /** + * Gets the Signature that formed part of this SignedData object + * + * @return Signature instance + */ + public ASignature getSignature() { + return signature; + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.SIGNED_DATA; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = publicKey.encodeRaw(bs,pos); + pos = signature.encodeRaw(bs,pos); + pos = valueRef.encode(bs,pos); + return pos; + } + + @Override + public int estimatedEncodingSize() { + return 1+AccountKey.LENGTH+Ed25519Signature.SIGNATURE_LENGTH+Format.MAX_EMBEDDED_LENGTH; + } + + /** + * Reads a SignedData instance from the given ByteBuffer + * + * @param data A ByteBuffer containing + * @return A SignedData object + * @throws BadFormatException If encoding is invalid + */ + public static SignedData read(ByteBuffer data) throws BadFormatException { + // header already assumed to be consumed + AccountKey address = AccountKey.readRaw(data); + ASignature sig = ASignature.read(data); + Ref value = Format.readRef(data); + return create(address, sig, value); + } + + /** + * Validates the signature in this SignedData instance. Caches result + * + * @return true if valid, false otherwise + */ + public boolean checkSignature() { + Ref> sigRef=getRef(); + int flags=sigRef.getFlags(); + if ((flags&Ref.BAD_MASK)!=0) return false; + if ((flags&Ref.VERIFIED_MASK)!=0) return true; + + Hash hash=valueRef.getHash(); + boolean check = signature.verify(hash, publicKey); + + if (check) { + markValidated(); + } else { + markBadSignature(); + } + return check; + } + + /** + * Checks if the signature has already gone through verification. MAy or may + * not be a valid signature. + * + * @return true if valid, false otherwise + */ + public boolean isSignatureChecked() { + Ref> sigRef=getRef(); + if (sigRef==null) return false; + int flags=sigRef.getFlags(); + return (flags&(Ref.BAD_MASK|Ref.VERIFIED_MASK))!=0; + } + + public void validateSignature() throws BadSignatureException { + if (!checkSignature()) throw new BadSignatureException("Signature not valid!", this); + } + + @Override + public boolean isCanonical() { + return true; + } + + @Override public final boolean isCVMValue() { + return false; + } + + @Override + public int getRefCount() { + // Value Ref only + return 1; + } + + @SuppressWarnings("unchecked") + @Override + public Ref getRef(int i) { + if (i != 0) throw new IndexOutOfBoundsException("Illegal SignedData ref index: " + i); + return (Ref) valueRef; + } + + @Override + public SignedData updateRefs(IRefFunction func) { + @SuppressWarnings("unchecked") + Ref newValueRef = (Ref) func.apply(valueRef); + if (valueRef == newValueRef) return this; + + // SECURITY: preserve verification flags + SignedData newSD= new SignedData(newValueRef, publicKey, signature); + newSD.cachedRef=newSD.getRef().withFlags(getRef().getFlags()); + return newSD; + } + + @Override + public void print(StringBuilder sb) { + sb.append("{"); + sb.append(":signed "+valueRef.getHash().toString()); + sb.append("}"); + } + + @Override + public void validate() throws InvalidDataException { + super.validate(); + } + + @Override + public void validateCell() throws InvalidDataException { + publicKey.validate(); + signature.validate(); + valueRef.validate(); + } + + public Ref getDataRef() { + return valueRef; + } + + /** + * SignedData is not embedded. We want to persist in store always to cache verification status + * + * @return Always false + */ + public boolean isEmbedded() { + return false; + } + + @Override + public String toString() { + return "{:signed "+getValue()+"}"; + } + + @Override + public byte getTag() { + return Tag.SIGNED_DATA; + } + + @Override + public ACell toCanonical() { + return this; + } +} diff --git a/convex-core/src/main/java/convex/core/data/StringShort.java b/convex-core/src/main/java/convex/core/data/StringShort.java new file mode 100644 index 000000000..3623655f3 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/StringShort.java @@ -0,0 +1,163 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; + +import convex.core.exceptions.InvalidDataException; +import convex.core.util.Utils; + +/** + * Class representing a short CVM string. + */ +public class StringShort extends AString { + + /** + * Length of longest StringShort value that is embedded in chars + * + * Just long enough for a 64-char hex string with 0x and 2 delimiters. If that helps. + */ + public static final int MAX_EMBEDDED_STRING_LENGTH=68; + + /** + * Length of longest StringShort value in chars + */ + public static final int MAX_LENGTH=1024; + + + private String data; + + protected StringShort(String data) { + super(data.length()); + this.data=data; + } + + /** + * Creates a StringShort instance from a regular Java String + * + * @param string String to wrap as StringShort + * @return StringShort instance, or null if String is of invalid size + */ + public static StringShort create(String string) { + int len=string.length(); + if ((len<0)||(len>MAX_LENGTH)) return null; + return new StringShort(string); + } + + + @Override + public char charAt(int index) { + return data.charAt(index); + } + + @Override + public StringShort subSequence(int start, int end) { + if ((start<0)||(end>length)) throw new IndexOutOfBoundsException("Out of range subSerqnce "+start+","+end); + if (endMAX_LENGTH) throw new InvalidDataException("StringShort too long: " +length,this); + if (length!=data.length()) throw new InvalidDataException("Wrong String length!",this); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos=Format.writeVLCLong(bs,pos, length); + + int n=data.length(); + for (int i=0; islen)) throw new IllegalArgumentException("Out of range"); + return new StringSlice(source,start,len); + } + + @Override + public char charAt(int index) { + return source.charAt(index-start); + } + + @Override + public AString subSequence(int start, int end) { + int len=end-start; + if (len==0) return Strings.EMPTY; + if (len<0) throw new IllegalArgumentException("Negative length"); + if ((start<0)||(start+len>=length)) throw new IllegalArgumentException("Out of range"); + if ((start==0)&&(len==length)) return this; + return source.subSequence(this.start+start, this.start+end); + } + + @Override + public void validateCell() throws InvalidDataException { + // Nothing? + + } + + @Override + public int encode(byte[] bs, int pos) { + throw new UnsupportedOperationException(""); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + throw new UnsupportedOperationException(""); + } + + @Override + public int estimatedEncodingSize() { + return 100; + } + + @Override + public boolean isCanonical() { + return false; + } + + @Override public final boolean isCVMValue() { + return false; + } + + @Override + public int getRefCount() { + return 0; + } + + + @Override + protected void appendToStringBuffer(StringBuilder sb, int start,int length) { + int sourceStart=this.start+start; + source.appendToStringBuffer(sb, sourceStart, length); + } + + @Override + protected AString append(char charValue) { + StringBuilder sb=new StringBuilder(); + appendToStringBuffer(sb, 0, length); + sb.append(charValue); + return Strings.create(sb.toString()); + } + + @Override + public AString toCanonical() { + return Strings.create(toString()); + } + + +} diff --git a/convex-core/src/main/java/convex/core/data/StringTree.java b/convex-core/src/main/java/convex/core/data/StringTree.java new file mode 100644 index 000000000..a69413c91 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/StringTree.java @@ -0,0 +1,190 @@ +package convex.core.data; + +import java.nio.ByteBuffer; + +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.util.Bits; + +public class StringTree extends AString { + + public static final int MINIMUM_LENGTH=StringShort.MAX_LENGTH+1; + + public static final int BASE_SHIFT = 10; + public static final int BIT_SHIFT_PER_LEVEL = 4; + public static final int FANOUT = 1 << BIT_SHIFT_PER_LEVEL; + + private final Ref[] children; + private final int shift; + + protected StringTree(int length,Ref[] children) { + super(length); + this.children=children; + this.shift=calcShift(length); + } + + protected static int calcShift(int length) { + if (length<=0) throw new IllegalArgumentException("Illegal length: "+length); + + int bitCount=32-Bits.leadingZeros(length-1)-1; + + int shift=BASE_SHIFT+(Math.floorDiv(bitCount-BASE_SHIFT, BIT_SHIFT_PER_LEVEL)*BIT_SHIFT_PER_LEVEL); + if (shift[] children = (Ref[]) new Ref[n]; + for (int i = 0; i < n; i++) { + int start=i*childSize; + AString child=Strings.create(s.substring(start, Math.min(len, start+childSize))); + Ref ref = child.getRef(); + children[i] = ref; + } + return new StringTree(len,children); + } + + private int childIndexAt(int index) { + int ci=index>>shift; + return ci; + } + + @Override + public char charAt(int index) { + int ci=index>>shift; + int cix=index-ci*childSize(); + return children[ci].getValue().charAt(cix); + } + + @Override + public AString subSequence(int start, int end) { + return StringSlice.create(this,start,end-start); + } + + @Override + public void validateCell() throws InvalidDataException { + // TODO Auto-generated method stub + + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos=Format.writeVLCLong(bs,pos, length); + int n=children.length; + for (int i=0; i[] children = (Ref[]) new Ref[n]; + for (int i = 0; i < n; i++) { + Ref ref = Format.readRef(bb); + children[i] = ref; + } + + return new StringTree(length,children); + } + + protected static int calcChildCount(int length, int shift) { + return ((length-1)>>shift)+1; + } + + @Override + public int estimatedEncodingSize() { + // Usually all children will be non-embedded Refs + return 10+Ref.INDIRECT_ENCODING_LENGTH*children.length; + } + + @Override + public boolean isCanonical() { + return true; + } + + @Override public final boolean isCVMValue() { + return true; + } + + @Override + public int getRefCount() { + return children.length; + } + + @SuppressWarnings("unchecked") + @Override + public Ref getRef(int i) { + int ic = children.length; + if (i < 0) throw new IndexOutOfBoundsException("Negative Ref index: " + i); + if (i < ic) return (Ref) children[i]; + throw new IndexOutOfBoundsException("Ref index out of range: " + i); + } + + @SuppressWarnings("unchecked") + @Override + public StringTree updateRefs(IRefFunction func) { + int ic = children.length; + Ref[] newChildren = children; + for (int i = 0; i < ic; i++) { + Ref current = children[i]; + Ref newChild = (Ref) func.apply(current); + + if (newChild!=current) { + if (children==newChildren) newChildren=children.clone(); + newChildren[i] = newChild; + } + } + if (newChildren==children) return this; // no change, safe to return this + return new StringTree(length,newChildren); + } + + @Override + protected void appendToStringBuffer(StringBuilder sb, int start, int length) { + int cstart=childIndexAt(start); + int cend=childIndexAt(start+length-1); + int csize=childSize(); + for (int i=cstart; i<=cend; i++) { + AString child=children[i].getValue(); + + // compute indexes indo child + int c0=Math.max(0, start-i*csize); + int c1=Math.min(child.length, start+length-i*csize); + child.appendToStringBuffer(sb, c0, c1-c0); + } + } + + @Override + protected AString append(char charValue) { + // TODO: SECURITY: needs to be O(1) + StringBuilder sb=new StringBuilder(); + appendToStringBuffer(sb, 0, length); + sb.append(charValue); + return Strings.create(sb.toString()); + } + + @Override + public StringTree toCanonical() { + return this; + } + + +} diff --git a/convex-core/src/main/java/convex/core/data/Strings.java b/convex-core/src/main/java/convex/core/data/Strings.java new file mode 100644 index 000000000..0b1f053be --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/Strings.java @@ -0,0 +1,42 @@ +package convex.core.data; + +import java.nio.ByteBuffer; + +import convex.core.exceptions.BadFormatException; + +public class Strings { + + public static final StringShort EMPTY = StringShort.create(""); + public static final StringShort NIL = StringShort.create("nil"); + + /** + * Reads a CVM String value from a bytebuffer. Assumes tag already read. + * + * @param bb ByteBuffer to read from + * @return String instance + * @throws BadFormatException If format has problems + */ + public static AString read(ByteBuffer bb) throws BadFormatException { + long length=Format.readVLCLong(bb); + if (length==0) return EMPTY; + if (length<0) throw new BadFormatException("Negative string length!"); + if (length>Integer.MAX_VALUE) throw new BadFormatException("String length too long! "+length); + if (length<=StringShort.MAX_LENGTH) { + return StringShort.read((int)length,bb); + } + return StringTree.read((int)length,bb); + } + + public static AString create(String s) { + int len=s.length(); + if (len==0) return EMPTY; + if (len<=StringShort.MAX_LENGTH) { + return StringShort.create(s); + } + return StringTree.create(s); + } + + public static AString empty() { + return EMPTY; + } +} diff --git a/convex-core/src/main/java/convex/core/data/Symbol.java b/convex-core/src/main/java/convex/core/data/Symbol.java new file mode 100644 index 000000000..b46a2c514 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/Symbol.java @@ -0,0 +1,147 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.util.WeakHashMap; + +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; + +/** + *

Class representing a Symbol. Symbols are more commonly used in CVM code to refer to functions and values in the + * execution environment.

+ * + *

Symbols are simply small immutable data Objects, and can be used freely in data structures. They can be used as map + * keys, however for most normal circumstances Strings or Keywords are more appropriate as keys. + *

+ * + *

+ * A Symbol comprises: + * - A name + *

+ * + *

+ * "Becoming sufficiently familiar with something is a substitute for + * understanding it." - John Conway + *

+ */ +public class Symbol extends ASymbolic { + + private Symbol(String name) { + super(name); + } + + public AType getType() { + return Types.SYMBOL; + } + + protected static final WeakHashMap cache=new WeakHashMap<>(100); + + /** + * Creates a Symbol with the given name + * @param name Symbol name + * @return Symbol instance, or null if the Symbol is invalid + */ + public static Symbol create(String name) { + if (!validateName(name)) return null; + Symbol sym= new Symbol(name); + + synchronized (cache) { + // TODO: figure out if caching Symbols is a net win or not + Symbol cached=cache.get(name); + if (cached!=null) return cached; + cache.put(name,sym); + } + + return sym; + } + + /** + * Creates a Symbol with the given name. Must be an unqualified name. + * + * @param name Name for Symbol + * @return Symbol instance, or null if the name is invalid for a Symbol. + */ + public static Symbol create(AString name) { + if (name==null) return null; + return create(name.toString()); + } + + @Override + public boolean equals(ACell o) { + if (o instanceof Symbol) return equals((Symbol) o); + return false; + } + + /** + * Tests if this Symbol is equal to another Symbol. Equality is defined by both namespace and name being equal. + * @param sym Symbol to compare with + * @return true if Symbols are equal, false otherwise + */ + public boolean equals(Symbol sym) { + return sym.name.equals(name); + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.SYMBOL; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = Format.writeRawUTF8String(bs, pos, name.toString()); + return pos; + } + + /** + * Reads a Symbol from the given ByteBuffer, assuming tag already consumed + * + * @param bb ByteBuffer source + * @return The Symbol read + * @throws BadFormatException If a Symbol could not be read correctly. + */ + public static Symbol read(ByteBuffer bb) throws BadFormatException { + String name=Format.readUTF8String(bb); + Symbol sym = Symbol.create(name); + if (sym == null) throw new BadFormatException("Can't read symbol"); + return sym; + } + + @Override + public boolean isCanonical() { + // Always canonical + return true; + } + + @Override + public void print(StringBuilder sb) { + sb.append(getName()); + } + + @Override + public int estimatedEncodingSize() { + return 50; + } + + @Override + public void validateCell() throws InvalidDataException { + super.validateCell(); + } + + @Override + public int getRefCount() { + return 0; + } + + @Override + public byte getTag() { + return Tag.SYMBOL; + } + + @Override + public ACell toCanonical() { + return this; + } +} diff --git a/convex-core/src/main/java/convex/core/data/Syntax.java b/convex-core/src/main/java/convex/core/data/Syntax.java new file mode 100644 index 000000000..4e354628d --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/Syntax.java @@ -0,0 +1,336 @@ +package convex.core.data; + +import java.nio.ByteBuffer; + +import convex.core.data.prim.CVMLong; +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.RT; +import convex.core.util.Utils; + +/** + * Class representing a Syntax Object. + * + * A Syntax Object wraps: + *
    + *
  • A Form (which may contain nested Syntax Objects)
  • + *
  • Metadata for the Syntax Object, which may be any arbitrary hashmap
  • + *
+ * + * Syntax Objects may not wrap another Syntax Object directly, but may contain nested + * Syntax Objects within data structures. + * + * Inspired by Racket. + * + */ +public class Syntax extends ACell { + public static final Syntax EMPTY = create(null, null); + + /** + * Ref to the unwrapped datum value. Cannot refer to another Syntax object + */ + private final Ref datumRef; + + /** + * Metadata map + * If empty, gets encoded as null in byte encoding + */ + private final AHashMap meta; + + private Syntax(Ref datumRef, AHashMap props) { + this.datumRef = datumRef; + this.meta = props; + } + + public AType getType() { + return Types.SYNTAX; + } + + public static Syntax createUnchecked(ACell value, AHashMap meta) { + return new Syntax(Ref.get(value),meta); + } + + /** + * Wraps a value as a Syntax Object, adding the given new metadata + * + * @param value Value to wrap in Syntax Object + * @param meta Metadata to merge, may be null + * @return Syntax instance + */ + public static Syntax create(ACell value, AHashMap meta) { + if (value instanceof Syntax) { + Syntax stx=((Syntax) value); + if (meta==null) return stx; + return stx.mergeMeta(meta); + } + if (meta==null) meta=Maps.empty(); + + return new Syntax(Ref.get(value), meta); + } + + /** + * Wraps a value as a Syntax Object with empty metadata. Does not change existing Syntax objects. + * + * @param value Any CVM value + * @return Syntax instance + */ + public static Syntax create(ACell value) { + if (value instanceof Syntax) return (Syntax) value; + return create(value, Maps.empty()); + } + + /** + * Wraps a value as a Syntax Object with empty metadata. Does not change existing Syntax objects. + * + * @param value Any value, will be converted to valid CVM type + * @return Syntax instance + */ + public static Syntax of(ACell value) { + return create(value); + } + + /** + * Create a Syntax Object with the given value. Converts to appropriate CVM type as a convenience + * + * @param value Value to wrap + * @return Syntax instance + */ + public static Syntax of(Object value) { + return create(RT.cvm(value)); + } + + /** + * Gets the value datum from this Syntax Object + * + * @param Expected datum type from Syntax object + * @return Value datum + */ + @SuppressWarnings("unchecked") + public R getValue() { + return (R) datumRef.getValue(); + } + + /** + * Gets the metadata for this syntax object. May be empty, but never null. + * + * @return Metadata for this Syntax Object as a hashmap + */ + public AHashMap getMeta() { + return meta; + } + + public Long getStart() { + Object v= meta.get(Keywords.START); + if (v instanceof CVMLong) return ((CVMLong)v).longValue(); + return null; + } + + public Long getEnd() { + Object v= meta.get(Keywords.END); + if (v instanceof CVMLong) return ((CVMLong)v).longValue(); + return null; + } + + public String getSource() { + Object v= meta.get(Keywords.SOURCE); + if (v instanceof AString) return v.toString(); + return null; + } + + @Override + public boolean isCanonical() { + return true; + } + + @Override public final boolean isCVMValue() { + return true; + } + + public static Syntax read(ByteBuffer bb) throws BadFormatException { + Ref datum = Format.readRef(bb); + AHashMap props = Format.read(bb); + if (props == null) { + props = Maps.empty(); // we encode empty props as null for efficiency + } else { + if (props.isEmpty()) { + throw new BadFormatException("Empty Syntax metadata should be encoded as nil"); + } + } + return new Syntax(datum, props); + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.SYNTAX; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos=datumRef.encode(bs,pos); + // encode empty props as null for efficiency + if (meta.isEmpty()) { + bs[pos++]=Tag.NULL; + } else { + pos=meta.encode(bs,pos); + } + return pos; + } + + @Override + public void print(StringBuilder sb) { + if (meta==null) { + sb.append("^{} "); + } else { + sb.append('^'); + meta.print(sb); + sb.append(' '); + } + Utils.print(sb, datumRef.getValue()); + } + + @Override + public String toString() { + return print(); + } + + @Override + public void validateCell() throws InvalidDataException { + if (datumRef == null) throw new InvalidDataException("null datum ref", this); + if (meta == null) throw new InvalidDataException("null metadata", this); + meta.validateCell(); + } + + @Override + public void validate() throws InvalidDataException { + super.validate(); + if (datumRef.getValue() instanceof Syntax) { + throw new InvalidDataException("Cannot double-wrap a Syntax value",this); + } + } + + @Override + public int estimatedEncodingSize() { + return 1+2*Format.MAX_EMBEDDED_LENGTH; + } + + @Override + public int getRefCount() { + return 1 + meta.getRefCount(); + } + + @SuppressWarnings("unchecked") + @Override + public Ref getRef(int i) { + if (i == 0) return (Ref) datumRef; + return meta.getRef(i - 1); + } + + + @Override + public Syntax updateRefs(IRefFunction func) { + @SuppressWarnings("unchecked") + Ref newDatum = (Ref)func.apply(datumRef); + AHashMap newMeta = meta.updateRefs(func); + if ((datumRef == newDatum) && (meta == newMeta)) return this; + return new Syntax(newDatum, newMeta); + } + + /** + * Merges metadata into this syntax object, overriding existing metadata + * + * @param additionalMetadata Extra metadata to merge + * @return Syntax Object with updated metadata + */ + public Syntax mergeMeta(AHashMap additionalMetadata) { + AHashMap mm = meta; + mm = mm.merge(additionalMetadata); + return this.withMeta(mm); + } + + /** + * Merge metadata into a Cell, after wrapping as a Syntax Object + * + * @param original Cell to enhance with merged metadata + * @param additional Syntax Object containing additional metadata. Any value will be ignored. + * @return Syntax object with merged metadata + */ + public static Syntax mergeMeta(ACell original, Syntax additional) { + Syntax x=Syntax.create(original); + if (additional!=null) { + x=x.mergeMeta(additional.getMeta()); + } + return x; + } + + /** + * Replaces metadata on this Syntax Object. Old metadata is discarded. + * + * @param newMetadata New metadata map + * @return Syntax Object with updated metadata + */ + public Syntax withMeta(AHashMap newMetadata) { + if (meta == newMetadata) return this; + return new Syntax(datumRef, newMetadata); + } + + /** + * Removes all metadata from this Syntax Object + * + * @return Syntax Object with empty metadata + */ + public Syntax withoutMeta() { + return withMeta(Maps.empty()); + } + + /** + * Unwraps a Syntax Object to get the underlying value. + * + * If the argument is not a Syntax object, return it unchanged (already unwrapped) + * @param Expected type of value + * @param x Any Object, which may be a Syntax Object + * @return The unwrapped value + */ + @SuppressWarnings("unchecked") + public static R unwrap(ACell x) { + return (x instanceof Syntax) ? ((Syntax) x).getValue() : (R) x; + } + + /** + * Recursively unwraps a Syntax object + * + * @param maybeSyntax Syntax Object to unwrap + * @return Unwrapped object + */ + @SuppressWarnings("unchecked") + public static R unwrapAll(ACell maybeSyntax) { + ACell a = unwrap(maybeSyntax); + + if (a instanceof ACollection) { + return (R) ((ACollection) a).map(e -> unwrapAll(e)); + } else if (a instanceof AMap) { + AMap m = (AMap) a; + return (R) m.reduceEntries((acc, e) -> { + return acc.assoc(unwrapAll(e.getKey()), unwrapAll(e.getValue())); + }, (AMap) Maps.empty()); + } else { + // nothing else can contain Syntax objects, so just return normally + return (R) a; + } + } + + @Override + public byte getTag() { + return Tag.SYNTAX; + } + + @Override + public ACell toCanonical() { + return this; + } + + + +} diff --git a/convex-core/src/main/java/convex/core/data/Tag.java b/convex-core/src/main/java/convex/core/data/Tag.java new file mode 100644 index 000000000..fc6a402c2 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/Tag.java @@ -0,0 +1,87 @@ +package convex.core.data; + +/** + * Class containing constant Tag values. + * + * All of this is critical to the wire format and hash calculation. + * + * This is the gospel. The whole truth, and nothing but the truth. + * + * Hack here at your peril. Changes will break every single database, most immutable Value IDs, and probably your heart. + */ +public class Tag { + // Basic Types: Primitive values and numerics + // we might add unsigned primitives at some point? + public static final byte NULL = (byte) 0x00; + public static final byte BYTE = (byte) 0x01; + //public static final byte SHORT = (byte) 0x05; + //public static final byte INT = (byte) 0x07; + public static final byte LONG = (byte) 0x09; + + //public static final byte BIG_INTEGER = (byte) 0x0a; // Arbitrary length integer + public static final byte CHAR = (byte) 0x0c; + public static final byte DOUBLE = (byte) 0x0d; + //public static final byte FLOAT = (byte) 0x0f; + //public static final byte BIG_DECIMAL = (byte) 0x0e; // E notation precise decimal + + // Amounts of tokens + // Note: Amounts use the low 4 bits of the tag for decimal scale factor + //public static final byte AMOUNT = (byte) 0x10; // Financial amount + + // crypto and security primitives + public static final byte REF = (byte) 0x20; + public static final byte ADDRESS = (byte) 0x21; + public static final byte SIGNATURE = (byte) 0x22; + + // Standard supported object data types + public static final byte STRING = (byte) 0x30; + public static final byte BLOB = (byte) 0x31; + public static final byte SYMBOL = (byte) 0x32; + public static final byte KEYWORD = (byte) 0x33; + + // data type tags beyond this point + + // general purpose data structures + public static final byte VECTOR = (byte) 0x80; + public static final byte LIST = (byte) 0x81; + public static final byte MAP = (byte) 0x82; + public static final byte SET = (byte) 0x83; + + public static final byte BLOBMAP = (byte) 0x84; + + public static final byte SYNTAX = (byte) 0x88; + + // special data structure + public static final byte SIGNED_DATA = (byte) 0x90; + + // Record data structures + public static final byte STATE = (byte) 0xA0; + public static final byte BELIEF = (byte) 0xAA; + public static final byte BLOCK = (byte) 0xAB; + public static final byte ORDER = (byte) 0xAC; + public static final byte RESULT = (byte)0xAD; // transaction result + public static final byte BLOCK_RESULT = (byte) 0xAE; + + public static final byte FALSE = (byte) 0xB0; + public static final byte TRUE = (byte) 0xB1; + + // Control structures + public static final byte COMMAND = (byte) 0xC0; + public static final byte ACCOUNT_STATUS = (byte) 0xC1; + public static final byte PEER_STATUS = (byte) 0xC2; + + // Code + public static final byte OP = (byte) 0xCC; + public static final byte CORE_DEF = (byte) 0xCD; + public static final byte FN = (byte) 0xCF; + public static final byte FN_MULTI = (byte) 0xCB; + + // transaction types + public static final byte INVOKE = (byte) 0xD0; + public static final byte TRANSFER = (byte) 0xD1; + public static final byte CALL = (byte) 0xD2; + + // F? Illegal / reserved + public static final byte ILLEGAL = (byte) 0xFF; + +} diff --git a/convex-core/src/main/java/convex/core/data/VectorArray.java b/convex-core/src/main/java/convex/core/data/VectorArray.java new file mode 100644 index 000000000..7ebd5ca6f --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/VectorArray.java @@ -0,0 +1,216 @@ +package convex.core.data; + +import java.util.ListIterator; +import java.util.Spliterator; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import convex.core.exceptions.InvalidDataException; + +/** + * Experimental: implementation of AVector backed by a Java array for temporary usage purposes. + * + * @param Type of vector elements + */ +public class VectorArray extends AVector { + + private final T[] array; + private final int offset; + private final int stride; + + protected VectorArray(long count, T[] array, int offset, int stride) { + super(count); + this.array=array; + this.offset=offset; + this.stride=stride; + } + + @Override + public ListIterator listIterator() { + throw new UnsupportedOperationException(); + } + + @Override + public int estimatedEncodingSize() { + return 100; + } + + @Override + public T get(long i) { + if ((i < 0) || (i >= count)) throw new IndexOutOfBoundsException("Index: " + i); + return array[offset+(int)(i*stride)]; + } + + @Override + public AVector appendChunk(VectorLeaf chunkVector) { + return toVector().appendChunk(chunkVector); + } + + @Override + public VectorLeaf getChunk(long offset) { + return toVector().getChunk(offset); + } + + @Override + public AVector append(T value) { + return toVector().append(value); + } + + @Override + public boolean isPacked() { + // TODO Auto-generated method stub + return false; + } + + + @Override + public boolean anyMatch(Predicate pred) { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean allMatch(Predicate pred) { + // TODO Auto-generated method stub + return false; + } + + @Override + public AVector map(Function mapper) { + // TODO Auto-generated method stub + return null; + } + + @Override + public AVector concat(ASequence b) { + return toVector().concat(b); + } + + @Override + public R reduce(BiFunction func, R value) { + throw new UnsupportedOperationException(); + } + + @Override + public Spliterator spliterator(long position) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ListIterator listIterator(long index) { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isCanonical() { + // Not a canonical vector! + return false; + } + + @Override public final boolean isCVMValue() { + return false; + } + + @Override + public AVector updateRefs(IRefFunction func) { + return toVector().updateRefs(func); + } + + @Override + public long commonPrefixLength(AVector b) { + return toVector().commonPrefixLength(b); + } + + @Override + public AVector next() { + if (count==0) return null; + return slice(1,count-1); + } + + @Override + public AVector assoc(long i, R value) { + return toVector().assoc(i,value); + } + + @Override + public long longIndexOf(Object value) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public long longLastIndexOf(Object value) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public void forEach(Consumer action) { + // TODO Auto-generated method stub + + } + + @Override + public void visitElementRefs(Consumer> f) { + // TODO Auto-generated method stub + + } + + @Override + public Ref getElementRef(long index) { + return Ref.get(get(index)); + } + + @SuppressWarnings("unchecked") + @Override + public VectorArray subVector(long start, long length) { + checkRange(start, length); + if (length == count) return (VectorArray) this; + + return new VectorArray(length,(R[])array,offset+(int)(start*stride),stride); + } + + @Override + public int encode(byte[] bs, int pos) { + return toVector().encode(bs, pos); + } + + @SuppressWarnings("unchecked") + @Override + public AVector toVector() { + if (stride==1) Vectors.create(array, offset, (int)count); + return (AVector) Vectors.create(toCellArray()); + } + + @Override + public void validateCell() throws InvalidDataException { + throw new UnsupportedOperationException(); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + return toVector().encodeRaw(bs, pos); + } + + @Override + public int getRefCount() { + throw new UnsupportedOperationException(); + } + + @Override + public AVector toCanonical() { + // Convert to vector + return toVector(); + } + + @Override + protected void copyToArray(R[] arr, int offset) { + // TODO Auto-generated method stub + + } + +} diff --git a/convex-core/src/main/java/convex/core/data/VectorLeaf.java b/convex-core/src/main/java/convex/core/data/VectorLeaf.java new file mode 100644 index 000000000..8a36a95e8 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/VectorLeaf.java @@ -0,0 +1,763 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.Spliterator; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * A Persistent Vector implementation representing 0-16 elements with a + * packed Vector prefix. + * + * Design goals: + *
    + *
  • Allows fast access to most recently appended items
  • + *
  • O(1) append, equals - O(log n) access, update
  • + *
  • O(log n) comparisons
  • + *
  • Fast computation of common prefix
  • + *
+ * + * Representation in bytes: + * + *
    + *
  • 0x80 - ListVector tag byte
  • + *
  • VLC Long - Length of list. Greater than 16 implies prefix must be + * present. Low 4 bits specify N (0 means 16 in presence of prefix)
  • + *
  • [Ref]*N - N Elements with length
  • + *
  • Ref? - Tail Ref (excluded if not present)
  • + *
+ * + * @param Type of vector elements + */ +public class VectorLeaf extends AVector { + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static final VectorLeaf EMPTY = new VectorLeaf(new Ref[0]); + + public static final Ref> EMPTY_REF = EMPTY.getRef(); + + static { + // Set empty Ref flags as internal embedded constant + EMPTY_REF.setFlags(Ref.INTERNAL_FLAGS); + } + + /** Maximum size of a single ListVector before a tail is required */ + public static final int MAX_SIZE = Vectors.CHUNK_SIZE; + + private final Ref[] items; + private Ref> prefix; + + VectorLeaf(Ref[] items, Ref> prefix, long count) { + super(count); + this.items = items; + this.prefix = prefix; + } + + VectorLeaf(Ref[] items) { + this(items, null, items.length); + } + + /** + * Creates a VectorLeaf with the given items + * + * @param elements Elements to add + * @param offset Offset into element array + * @param length Number of elements to include from array + * @return New ListVector + */ + @SuppressWarnings("unchecked") + public static VectorLeaf create(ACell[] elements, int offset, int length) { + if (length == 0) return (VectorLeaf) VectorLeaf.EMPTY; + if (length > Vectors.CHUNK_SIZE) + throw new IllegalArgumentException("Too many elements for ListVector: " + length); + Ref[] items = new Ref[length]; + for (int i = 0; i < length; i++) { + T value=(T) elements[i + offset]; + items[i] = Ref.get(value); + } + return new VectorLeaf(items); + } + + /** + * Creates a ListVector with the given items appended to the specified tail + * + * @param elements Elements to add + * @param offset Offset into element array + * @param length Number of elements to include from array + * @param prefix Prefix vector to append to + * @return The updated ListVector + */ + @SuppressWarnings("unchecked") + public static VectorLeaf create(ACell[] elements, int offset, int length, AVector prefix) { + if (length == 0) + throw new IllegalArgumentException("ListVector with tail cannot be created with zero head elements"); + if (length > Vectors.CHUNK_SIZE) + throw new IllegalArgumentException("Too many elements for ListVector: " + length); + Ref[] items = new Ref[length]; + for (int i = 0; i < length; i++) { + T value=(T) elements[i + offset]; + items[i] = Ref.get(value); + } + return new VectorLeaf(items, prefix.getRef(), prefix.count() + length); + } + + public static VectorLeaf create(T[] things) { + return create(things, 0, things.length); + } + + @SuppressWarnings("unchecked") + @Override + public final AVector toVector() { + return (AVector) this; + } + + @SuppressWarnings("unchecked") + @Override + public AVector append(T value) { + int localSize = items.length; + if (localSize < Vectors.CHUNK_SIZE) { + // extend storage array + Ref[] newItems = new Ref[localSize + 1]; + System.arraycopy(items, 0, newItems, 0, localSize); + newItems[localSize] = Ref.get(value); + + if (localSize + 1 == Vectors.CHUNK_SIZE) { + // need to extend to TreeVector + VectorLeaf chunk = new VectorLeaf(newItems); + if (!hasPrefix()) return chunk; // exactly one whole chunk + return prefix.getValue().appendChunk(chunk); + } else { + // just grow current ListVector head + return new VectorLeaf(newItems, prefix, count + 1); + } + } else { + // this must be a full single chunk already, so turn this into tail of new + // ListVector + AVector newTail = this; + return new VectorLeaf(new Ref[] { Ref.get(value) }, newTail.getRef(), count + 1); + } + } + + @SuppressWarnings("unchecked") + @Override + public AVector concat(ASequence b) { + // Maybe can optimise? + long aLen = count(); + long bLen = b.count(); + AVector result = (AVector) this; + long i = aLen; + long end = aLen + bLen; + while (i < end) { + if ((i & Vectors.BITMASK) == 0) { + int rn = Utils.checkedInt(Math.min(Vectors.CHUNK_SIZE, end - i)); + if (rn == Vectors.CHUNK_SIZE) { + // we can append a whole chunk + result = result.appendChunk((VectorLeaf) b.subVector(i - aLen, rn)); + i += Vectors.CHUNK_SIZE; + continue; + } + } + // otherwise just append one-by-one + result = result.append(b.get(i - aLen)); + i++; + } + return result; + } + + @Override + public AVector appendChunk(VectorLeaf chunk) { + if (chunk.count != Vectors.CHUNK_SIZE) + throw new IllegalArgumentException("Can't append a chunk of size: " + chunk.count()); + + if (this.count == 0) return chunk; + if (this.hasPrefix()) { + throw new IllegalArgumentException( + "Can't append chunk to a ListVector with a tail (length = " + count + ")"); + } + if (this.count != Vectors.CHUNK_SIZE) + throw new IllegalArgumentException("Can't append chunk to a ListVector of size: " + this.count); + return VectorTree.wrap2(chunk, this); + } + + @Override + public T get(long i) { + if ((i < 0) || (i >= count)) throw new IndexOutOfBoundsException("Index: " + i); + long ix = i - prefixLength(); + if (ix >= 0) { + return items[(int) ix].getValue(); + } else { + return prefix.getValue().get(i); + } + } + + @Override + public Ref getElementRef(long i) { + if ((i < 0) || (i >= count)) throw new IndexOutOfBoundsException("Index: " + i); + long ix = i - prefixLength(); + if (ix >= 0) { + return items[(int) ix]; + } else { + return prefix.getValue().getElementRef(i); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public AVector assoc(long i, R value) { + if ((i < 0) || (i >= count)) return null; + + long ix = i - prefixLength(); + if (ix >= 0) { + R old = (R) items[(int) ix].getValue(); + if (old == value) return (AVector) this; + Ref[] newItems = (Ref[]) items.clone(); + newItems[(int) ix] = Ref.get(value); + return new VectorLeaf(newItems, (Ref)prefix, count); + } else { + AVector tl = prefix.getValue(); + AVector newTail = tl.assoc(i, value); + if (tl == newTail) return (AVector) this; + return new VectorLeaf((Ref[])items, newTail.getRef(), count); + } + } + + /** + * Reads a ListVector from the provided ByteBuffer + * + * Assumes the header byte and count is already read. + * + * @param bb ByteBuffer to read from + * @param count Number of elements + * @return VectorLeaf read from ByteBuffer + * @throws BadFormatException If encoding is invalid + */ + @SuppressWarnings("unchecked") + public static VectorLeaf read(ByteBuffer bb, long count) throws BadFormatException { + if (count < 0) throw new BadFormatException("Negative length"); + if (count == 0) return (VectorLeaf) EMPTY; + boolean prefixPresent = count > MAX_SIZE; + + int n = ((int) count) & 0xF; + if (n == 0) { + if (count > 16) throw new BadFormatException("Vector not valid for size 0 mod 16: " + count); + n = VectorLeaf.MAX_SIZE; // we know this must be true since zero already caught + } + + Ref[] items = (Ref[]) new Ref[n]; + for (int i = 0; i < n; i++) { + Ref ref = Format.readRef(bb); + items[i] = ref; + } + + Ref> tail = null; + if (prefixPresent) { + tail=Format.readRef(bb); + } + + return new VectorLeaf(items, tail, count); + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.VECTOR; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + int ilength = items.length; + boolean hasPrefix = hasPrefix(); + + // count field + pos = Format.writeVLCLong(bs,pos, count); + + for (int i = 0; i < ilength; i++) { + pos= items[i].encode(bs,pos); + } + + if (hasPrefix) { + pos = prefix.encode(bs,pos); + } + return pos; + } + + @Override + public int estimatedEncodingSize() { + // allow space for header of reasonable length + // Estimate 64 bytes per element ref (plus space for tail/ other overhead) + int ESTIMATED_REF_SIZE=70; + return 1 + 9 + ESTIMATED_REF_SIZE * (items.length + 1); + } + + @Override + public long getEncodingLength() { + if (encoding!=null) return encoding.count(); + + // tag and count + long length=1+Format.getVLCLength(count); + int n = items.length; + if (prefix!=null) length+=prefix.getEncodingLength(); + for (int i = 0; i < n; i++) { + length+=items[i].getEncodingLength(); + } + return length; + } + + public static int MAX_ENCODING_SIZE = 1 + Format.MAX_VLC_LONG_LENGTH + Format.MAX_EMBEDDED_LENGTH * (MAX_SIZE+1); + + /** + * Returns true if this ListVector has a prefix AVector. + * + * @return true if this ListVector has a prefix, false otherwise + */ + public boolean hasPrefix() { + return prefix != null; + } + + public VectorLeaf withPrefix(AVector newPrefix) { + if ((newPrefix == null) && !hasPrefix()) return this; + long tc = (newPrefix == null) ? 0L : newPrefix.count(); + return new VectorLeaf(items, (newPrefix == null) ? null : newPrefix.getRef(), tc + items.length); + } + + @Override + public boolean isPacked() { + return (!hasPrefix()) && (items.length == Vectors.CHUNK_SIZE); + } + + @Override + public ListIterator listIterator() { + return listIterator(0); + } + + @Override + public ListIterator listIterator(long index) { + return new ListVectorIterator(index); + } + + /** + * Custom ListIterator for ListVector + */ + private class ListVectorIterator implements ListIterator { + ListIterator prefixIterator; + int pos; + + public ListVectorIterator(long index) { + if (index < 0L) throw new IndexOutOfBoundsException((int)index); + + long tc = prefixLength(); + if (index >= tc) { + // in the list head + if (index > count) throw new IndexOutOfBoundsException((int)index); + pos = (int) (index - tc); + this.prefixIterator = (prefix == null) ? null : prefix.getValue().listIterator(tc); + } else { + // in the prefix + pos = 0; + this.prefixIterator = (prefix == null) ? null : prefix.getValue().listIterator(index); + } + } + + @Override + public boolean hasNext() { + if ((prefixIterator != null) && prefixIterator.hasNext()) return true; + return pos < items.length; + } + + @Override + public T next() { + if (prefixIterator != null) { + if (prefixIterator.hasNext()) return prefixIterator.next(); + } + return items[pos++].getValue(); + } + + @Override + public boolean hasPrevious() { + if (pos > 0) return true; + if (prefixIterator != null) return prefixIterator.hasPrevious(); + return false; + } + + @Override + public T previous() { + if (pos > 0) return items[--pos].getValue(); + + if (prefixIterator != null) return prefixIterator.previous(); + throw new NoSuchElementException(); + } + + @Override + public int nextIndex() { + if ((prefixIterator != null) && prefixIterator.hasNext()) return prefixIterator.nextIndex(); + return Utils.checkedInt(prefixLength() + pos); + } + + @Override + public int previousIndex() { + if (pos > 0) return Utils.checkedInt(prefixLength() + pos - 1); + if (prefixIterator != null) return prefixIterator.previousIndex(); + return -1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public void set(T e) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public void add(T e) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + } + + public long prefixLength() { + return count - items.length; + } + + @SuppressWarnings("unchecked") + @Override + protected void copyToArray(K[] arr, int offset) { + int s = size(); + if (prefix != null) { + prefix.getValue().copyToArray(arr, offset); + } + int ilen = items.length; + for (int i = 0; i < ilen; i++) { + K value = (K) items[i].getValue(); + ; + arr[offset + s - ilen + i] = value; + } + } + + @Override + public long longIndexOf(Object o) { + if (prefix != null) { + long pi = prefix.getValue().longIndexOf(o); + if (pi >= 0L) return pi; + } + for (int i = 0; i < items.length; i++) { + if (Utils.equals(items[i].getValue(), o)) return (count - items.length + i); + } + return -1L; + } + + @Override + public long longLastIndexOf(Object o) { + for (int i = items.length - 1; i >= 0; i--) { + if (Utils.equals(items[i].getValue(), o)) return (count - items.length + i); + } + if (prefix != null) { + long ti = prefix.getValue().longLastIndexOf(o); + if (ti >= 0L) return ti; + } + return -1L; + } + + @Override + public void forEach(Consumer action) { + if (prefix != null) { + prefix.getValue().forEach(action); + + for (Ref r : items) { + action.accept(r.getValue()); + } + } + } + + @Override + public boolean anyMatch(Predicate pred) { + if ((prefix != null) && (prefix.getValue().anyMatch(pred))) return true; + for (Ref r : items) { + if (pred.test(r.getValue())) return true; + } + return false; + } + + @Override + public boolean allMatch(Predicate pred) { + if ((prefix != null) && !(prefix.getValue().allMatch(pred))) return false; + for (Ref r : items) { + if (!pred.test(r.getValue())) return false; + } + return true; + } + + @SuppressWarnings("unchecked") + @Override + public AVector map(Function mapper) { + Ref> newPrefix = (prefix == null) ? null : prefix.getValue().map(mapper).getRef(); + + int ilength = items.length; + Ref[] newItems = (Ref[]) new Ref[ilength]; + for (int i = 0; i < ilength; i++) { + Ref iref=items[i]; + R r = mapper.apply(iref.getValue()); + newItems[i] = Ref.get(r); + } + + return (prefix == null) ? new VectorLeaf(newItems) : new VectorLeaf(newItems, newPrefix, count); + } + + @Override + public void visitElementRefs(Consumer> f) { + if (prefix != null) prefix.getValue().visitElementRefs(f); + for (Ref item : items) { + f.accept(item); + } + } + + @Override + public R reduce(BiFunction func, R value) { + if (prefix != null) value = prefix.getValue().reduce(func, value); + int ilength = items.length; + for (int i = 0; i < ilength; i++) { + value = func.apply(value, items[i].getValue()); + } + return value; + } + + @Override + public Spliterator spliterator(long position) { + return new ListVectorSpliterator(position); + } + + private class ListVectorSpliterator implements Spliterator { + long pos = 0; + + public ListVectorSpliterator(long position) { + if ((position < 0) || (position > count)) + throw new IllegalArgumentException(Errors.illegalPosition(position)); + this.pos = position; + } + + @Override + public boolean tryAdvance(Consumer action) { + if (pos >= count) return false; + action.accept((T) get(pos++)); + return true; + } + + @Override + public Spliterator trySplit() { + long tlength = prefixLength(); + if (pos < tlength) { + pos = tlength; + return prefix.getValue().spliterator(pos); + } + return null; + } + + @Override + public long estimateSize() { + return count; + } + + @Override + public int characteristics() { + return Spliterator.IMMUTABLE | Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED; + } + } + + @Override + public boolean isCanonical() { + return true; + } + + @Override public final boolean isCVMValue() { + return true; + } + + @Override + public int getRefCount() { + return items.length + (hasPrefix() ? 1 : 0); + } + + @SuppressWarnings("unchecked") + @Override + public Ref getRef(int i) { + if (prefix != null) { + if (i==0) return (Ref) prefix; + i--; // Decrement so that i indexes into child array after skipping prefix ref + } + int itemsCount = items.length; + if (i < 0) throw new IndexOutOfBoundsException("Negative Ref index: " + i); + if (i < itemsCount) return (Ref) items[i]; + throw new IndexOutOfBoundsException("Ref index out of range: " + i); + } + + @SuppressWarnings("unchecked") + @Override + public VectorLeaf updateRefs(IRefFunction func) { + Ref newPrefix = (prefix == null) ? null : func.apply(prefix); // do this first for in-order traversal + int ic = items.length; + Ref[] newItems = items; + for (int i = 0; i < ic; i++) { + Ref current = items[i]; + Ref newItem = func.apply(current); + if (newItem!=current) { + if (items==newItems) newItems=items.clone(); + newItems[i] = newItem; + } + } + if ((items==newItems) && (prefix == newPrefix)) return this; // if no change, safe to return this + return new VectorLeaf((Ref[]) newItems, (Ref>) newPrefix, count); + } + + @SuppressWarnings("unchecked") + @Override + public boolean equals(ACell a) { + if (a instanceof VectorLeaf) return equals((VectorLeaf)a); + if (!(a instanceof AVector)) return false; + + // Its a vector, but not canonical? + AVector v=(AVector) a; + if (v.count()!=count) return false; + return a.getEncoding().equals(this.getEncoding()); + } + + public boolean equals(VectorLeaf v) { + if (this == v) return true; + if (this.count != v.count()) return false; + if (!Utils.equals(this.prefix, v.prefix)) return false; + for (int i = 0; i < items.length; i++) { + if (!items[i].equalsValue(v.items[i])) return false; + } + return true; + } + + @Override + public long commonPrefixLength(AVector b) { + long n = count(); + if (this==b) return n; + int il = items.length; + long prefixLength = n - il; + if (prefixLength > 0) { + long prefixMatchLength = prefix.getValue().commonPrefixLength(b); + if (prefixMatchLength < prefixLength) return prefixMatchLength; // matched segment entirely within prefix + } + // must have matched prefixLength at least + long nn = Math.min(n, b.count()) - prefixLength; // number of extra elements to check + if (nn==0) return prefixLength; + VectorLeaf bChunk=b.getChunk(prefixLength); + for (int i = 0; i < nn; i++) { + if (!items[i].equalsValue(bChunk.items[i])) { + return prefixLength + i; + } + } + return prefixLength + nn; + } + + @Override + public VectorLeaf getChunk(long offset) { + if (prefix == null) { + if (offset == 0) return this; + } else { + AVector pre=prefix.getValue(); + long prefixLength=pre.count(); + if (offset AVector subVector(long start, long length) { + checkRange(start, length); + if (length == count) return (AVector) this; + + if (prefix == null) { + int len = Utils.checkedInt(length); + Ref[] newItems; + //if (start==0) { + // can share items if starting from zero index + // newItems=(Ref[]) items; + //} else { + newItems= new Ref[len]; + System.arraycopy(items, Utils.checkedInt(start), newItems, 0, len); + //} + + return new VectorLeaf(newItems, null, length); + } else { + long tc = prefixLength(); + if (start >= tc) { + // range is in tail of vector + return this.withPrefix(null).subVector(start - tc, length); + } + + AVector tv = prefix.getValue(); + if ((start + length) <= tc) { + // Range is entirely in prefix + return tv.subVector(start, length); + } else { + long split = tc - start; + return tv.subVector(start, split).concat(this.withPrefix(null).subVector(0, length - split)); + } + } + } + + @Override + public AVector next() { + if (count <= 1) return null; + return slice(1, count - 1); + } + + @SuppressWarnings("unchecked") + @Override + public void validate() throws InvalidDataException { + // TODO: Needs to ensure children are validated? + super.validate(); + if (prefix != null) { + // if we have a prefix, should be 1..15 elements only + if (count == Vectors.CHUNK_SIZE) { + throw new InvalidDataException("Full ListVector with prefix? This is not right...", this); + } + + if (count == 0) { + throw new InvalidDataException("Empty ListVector with prefix? This is not right...", this); + } + + ACell ccell=prefix.getValue(); + if (!(ccell instanceof AVector)) { + throw new InvalidDataException("Prefix is not a vector", this); + } + + AVector tv = (AVector)ccell; + if (prefixLength() != tv.count()) { + throw new InvalidDataException("Expected prefix length: " + prefixLength() + " but found " + tv.count(), + this); + } + tv.validate(); + } + } + + @Override + public void validateCell() throws InvalidDataException { + if ((count > 0) && (items.length == 0)) throw new InvalidDataException("Should be items present!", this); + if (!isCanonical()) throw new InvalidDataException("Not a canonical ListVector!", this); + } + + @Override + public ACell toCanonical() { + return this; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/VectorTree.java b/convex-core/src/main/java/convex/core/data/VectorTree.java new file mode 100644 index 000000000..b49d8f95a --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/VectorTree.java @@ -0,0 +1,719 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.Spliterator; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * Persistent Vector implemented as a merkle tree of chunks + * + * shift indicates the level of the tree: 4 = 1st level, 8 = second etc. + * + * Invariants: + *
    + *
  • All children except the last must be fully packed
  • + *
  • Each non-terminal leaf chunk must be a tailless VectorLeaf of size 16
  • + *
+ * + * This implies that the entire tree must be a multiple of 16 in size. This is a + * desirable property as we want dense trees in our canonical representation. + * Any extra elements must be stored in a ListVector. + * + * This structure facilitates fast ~O(log(n)) operations for lookup and vector + * element update, and usually O(1) element additions/lookup at end. + * + * "Software gets slower faster than hardware gets faster" + * + * - Niklaus Wirth + * + * @param Type of Vector elements + */ +public class VectorTree extends AVector { + + public static final int MINIMUM_SIZE = 2 * Vectors.CHUNK_SIZE; + private final int shift; // bits in each child block + + private final Ref>[] children; + + private VectorTree(Ref>[] children, long count) { + super(count); + this.shift = computeShift(count); + this.children = children; + } + + /** + * Computes the shift value for a BlockVector of the given count Note: if + * returns zero, count cannot be supported by a valid BlockVector + * + * @param count Number of elements + * @return Shift value + */ + public static int computeShift(long count) { + int shift = 0; + if (count >= (1L << 60)) return 60; + while ((1L << (shift + Vectors.BITS_PER_LEVEL)) < count) { + shift += Vectors.BITS_PER_LEVEL; + } + return shift; + } + + /** + * Gets the index of the start of a given child + * + * @param bpos + * @return + */ + private long childIndex(int childNumber) { + return childNumber * childSize(); + } + + /** + * Compute the size of the child array for a given count + * + * @param count + * @param shift + * @return + */ + static int computeArraySize(long count) { + int shift = computeShift(count); + long bsize = 1L << shift; + return (int) ((count + (bsize - 1)) / bsize); + } + + /** + * Create a TreeVector with the specified elements - things must have at least + * 32 elements (the minimum TreeVector size) - must be a whole multiple of 16 + * elements (complete chunks only) + * + * @param things Elements to include + * @param offset Offset into element array + * @param length Number of elements to include + * @return New TreeVector instance + */ + public static VectorTree create(ACell[] things, int offset, int length) { + if (length < MINIMUM_SIZE) + throw new IllegalArgumentException("Can't create BlockVector with insufficient size: " + length); + if ((length & Vectors.BITMASK) != 0) + throw new IllegalArgumentException("Can't create BlockVector with odd elements: " + length); + int shift = computeShift(length); + + int bSize = 1 << shift; + int bNum = (length + (bSize - 1)) / bSize; // enough blocks + @SuppressWarnings("unchecked") + Ref>[] bs = (Ref>[]) new Ref[bNum]; + for (int i = 0; i < bNum; i++) { + int bLen = Math.min(bSize, length - bSize * i); + bs[i] = Vectors.createChunked(things, offset + i * bSize, bLen).getRef(); + } + VectorTree tv = new VectorTree(bs, length); + return tv; + } + + @Override + public T get(long i) { + if ((i < 0) || (i >= count)) throw new IndexOutOfBoundsException("Index: " + i); + long bSize = 1L << shift; // size of a fully packed block + int b = (int) (i >> shift); + return children[b].getValue().get(i - b * bSize); + } + + @Override + public Ref getElementRef(long i) { + if ((i < 0) || (i >= count)) throw new IndexOutOfBoundsException("Index: " + i); + long bSize = 1L << shift; // size of a fully packed block + int b = (int) (i >> shift); + return children[b].getValue().getElementRef(i - b * bSize); + } + + @SuppressWarnings("unchecked") + @Override + public AVector assoc(long i, R value) { + if ((i < 0) || (i >= count)) return null; + + Ref>[] rchildren=(Ref[])children; + + long bSize = 1L << shift; // size of a fully packed block + int b = (int) (i >> shift); + AVector oc = rchildren[b].getValue(); + AVector nc = oc.assoc(i - (b * bSize), value); + if (oc == nc) return (AVector) this; + + Ref>[] newChildren = rchildren.clone(); + newChildren[b] = nc.getRef(); + return new VectorTree(newChildren, count); + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.VECTOR; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos= Format.writeVLCLong(bs,pos, count); + + int n = children.length; + for (int i = 0; i < n; i++) { + pos = children[i].encode(bs,pos); + } + return pos; + } + + @Override + public long getEncodingLength() { + if (encoding!=null) return encoding.count(); + + // tag and count + long length=1+Format.getVLCLength(count); + int n = children.length; + for (int i = 0; i < n; i++) { + length+=children[i].getEncodingLength(); + } + return length; + } + + @Override + public int estimatedEncodingSize() { + // Allow tag, long count, 80 bytes per child average plus some headroom + return 12 + (64 * (children.length+3)); + } + + public static int MAX_ENCODING_SIZE= 1 + Format.MAX_VLC_LONG_LENGTH + (Format.MAX_EMBEDDED_LENGTH * Vectors.CHUNK_SIZE); + + /** + * Reads a VectorTree from the provided ByteBuffer + * + * Assumes the header byte and count is already read. + * + * @param bb ByteBuffer to read from + * @param count Number of elements + * @return TreeVector instance as read from ByteBuffer + * @throws BadFormatException If encoding is invalid + */ + @SuppressWarnings("unchecked") + public static VectorTree read(ByteBuffer bb, long count) + throws BadFormatException { + if (count < 0) throw new BadFormatException("Negative count?"); + int n = computeArraySize(count); + Ref>[] items = (Ref>[]) new Ref[n]; + for (int i = 0; i < n; i++) { + Ref> ref = Format.readRef(bb); + items[i] = ref; + } + + return new VectorTree(items, count); + } + + @SuppressWarnings("unchecked") + @Override + public VectorTree appendChunk(VectorLeaf b) { + if (b.hasPrefix()) throw new IllegalArgumentException("Can't append a block with a tail"); + if (b.count() != VectorLeaf.MAX_SIZE) + throw new IllegalArgumentException("Invalid block size for append: " + b.count()); + if (isPacked()) { + // full blockvector, so need to elevate to the next level + Ref>[] newBlocks = new Ref[2]; + newBlocks[0] = this.getRef(); + newBlocks[1] = b.getRef(); + return new VectorTree(newBlocks, this.count() + b.count()); + } + + int blength = children.length; + AVector lastBlock = children[blength - 1].getValue(); + if (lastBlock.count() == childSize()) { + // need to extend block array + Ref>[] newBlocks = new Ref[blength + 1]; + System.arraycopy(children, 0, newBlocks, 0, blength); + newBlocks[blength] = b.getRef(); + return new VectorTree(newBlocks, count + Vectors.CHUNK_SIZE); + } else { + // add b into current last block + AVector newLast = lastBlock.appendChunk(b); + Ref>[] newBlocks = new Ref[blength]; + System.arraycopy(children, 0, newBlocks, 0, blength - 1); + newBlocks[blength - 1] = newLast.getRef(); + return new VectorTree(newBlocks, count + Vectors.CHUNK_SIZE); + } + } + + /** + * Get the child size in number of chunks (for all except the last child) + */ + private final long childSize() { + return 1L << (shift); + } + + /** + * Get the child size in number of chunks for a specific child + */ + private static long childSize(long count, int i) { + long n=computeArraySize(count); + if ((i<0)||(i>=n)) throw new IndexOutOfBoundsException("Bad child: "+i); + int shift=computeShift(count); + long cs= 1L< append(T value) { + return new VectorLeaf(new Ref[] { Ref.get(value) }, this.getRef(), count + 1); + } + + @SuppressWarnings("unchecked") + @Override + public AVector concat(ASequence b) { + long bLen = b.count(); + VectorTree result = (VectorTree) this; + long bi = 0; + while (bi < bLen) { + if ((bi + Vectors.CHUNK_SIZE) <= bLen) { + // can append a whole chunk + VectorLeaf chunk = (VectorLeaf) b.subVector(bi, Vectors.CHUNK_SIZE); + result = result.appendChunk(chunk); + bi += Vectors.CHUNK_SIZE; + } else { + // we have less than a chunk left, so final result must be a ListVector with the + // current result as tail + VectorLeaf head = (VectorLeaf) b.subVector(bi, bLen - bi); + return ((VectorLeaf)head).withPrefix(result); + } + } + return result; + } + + /** + * Creates a TreeVector with exactly two chunks + * + * @param + * @param head + * @param tail + * @return + */ + @SuppressWarnings("unchecked") + static VectorTree wrap2(VectorLeaf head, VectorLeaf tail) { + Ref>[] newBlocks = new Ref[2]; + newBlocks[0] = tail.getRef(); + newBlocks[1] = head.getRef(); + return new VectorTree(newBlocks, 2 * Vectors.CHUNK_SIZE); + } + + @Override + protected void copyToArray(K[] arr, int offset) { + for (int i = 0; i < children.length; i++) { + AVector b = children[i].getValue(); + b.copyToArray(arr, offset); + offset += b.count(); + } + } + + @Override + public long longIndexOf(Object o) { + long offset = 0; + for (int i = 0; i < children.length; i++) { + AVector b = children[i].getValue(); + long bpos = b.longIndexOf(o); + if (bpos >= 0) return bpos + offset; + offset += b.count(); + } + return -1; + } + + @Override + public long longLastIndexOf(Object o) { + long offset = count; + for (int i = children.length - 1; i >= 0; i--) { + AVector b = children[i].getValue(); + offset -= b.count(); + long bpos = b.longLastIndexOf(o); + if (bpos >= 0) return bpos + offset; + } + return -1; + } + + @Override + public ListIterator listIterator() { + return new VectorTreeIterator(); + } + + @Override + public ListIterator listIterator(long index) { + return new VectorTreeIterator(index); + } + + private class VectorTreeIterator implements ListIterator { + int bpos; + ListIterator sub; + + public VectorTreeIterator() { + this(0); + } + + public VectorTreeIterator(final long index) { + long ix=index; + if (index < 0L) throw new IndexOutOfBoundsException((int)index); + + bpos = 0; + for (int i = 0; i < children.length; i++) { + AVector b = children[bpos].getValue(); + long bc = b.count(); + if (ix <= bc) { + sub = b.listIterator(ix); + return; + } + ix -= bc; + bpos++; + } + // appears to be beyond the end? + throw new IndexOutOfBoundsException((int)index); + } + + @Override + public boolean hasNext() { + if (sub.hasNext()) return true; + return bpos < children.length - 1; + } + + @Override + public T next() { + if (sub.hasNext()) return sub.next(); + if (bpos < children.length - 1) { + bpos++; + sub = children[bpos].getValue().listIterator(); + return sub.next(); + } + ; + throw new NoSuchElementException(); + } + + @Override + public boolean hasPrevious() { + if (sub.hasPrevious()) return true; + return bpos > 0; + } + + @Override + public T previous() { + if (sub.hasPrevious()) return sub.previous(); + if (bpos > 0) { + bpos = bpos - 1; + AVector b = children[bpos].getValue(); + sub = b.listIterator(b.count()); + return sub.previous(); + } + throw new NoSuchElementException(); + } + + @Override + public int nextIndex() { + return Utils.checkedInt(childIndex(bpos) + sub.nextIndex()); + } + + @Override + public int previousIndex() { + return Utils.checkedInt(childIndex(bpos) + sub.previousIndex()); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public void set(T e) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + @Override + public void add(T e) { + throw new UnsupportedOperationException(Errors.immutable(this)); + } + + } + + @Override + public void forEach(Consumer action) { + for (Ref> r : children) { + r.getValue().forEach(action); + } + } + + @Override + public boolean anyMatch(Predicate pred) { + for (Ref> r : children) { + if (r.getValue().anyMatch(pred)) return true; + } + return false; + } + + @Override + public boolean allMatch(Predicate pred) { + for (Ref> r : children) { + if (!r.getValue().allMatch(pred)) return false; + } + return true; + } + + @SuppressWarnings("unchecked") + @Override + public AVector map(Function mapper) { + int blength = children.length; + Ref>[] newBlocks = (Ref>[]) new Ref[blength]; + for (int i = 0; i < blength; i++) { + AVector r = children[i].getValue().map(mapper); + newBlocks[i] = r.getRef(); + } + return new VectorTree(newBlocks, count); + } + + @Override + public void visitElementRefs(Consumer> f) { + for (Ref> item : children) { + item.getValue().visitElementRefs(f); + } + } + + @Override + public R reduce(BiFunction func, R value) { + int blength = children.length; + for (int i = 0; i < blength; i++) { + value = children[i].getValue().reduce(func, value); + } + return value; + } + + @Override + public Spliterator spliterator(long position) { + return new TreeVectorSpliterator(position); + } + + private class TreeVectorSpliterator implements Spliterator { + long pos = 0; + + public TreeVectorSpliterator(long position) { + if ((position < 0) || (position > count)) + throw new IllegalArgumentException(Errors.illegalPosition(position)); + this.pos = position; + } + + @Override + public boolean tryAdvance(Consumer action) { + if (pos >= count) return false; + action.accept((T) get(pos++)); + return true; + } + + @Override + public Spliterator trySplit() { + for (int i = 0; i < children.length; i++) { + long bpos = childIndex(i); + AVector b = children[i].getValue(); + + long bcount = b.count(); + long blockEnd = childIndex(i) + bcount; + if (pos < blockEnd) { + Spliterator ss = b.spliterator(pos - bpos); + pos = blockEnd; + return ss; + } + } + return null; + } + + @Override + public long estimateSize() { + return count; + } + + @Override + public int characteristics() { + return Spliterator.IMMUTABLE | Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED; + } + } + + @Override + public boolean isCanonical() { + if (count < MINIMUM_SIZE) return false; + return true; + } + + @Override public final boolean isCVMValue() { + return true; + } + + @SuppressWarnings("unchecked") + @Override + public final AVector toVector() { + assert (isCanonical()); + return (AVector) this; + } + + @Override + public int getRefCount() { + return children.length; + } + + @SuppressWarnings("unchecked") + @Override + public Ref getRef(int i) { + int ic = children.length; + if (i < 0) throw new IndexOutOfBoundsException("Negative Ref index: " + i); + if (i < ic) return (Ref) children[i]; + throw new IndexOutOfBoundsException("Ref index out of range: " + i); + } + + @SuppressWarnings("unchecked") + @Override + public VectorTree updateRefs(IRefFunction func) { + int ic = children.length; + Ref>[] newChildren = children; + for (int i = 0; i < ic; i++) { + Ref> current = children[i]; + Ref> newChild = (Ref>) func.apply(current); + + if (newChild!=current) { + if (children==newChildren) newChildren=children.clone(); + newChildren[i] = newChild; + } + } + if (newChildren==children) return this; // no change, safe to return this + return new VectorTree<>(newChildren, count); + } + + @Override + public long commonPrefixLength(AVector b) { + if (b instanceof VectorTree) return commonPrefixLength((VectorTree) b); + return b.commonPrefixLength(this); // Handle MapEntry and ListVectors + } + + private long commonPrefixLength(VectorTree b) { + if (this.equals(b)) return count; + + long cs = childSize(); + long bcs = b.childSize(); + if (cs == bcs) { + return commonPrefixLengthAligned(b); + } else if (cs < bcs) { + // b is longer + AVector bChild = b.children[0].getValue(); + return commonPrefixLength(bChild); + } else { + // this is longer + AVector child = children[0].getValue(); + return child.commonPrefixLength(b); + } + } + + // compute common prefix length assuming TreeVectors are aligned (same child + // size) + private long commonPrefixLengthAligned(VectorTree b) { + // check if we have the same stored hash. If so, quick exit! + Hash thisHash = cachedHash(); + if (thisHash != null) { + Hash bHash = b.cachedHash(); + if ((bHash != null) && (thisHash.equals(bHash))) return count; + } + + int n = Math.min(children.length, b.children.length); + long cs = childSize(); + long result = 0; + for (int i = 0; i < n; i++) { + long cpl = children[i].getValue().commonPrefixLength(b.children[i].getValue()); + if (cpl < cs) return result + cpl; + result += cs; // we have validated cs elements as equal + } + return result; + } + + @Override + public VectorLeaf getChunk(long offset) { + long cs = childSize(); + int ix = (int) (offset / cs); + AVector child = children[ix].getValue(); + long cOffset = offset - (ix * cs); + if (cs == VectorLeaf.MAX_SIZE) { + if (cOffset != 0) throw new IndexOutOfBoundsException("Index: " + offset); + return (VectorLeaf) child; + } + return child.getChunk(cOffset); + } + + @SuppressWarnings("unchecked") + @Override + public AVector subVector(long start, long length) { + checkRange(start, length); + if ((start & Vectors.BITMASK) == 0) { + // TODO: this can be fast! + } + ACell[] arr = new ACell[Utils.checkedInt(length)]; + for (int i = 0; i < length; i++) { + arr[i] = get(start + i); + } + return (AVector) Vectors.create(arr); + } + + @Override + public AVector next() { + return slice(1L, count - 1); + } + + @Override + public void validate() throws InvalidDataException { + super.validate(); + long c = 0; + int blen = children.length; + if (blen < 2) throw new InvalidDataException("Insufficient children: " + blen, this); + long bsize = childSize(); + for (int i = 0; i < blen; i++) { + ACell ch = children[i].getValue(); + if (!(ch instanceof AVector)) throw new InvalidDataException("Child "+i+" is not a vector!",this); + @SuppressWarnings("unchecked") + AVector b=(AVector)ch; + + b.validate(); + long expectedChildSize=childSize(count,i); + if (expectedChildSize != b.count()) { + throw new InvalidDataException("Expected block size: " + bsize + " for blocks[" + i + "] but was: " + + b.count() + " in BlockVector of size: " + count, this); + } + + c += b.count(); + } + if (c != count) { + throw new InvalidDataException("Expected count: " + count + " but sum of child sizes was: " + c, this); + } + } + + @Override + public void validateCell() throws InvalidDataException { + int blen = children.length; + if (count < blen) throw new InvalidDataException("Implausible low count: " + count, this); + + if (blen < 2) throw new InvalidDataException("Insufficient children: " + blen, this); + + } + + @Override + public ACell toCanonical() { + // TODO Should be always true? + return this; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/Vectors.java b/convex-core/src/main/java/convex/core/data/Vectors.java new file mode 100644 index 000000000..d2abe8056 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/Vectors.java @@ -0,0 +1,134 @@ +package convex.core.data; + +import java.nio.ByteBuffer; +import java.util.Collection; + +import org.bouncycastle.util.Arrays; + +import convex.core.exceptions.BadFormatException; +import convex.core.lang.RT; +import convex.core.util.Utils; + +public class Vectors { + + protected static final int BITS_PER_LEVEL = 4; + protected static final int CHUNK_SIZE = 1 << BITS_PER_LEVEL; // 16 + protected static final int BITMASK = CHUNK_SIZE - 1; // 15 + + /** + * Creates a canonical AVector with the given elements + * + * @param elements Elements to include + * @param offset Offset into element array + * @param length Number of elements to take + * @return New vector with the specified elements + */ + public static AVector create(ACell[] elements, int offset, int length) { + if (length < 0) throw new IllegalArgumentException("Cannot create vector of negative length!"); + if (length <= CHUNK_SIZE) return VectorLeaf.create(elements, offset, length); + int tailLength = Utils.checkedInt((length >> BITS_PER_LEVEL) << BITS_PER_LEVEL); + AVector tail = Vectors.createChunked(elements, offset, tailLength); + if (tail.count() == length) return tail; + return VectorLeaf.create(elements, offset + tailLength, length - tailLength, tail); + } + + /** + * Create a canonical vector using blocks. Suitable for a ListVector tail. + * + * @param elements + * @param offset + * @param length + * @return A vector, which must consist of a positive number of complete chunks. + */ + static AVector createChunked(ACell[] elements, int offset, int length) { + if ((length == 0) || (length & BITMASK) != 0) + throw new IllegalArgumentException("Invalid vector length: " + length); + if (length == CHUNK_SIZE) return VectorLeaf.create(elements, offset, length); + return VectorTree.create(elements, offset, length); + } + + /** + * Create a vector from an array of elements. + * + * @param Type of elements + * @param elements Elements to include + * @return New vector with the specified elements + */ + public static AVector create(ACell[] elements) { + return create(elements, 0, elements.length); + } + + /** + * Coerces a collection to a vector. Not necessarily the most efficient. + * Performs an unchecked cast. + * + * @param Type of Vector elements to produce + * @param Type of source collection elements + * @param elements Elements to include + * @return New vector with the specified collection of elements + */ + @SuppressWarnings("unchecked") + public static AVector create(Collection elements) { + if (elements instanceof ASequence) return create((ASequence) elements); + if (elements.size() == 0) return empty(); + ACell[] cells=Utils.toCellArray(elements.toArray()); + return (AVector) create(cells); + } + + public static AVector create(ASequence list) { + if (list instanceof AVector) return (AVector) list; + if (list.size() == 0) return empty(); + return create(list.toCellArray()); + } + + + @SuppressWarnings("unchecked") + public static AVector empty() { + return (AVector) VectorLeaf.EMPTY; + } + + /** + * Creates a vector with the given values. Performs conversion to CVM types. + * @param Type of elements (after CVM conversion) + * @param elements Elements to include + * @return New Vector + */ + @SuppressWarnings("unchecked") + @SafeVarargs + public static AVector of(Object... elements) { + int n=elements.length; + ACell[] es= new ACell[n]; + for (int i=0; i AVector repeat(T m, int count) { + ACell[] obs = new ACell[count]; + Arrays.fill(obs, m); + return (AVector) create(obs); + } + + /** + * Reads a Vector for the specified bytebuffer. Assumes Tag byte already consumed. + * + * Distinguishes between child types according to count. + * + * @param Type of elements + * @param bb ByteBuffer to read from + * @return Vector read from ByteBuffer + * @throws BadFormatException If encoding is invalid + */ + public static AVector read(ByteBuffer bb) throws BadFormatException { + long count = Format.readVLCLong(bb); + if ((count <= VectorLeaf.MAX_SIZE) || ((count & 0x0F) != 0)) { + return VectorLeaf.read(bb, count); + } else { + return VectorTree.read(bb, count); + } + } + +} diff --git a/convex-core/src/main/java/convex/core/data/package-info.java b/convex-core/src/main/java/convex/core/data/package-info.java new file mode 100644 index 000000000..d5d3b05c5 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/package-info.java @@ -0,0 +1,5 @@ +/** + * Data structures and algorithms, including a complete set of classes + * required to implement immutable, decentralised data objects. + */ +package convex.core.data; \ No newline at end of file diff --git a/convex-core/src/main/java/convex/core/data/prim/APrimitive.java b/convex-core/src/main/java/convex/core/data/prim/APrimitive.java new file mode 100644 index 000000000..e29caa9de --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/prim/APrimitive.java @@ -0,0 +1,66 @@ +package convex.core.data.prim; + +import convex.core.data.ACell; +import convex.core.data.Ref; +import convex.core.data.RefDirect; + +/** + * Abstract base class for small CVM primitive values. + * + * Primitives never contain Refs, are always embedded, and are always canonical + */ +public abstract class APrimitive extends ACell { + @Override + public final boolean isCanonical() { + return true; + } + + @Override + @SuppressWarnings("unchecked") + protected Ref createRef() { + // Create Ref at maximum status to reflect internal embedded nature + Ref newRef= RefDirect.create(this,cachedHash(),Ref.INTERNAL_FLAGS); + cachedRef=newRef; + return (Ref) newRef; + } + + @Override + public final int getRefCount() { + return 0; + } + + @Override + public final boolean isEmbedded() { + return true; + } + + @Override public final boolean isCVMValue() { + return true; + } + + @Override + protected long calcMemorySize() { + // always embedded and no child Refs, so memory size == 0 + return 0; + } + + /** + * @return long value representing primitive + */ + public abstract long longValue(); + + + /** + * @return double value representing primitive + */ + public abstract double doubleValue(); + + @Override + public ACell toCanonical() { + // Always canonical, probably? + return this; + } + + + +} diff --git a/convex-core/src/main/java/convex/core/data/prim/CVMBool.java b/convex-core/src/main/java/convex/core/data/prim/CVMBool.java new file mode 100644 index 000000000..df41c26ff --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/prim/CVMBool.java @@ -0,0 +1,101 @@ +package convex.core.data.prim; + +import convex.core.data.ACell; +import convex.core.data.Tag; +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.InvalidDataException; + +/** + * Class for CVM Boolean types. + * + * Two canonical values are provided, TRUE and FALSE. No other instances should exist. + */ +public final class CVMBool extends APrimitive { + + private final boolean value; + + public static final CVMBool TRUE=new CVMBool(true); + public static final CVMBool FALSE=new CVMBool(false); + + private CVMBool(boolean value) { + this.value=value; + } + + @Override + public AType getType() { + return Types.BOOLEAN; + } + + + public static CVMBool create(boolean value) { + return value?TRUE:FALSE; + } + + /** + * Get the canonical CVMBool value for true or false + * + * @param b Boolean specifying + * @return CVMBool value representing false or true + */ + public static CVMBool of(boolean b) { + return b?TRUE:FALSE; + } + + + @Override + public long longValue() { + return value?1:0; + } + + @Override + public int estimatedEncodingSize() { + return 1; + } + + @Override + public void validateCell() throws InvalidDataException { + // Nothing to check. Always valid + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=value?Tag.TRUE:Tag.FALSE; + return pos; + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + throw new UnsupportedOperationException("Not meaningful to encode raw data for CVMBool"); + } + + @Override + public void print(StringBuilder sb) { + sb.append(value?"true":"false"); + } + + @Override + public double doubleValue() { + return value?1:0; + } + + public boolean booleanValue() { + return value; + } + + @Override + public byte getTag() { + return (value)?Tag.TRUE:Tag.FALSE; + } + + public static ACell parse(String text) { + if ("true".equals(text)) return TRUE; + if ("false".equals(text)) return FALSE; + return null; + } + + + + + +} diff --git a/convex-core/src/main/java/convex/core/data/prim/CVMByte.java b/convex-core/src/main/java/convex/core/data/prim/CVMByte.java new file mode 100644 index 000000000..d7ed8397a --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/prim/CVMByte.java @@ -0,0 +1,117 @@ +package convex.core.data.prim; + +import convex.core.data.INumeric; +import convex.core.data.Tag; +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.InvalidDataException; + +/** + * Class for CVM Byte instances. + * + * Bytes are unsigned 8-bit integers which upcast to long for numerical operations. + * + */ +public final class CVMByte extends APrimitive implements INumeric { + + private final byte value; + + private static final CVMByte[] CACHE= new CVMByte[256]; + + public static final CVMByte ZERO; + public static final CVMByte ONE; + + // Private constructor to enforce singleton instances + private CVMByte(byte value) { + this.value=value; + } + + public static CVMByte create(long value) { + return CACHE[((int)(value))&0xFF]; + } + + static { + for (int i=0; i<256; i++) { + CACHE[i]=new CVMByte((byte)i); + } + ZERO=CACHE[0]; + ONE=CACHE[1]; + } + + public AType getType() { + return Types.BYTE; + } + + @Override + public long longValue() { + return 0xFFL&value; + } + + @Override + public int estimatedEncodingSize() { + return 2; + } + + @Override + public void validateCell() throws InvalidDataException { + // Nothing to check. Always valid + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.BYTE; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + bs[pos++]=value; + return pos; + } + + @Override + public void print(StringBuilder sb) { + sb.append(longValue()); + } + + @Override + public Class numericType() { + return Long.class; + } + + @Override + public double doubleValue() { + return (double)longValue(); + } + + @Override + public byte getTag() { + return Tag.BYTE; + } + + @Override + public CVMLong toLong() { + return CVMLong.create(longValue()); + } + + @Override + public CVMDouble toDouble() { + return CVMDouble.create(doubleValue()); + } + + @Override + public CVMLong signum() { + if (value==0) return CVMLong.ZERO; + return CVMLong.ONE; + } + + public byte byteValue() { + return value; + } + + @Override + public INumeric toStandardNumber() { + return toLong(); + } + +} diff --git a/convex-core/src/main/java/convex/core/data/prim/CVMChar.java b/convex-core/src/main/java/convex/core/data/prim/CVMChar.java new file mode 100644 index 000000000..23f68a1d9 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/prim/CVMChar.java @@ -0,0 +1,132 @@ +package convex.core.data.prim; + +import convex.core.data.Tag; +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.reader.ReaderUtils; +import convex.core.util.Utils; + +/** + * Class for CVM character values. + * + * Chars are 16-bit UTF-16 unsigned integers, and are the elements of Strings CVM. + */ +public final class CVMChar extends APrimitive { + + public static final CVMChar A = CVMChar.create('a'); + + private final char value; + + public CVMChar(char value) { + this.value=value; + } + + @Override + public AType getType() { + return Types.CHARACTER; + } + + + public static CVMChar create(long value) { + return new CVMChar((char)value); + } + + @Override + public long longValue() { + return value; + } + + @Override + public int estimatedEncodingSize() { + return 1+2; + } + + @Override + public void validateCell() throws InvalidDataException { + // Nothing to check. Always valid + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.CHAR; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + return Utils.writeChar(bs,pos,((char)value)); + } + + @Override + public void print(StringBuilder sb) { + // Prints like EDN. + // Characters are preceded by a backslash: \c, \newline, \return, \space and + // \tab yield + // the corresponding characters. + // Unicode characters are represented as in Java. + // Backslash cannot be followed by whitespace. + // + String s; + switch(value) { + case '\n': s = "\\newline"; break; + case '\r': s = "\\return"; break; + case ' ': s = "\\space"; break; + case '\t': s = "\\tab"; break; + default: s = "\\" + Character.toString(value); + } + sb.append(s); + } + + /** + * Returns the String representation of this CVMChar. + * + * Different from {@link #print() print()} which returns a readable representation. + * + * For instance, on CVMChar \a, this methods returns "a" while {@link #print() print()} returns "\a". + */ + @Override + public String toString() { + // Usually, primitive types are stringified using `print`. This method + return Character.toString(value); + } + + @Override + public double doubleValue() { + return (double)value; + } + + /** + * Parses a Character from a String + * @param s String to parse + * @return CVMChar instance, or null if not valid + */ + public static CVMChar parse(String s) { + int n=s.length(); + + if (n<2) return null; + + if (n==2) { + return CVMChar.create(s.charAt(1)); + } + + if (s.charAt(1)=='u') { + if (n==6) { + char c = (char) Long.parseLong(s.substring(2),16); + return CVMChar.create(c); + } + } + + s=s.substring(1); + return ReaderUtils.specialCharacter(s); + } + + public char charValue() { + return value; + } + + @Override + public byte getTag() { + return Tag.CHAR; + } +} diff --git a/convex-core/src/main/java/convex/core/data/prim/CVMDouble.java b/convex-core/src/main/java/convex/core/data/prim/CVMDouble.java new file mode 100644 index 000000000..49323dea8 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/prim/CVMDouble.java @@ -0,0 +1,130 @@ +package convex.core.data.prim; + +import convex.core.data.INumeric; +import convex.core.data.Tag; +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.InvalidDataException; +import convex.core.util.Utils; + +/** + * Class for CVM double floating-point values. + * + * Follows the Java standard / IEEE 784 spec. + */ +public final class CVMDouble extends APrimitive implements INumeric { + + public static final CVMDouble ZERO = CVMDouble.create(0.0); + public static final CVMDouble NEGATIVE_ZERO = CVMDouble.create(-0.0); + public static final CVMDouble ONE = CVMDouble.create(1.0); + public static final CVMDouble MINUS_ONE = CVMDouble.create(-1.0); + + public static final CVMDouble NaN = CVMDouble.create(Double.NaN); + public static final CVMDouble POSITIVE_INFINITY = CVMDouble.create(Double.POSITIVE_INFINITY); + public static final CVMDouble NEGATIVE_INFINITY = CVMDouble.create(Double.NEGATIVE_INFINITY); + + private final double value; + + public CVMDouble(double value) { + this.value=value; + } + + public static CVMDouble create(double value) { + return new CVMDouble(value); + } + + @Override + public AType getType() { + return Types.DOUBLE; + } + + @Override + public long longValue() { + return (long)value; + } + + @Override + public CVMLong toLong() { + return CVMLong.create(longValue()); + } + + @Override + public CVMDouble toDouble() { + return this; + } + + @Override + public CVMDouble signum() { + if (value>0.0) return CVMDouble.ONE; + if (value<0.0) return CVMDouble.MINUS_ONE; + if (Double.isNaN(value)) return NaN; // NaN special case + return this; + } + + @Override + public int estimatedEncodingSize() { + return 1+8; + } + + @Override + public void validateCell() throws InvalidDataException { + // Nothing to check. Always valid + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.DOUBLE; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + long doubleBits=Double.doubleToRawLongBits(value); + return Utils.writeLong(bs,pos,doubleBits); + } + + @Override + public String toString() { + if (Double.isInfinite(value)) { + if (value>0.0) { + return "##Inf"; + } else { + return "##-Inf"; + } + } else if (Double.isNaN(value)) { + return "##NaN"; + } else { + return Double.toString(value); + } + } + + @Override + public void print(StringBuilder sb) { + sb.append(toString()); + } + + @Override + public Class numericType() { + return Double.class; + } + + @Override + public double doubleValue() { + return value; + } + + public static CVMDouble parse(String s) { + return create(Double.parseDouble(s)); + } + + @Override + public byte getTag() { + return Tag.DOUBLE; + } + + @Override + public INumeric toStandardNumber() { + return this; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/prim/CVMLong.java b/convex-core/src/main/java/convex/core/data/prim/CVMLong.java new file mode 100644 index 000000000..7d400dc5f --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/prim/CVMLong.java @@ -0,0 +1,129 @@ +package convex.core.data.prim; + +import convex.core.data.Format; +import convex.core.data.INumeric; +import convex.core.data.Tag; +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.InvalidDataException; + +/** + * Class for CVM long values. + * + * Longs are signed 64-bit integers, and are the primary fixed point integer type on the CVM. + */ +public final class CVMLong extends APrimitive implements INumeric { + + private static final int CACHE_SIZE = 256; + private static final CVMLong[] CACHE= new CVMLong[CACHE_SIZE]; + + static { + for (int i=0; i<256; i++) { + CACHE[i]=new CVMLong(i); + } + ZERO=CACHE[0]; + ONE=CACHE[1]; + } + + public static final CVMLong ZERO; + public static final CVMLong ONE; + public static final CVMLong MINUS_ONE = CVMLong.create(-1L); + public static final CVMLong MAX_VALUE = CVMLong.create(Long.MAX_VALUE); + public static final CVMLong MIN_VALUE = CVMLong.create(Long.MIN_VALUE); + + private final long value; + + public CVMLong(long value) { + this.value=value; + } + + public static CVMLong create(long value) { + if ((value=0)) { + return CACHE[(int)value]; + } + return new CVMLong(value); + } + + @Override + public AType getType() { + return Types.LONG; + } + + @Override + public long longValue() { + return value; + } + + @Override + public CVMLong toLong() { + return this; + } + + @Override + public CVMDouble toDouble() { + return CVMDouble.create(doubleValue()); + } + + @Override + public int estimatedEncodingSize() { + return 1+Format.MAX_VLC_LONG_LENGTH; + } + + @Override + public void validateCell() throws InvalidDataException { + // Nothing to check. Always valid + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.LONG; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + return Format.writeVLCLong(bs, pos, value); + } + + @Override + public void print(StringBuilder sb) { + sb.append(value); + } + + @Override + public Class numericType() { + return Long.class; + } + + @Override + public double doubleValue() { + return (double)value; + } + + /** + * Parse a String as a CVM Long. Throws an exception if the string is not valid + * @param s String to parse + * @return CVM Long value + */ + public static CVMLong parse(String s) { + return create(Long.parseLong(s)); + } + + @Override + public byte getTag() { + return Tag.LONG; + } + + @Override + public CVMLong signum() { + if (value>0) return CVMLong.ONE; + if (value<0) return CVMLong.MINUS_ONE; + return CVMLong.ZERO; + } + + @Override + public INumeric toStandardNumber() { + return this; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/type/ANumericType.java b/convex-core/src/main/java/convex/core/data/type/ANumericType.java new file mode 100644 index 000000000..64023937c --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/ANumericType.java @@ -0,0 +1,11 @@ +package convex.core.data.type; + +import convex.core.data.prim.APrimitive; + +public abstract class ANumericType extends AStandardType { + + protected ANumericType(Class klass) { + super(klass); + } + +} diff --git a/convex-core/src/main/java/convex/core/data/type/AStandardType.java b/convex-core/src/main/java/convex/core/data/type/AStandardType.java new file mode 100644 index 000000000..1e6e679f2 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/AStandardType.java @@ -0,0 +1,42 @@ +package convex.core.data.type; + +import convex.core.data.ACell; + +/** + * Base type for standard types mapped directly to a branch of ACell hierarchy + * @param Java Type (common superclass) + */ +public abstract class AStandardType extends AType { + + Class klass; + + protected AStandardType(Class klass) { + this.klass=klass; + } + + @Override + public boolean check(ACell value) { + return klass.isInstance(value); + } + + @Override + public final boolean allowsNull() { + return false; + } + + @Override + public abstract T defaultValue(); + + @SuppressWarnings("unchecked") + @Override + public T implicitCast(ACell a) { + if (check(a)) return (T)a; + return null; + } + + @Override + public final Class getJavaClass() { + return klass; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/type/AType.java b/convex-core/src/main/java/convex/core/data/type/AType.java new file mode 100644 index 000000000..fab808398 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/AType.java @@ -0,0 +1,46 @@ +package convex.core.data.type; + +import convex.core.data.ACell; + +/** + * Abstract base class for CVM value types + */ +public abstract class AType { + + /** + * Checks if a value is an instance of this Type. + * @param value Any CVM value + * @return true if value is an instance of this Type, false otherwise + */ + public abstract boolean check(ACell value); + + /** + * Checks if this type allows a null value. + * + * @return True if this type allows null values, false otherwise + */ + public abstract boolean allowsNull(); + + @Override + public abstract String toString(); + + /** + * Gets the default value for this type. May return null. + * @return Default value for the given Type + */ + public abstract ACell defaultValue(); + + /** + * Gets the default value for this type. Returns null if the cast fails. + * @param a Value to cast + * @return Value cast to this Type, or null if the cast fails + */ + public abstract ACell implicitCast(ACell a); + + /** + * Gets the Java common base class for all instances of this type. + * + * @return Java Class representing this Type + */ + public abstract Class getJavaClass(); +} diff --git a/convex-core/src/main/java/convex/core/data/type/AddressType.java b/convex-core/src/main/java/convex/core/data/type/AddressType.java new file mode 100644 index 000000000..727ece567 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/AddressType.java @@ -0,0 +1,39 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.Address; + +/** + * Type that represents CVM Byte values + */ +public final class AddressType extends AStandardType
{ + /** + * Singleton runtime instance + */ + public static final AddressType INSTANCE = new AddressType(); + + private AddressType() { + super (Address.class); + } + + @Override + public boolean check(ACell value) { + return value instanceof Address; + } + + @Override + public String toString () { + return "Address"; + } + + @Override + public Address defaultValue() { + return Address.ZERO; + } + + @Override + public Address implicitCast(ACell a) { + if (a instanceof Address) return (Address)a; + return null; + } +} diff --git a/convex-core/src/main/java/convex/core/data/type/Any.java b/convex-core/src/main/java/convex/core/data/type/Any.java new file mode 100644 index 000000000..f684f5e4b --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Any.java @@ -0,0 +1,46 @@ +package convex.core.data.type; + +import convex.core.data.ACell; + +/** + * Type that represents any CVM value + */ +public class Any extends AType { + + public static final Any INSTANCE = new Any(); + + private Any() { + + } + + @Override + public boolean check(ACell value) { + return true; + } + + @Override + public boolean allowsNull() { + return true; + } + + @Override + public String toString() { + return "Any"; + } + + @Override + public ACell defaultValue() { + return null; + } + + @Override + public ACell implicitCast(ACell a) { + return a; + } + + @Override + public Class getJavaClass() { + return ACell.class; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/type/Blob.java b/convex-core/src/main/java/convex/core/data/type/Blob.java new file mode 100644 index 000000000..08713ce27 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Blob.java @@ -0,0 +1,39 @@ +package convex.core.data.type; + +import convex.core.data.ABlob; +import convex.core.data.ACell; + +/** + * Type that represents any Blob + */ +public class Blob extends AStandardType { + + public static final Blob INSTANCE = new Blob(); + + private Blob() { + super(ABlob.class); + } + + @Override + public boolean check(ACell value) { + return (value instanceof ABlob); + } + + @Override + public String toString() { + return "Blob"; + } + + @Override + public ABlob defaultValue() { + return convex.core.data.Blob.EMPTY; + } + + @Override + public ABlob implicitCast(ACell a) { + if (a instanceof ABlob) return (ABlob)a; + return null; + } + + +} diff --git a/convex-core/src/main/java/convex/core/data/type/BlobMapType.java b/convex-core/src/main/java/convex/core/data/type/BlobMapType.java new file mode 100644 index 000000000..49b70e921 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/BlobMapType.java @@ -0,0 +1,41 @@ +package convex.core.data.type; + +import convex.core.data.ABlobMap; +import convex.core.data.ACell; +import convex.core.data.BlobMap; +import convex.core.data.BlobMaps; + +/** + * Type that represents any CVM map + */ +@SuppressWarnings("rawtypes") +public class BlobMapType extends AStandardType { + + public static final BlobMapType INSTANCE = new BlobMapType(); + + private BlobMapType() { + super(ABlobMap.class); + } + + @Override + public boolean check(ACell value) { + return (value instanceof ABlobMap); + } + + @Override + public String toString() { + return "BlobMap"; + } + + @Override + public ABlobMap defaultValue() { + return BlobMaps.empty(); + } + + @Override + public ABlobMap implicitCast(ACell a) { + if (a instanceof BlobMap) return (BlobMap)a; + return null; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/type/Boolean.java b/convex-core/src/main/java/convex/core/data/type/Boolean.java new file mode 100644 index 000000000..56cfb9caf --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Boolean.java @@ -0,0 +1,40 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.prim.CVMBool; +import convex.core.lang.RT; + +/** + * Type that represents CVM Long values + */ +public final class Boolean extends AStandardType { + /** + * Singleton runtime instance + */ + public static final Boolean INSTANCE = new Boolean(); + + private Boolean() { + super(CVMBool.class); + } + + @Override + public boolean check(ACell value) { + return value instanceof CVMBool; + } + + @Override + public String toString () { + return "Boolean"; + } + + @Override + public CVMBool defaultValue() { + return CVMBool.FALSE; + } + + @Override + public CVMBool implicitCast(ACell a) { + return RT.bool(a)?CVMBool.TRUE:CVMBool.FALSE; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/type/Byte.java b/convex-core/src/main/java/convex/core/data/type/Byte.java new file mode 100644 index 000000000..1b2267f78 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Byte.java @@ -0,0 +1,40 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.prim.CVMByte; + +/** + * Type that represents CVM Byte values + */ +public final class Byte extends ANumericType { + + /** + * Singleton runtime instance + */ + public static final Byte INSTANCE = new Byte(); + + private Byte() { + super (CVMByte.class); + } + + @Override + public boolean check(ACell value) { + return value instanceof CVMByte; + } + + @Override + public String toString () { + return "Byte"; + } + + @Override + public CVMByte defaultValue() { + return CVMByte.ZERO; + } + + @Override + public CVMByte implicitCast(ACell a) { + if (a instanceof CVMByte) return (CVMByte)a; + return null; + } +} diff --git a/convex-core/src/main/java/convex/core/data/type/CharacterType.java b/convex-core/src/main/java/convex/core/data/type/CharacterType.java new file mode 100644 index 000000000..cfd17f07c --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/CharacterType.java @@ -0,0 +1,40 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.prim.CVMChar; + +/** + * Type that represents CVM Byte values + */ +public final class CharacterType extends AStandardType { + + /** + * Singleton runtime instance + */ + public static final CharacterType INSTANCE = new CharacterType(); + + private CharacterType() { + super(CVMChar.class); + } + + @Override + public boolean check(ACell value) { + return value instanceof CVMChar; + } + + @Override + public String toString () { + return "Character"; + } + + @Override + public CVMChar defaultValue() { + return CVMChar.A; + } + + @Override + public CVMChar implicitCast(ACell a) { + if (a instanceof CVMChar) return (CVMChar)a; + return null; + } +} diff --git a/convex-core/src/main/java/convex/core/data/type/Collection.java b/convex-core/src/main/java/convex/core/data/type/Collection.java new file mode 100644 index 000000000..b547448e2 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Collection.java @@ -0,0 +1,39 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.ACollection; +import convex.core.data.Vectors; + +/** + * Type that represents any CVM collection + */ +@SuppressWarnings("rawtypes") +public class Collection extends AStandardType { + + public static final Collection INSTANCE = new Collection(); + + private Collection() { + super (ACollection.class); + } + + @Override + public boolean check(ACell value) { + return (value instanceof ACollection); + } + + @Override + public String toString() { + return "Collection"; + } + + @Override + public ACollection defaultValue() { + return Vectors.empty(); + } + + @Override + public ACollection implicitCast(ACell a) { + if (a instanceof ACollection) return (ACollection)a; + return null; + } +} diff --git a/convex-core/src/main/java/convex/core/data/type/DataStructure.java b/convex-core/src/main/java/convex/core/data/type/DataStructure.java new file mode 100644 index 000000000..512b85541 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/DataStructure.java @@ -0,0 +1,40 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.ADataStructure; +import convex.core.data.Vectors; + +/** + * Type that represents any CVM sequence + */ +@SuppressWarnings("rawtypes") +public class DataStructure extends AStandardType { + + public static final DataStructure INSTANCE = new DataStructure(); + + private DataStructure() { + super(ADataStructure.class); + } + + @Override + public boolean check(ACell value) { + return (value instanceof ADataStructure); + } + + @Override + public String toString() { + return "DataStructure"; + } + + @Override + public ADataStructure defaultValue() { + return Vectors.empty(); + } + + @Override + public ADataStructure implicitCast(ACell a) { + if (a instanceof ADataStructure) return (ADataStructure)a; + return null; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/type/Double.java b/convex-core/src/main/java/convex/core/data/type/Double.java new file mode 100644 index 000000000..1fe10ffac --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Double.java @@ -0,0 +1,40 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.prim.CVMDouble; +import convex.core.lang.RT; + +/** + * Type that represents CVM Double values + */ +public final class Double extends ANumericType { + + /** + * Singleton runtime instance + */ + public static final Double INSTANCE = new Double(); + + private Double() { + super (CVMDouble.class); + } + + @Override + public boolean check(ACell value) { + return value instanceof CVMDouble; + } + + @Override + public String toString () { + return "Double"; + } + + @Override + public CVMDouble defaultValue() { + return CVMDouble.ZERO; + } + + @Override + public CVMDouble implicitCast(ACell a) { + return RT.ensureDouble(a); + } +} diff --git a/convex-core/src/main/java/convex/core/data/type/Function.java b/convex-core/src/main/java/convex/core/data/type/Function.java new file mode 100644 index 000000000..896b3ddc5 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Function.java @@ -0,0 +1,40 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.lang.AFn; +import convex.core.lang.Core; + +/** + * Type that represents any CVM collection + */ +@SuppressWarnings("rawtypes") +public class Function extends AStandardType { + + public static final Function INSTANCE = new Function(); + + private Function() { + super(AFn.class); + } + + @Override + public boolean check(ACell value) { + return (value instanceof AFn); + } + + @Override + public String toString() { + return "Function"; + } + + @Override + public AFn defaultValue() { + return Core.VECTOR; + } + + @Override + public AFn implicitCast(ACell a) { + if (a instanceof AFn) return (AFn)a; + return null; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/type/KeywordType.java b/convex-core/src/main/java/convex/core/data/type/KeywordType.java new file mode 100644 index 000000000..16a3e8088 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/KeywordType.java @@ -0,0 +1,41 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.Keyword; +import convex.core.data.Keywords; + +/** + * Type that represents CVM Byte values + */ +public final class KeywordType extends AStandardType { + + /** + * Singleton runtime instance + */ + public static final KeywordType INSTANCE = new KeywordType(); + + private KeywordType() { + super(Keyword.class); + } + + @Override + public boolean check(ACell value) { + return value instanceof Keyword; + } + + @Override + public String toString () { + return "Keyword"; + } + + @Override + public Keyword defaultValue() { + return Keywords.FOO; + } + + @Override + public Keyword implicitCast(ACell a) { + if (a instanceof Keyword) return (Keyword)a; + return null; + } +} diff --git a/convex-core/src/main/java/convex/core/data/type/List.java b/convex-core/src/main/java/convex/core/data/type/List.java new file mode 100644 index 000000000..a1335a853 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/List.java @@ -0,0 +1,39 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.AList; +import convex.core.data.Lists; + +/** + * Type that represents any CVM collection + */ +@SuppressWarnings("rawtypes") +public class List extends AStandardType { + + public static final List INSTANCE = new List(); + + private List() { + super(AList.class); + } + + @Override + public boolean check(ACell value) { + return (value instanceof AList); + } + + @Override + public String toString() { + return "List"; + } + + @Override + public AList defaultValue() { + return Lists.empty(); + } + + @Override + public AList implicitCast(ACell a) { + if (a instanceof AList) return (AList)a; + return null; + } +} diff --git a/convex-core/src/main/java/convex/core/data/type/Long.java b/convex-core/src/main/java/convex/core/data/type/Long.java new file mode 100644 index 000000000..7b333b18d --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Long.java @@ -0,0 +1,40 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.prim.CVMLong; +import convex.core.lang.RT; + +/** + * Type that represents CVM Long values + */ +public final class Long extends ANumericType { + + /** + * Singleton runtime instance + */ + public static final Long INSTANCE = new Long(); + + private Long() { + super (CVMLong.class); + } + + @Override + public boolean check(ACell value) { + return value instanceof CVMLong; + } + + @Override + public String toString () { + return "Long"; + } + + @Override + public CVMLong defaultValue() { + return CVMLong.ZERO; + } + + @Override + public CVMLong implicitCast(ACell a) { + return RT.ensureLong(a); + } +} diff --git a/convex-core/src/main/java/convex/core/data/type/Map.java b/convex-core/src/main/java/convex/core/data/type/Map.java new file mode 100644 index 000000000..8cada737a --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Map.java @@ -0,0 +1,40 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.AMap; +import convex.core.data.Maps; + +/** + * Type that represents any CVM map + */ +@SuppressWarnings("rawtypes") +public class Map extends AStandardType { + + public static final Map INSTANCE = new Map(); + + private Map() { + super(AMap.class); + } + + @Override + public boolean check(ACell value) { + return (value instanceof AMap); + } + + @Override + public String toString() { + return "Map"; + } + + @Override + public AMap defaultValue() { + return Maps.empty(); + } + + @Override + public AMap implicitCast(ACell a) { + if (a instanceof AMap) return (AMap)a; + return null; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/type/Nil.java b/convex-core/src/main/java/convex/core/data/type/Nil.java new file mode 100644 index 000000000..2ba1e5f20 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Nil.java @@ -0,0 +1,47 @@ +package convex.core.data.type; + +import convex.core.data.ACell; + +/** + * The Type representing the single value 'nil' + */ +public class Nil extends AType { + + public static final Nil INSTANCE = new Nil(); + + private Nil() { + + } + + @Override + public boolean check(ACell value) { + return value==null; + } + + @Override + public boolean allowsNull() { + return true; + } + + @Override + public String toString() { + return "Nil"; + } + + @Override + public ACell defaultValue() { + return null; + } + + @Override + public ACell implicitCast(ACell a) { + // TODO: confirm anything can cast to null? + return null; + } + + @Override + public Class getJavaClass() { + return ACell.class; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/type/Number.java b/convex-core/src/main/java/convex/core/data/type/Number.java new file mode 100644 index 000000000..6dce5a37a --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Number.java @@ -0,0 +1,48 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.INumeric; +import convex.core.data.prim.APrimitive; +import convex.core.data.prim.CVMLong; +import convex.core.lang.RT; + +public class Number extends AType { + + public static final Number INSTANCE = new Number(); + + private Number() { + + } + + @Override + public boolean check(ACell value) { + return RT.isNumber(value); + } + + @Override + public boolean allowsNull() { + return false; + } + + @Override + public String toString() { + return "Number"; + } + + @Override + public APrimitive defaultValue() { + return CVMLong.ZERO; + } + + @Override + public APrimitive implicitCast(ACell a) { + if (a instanceof INumeric) return (APrimitive)a; + return null; + } + + @Override + public Class getJavaClass() { + return APrimitive.class; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/type/OpCode.java b/convex-core/src/main/java/convex/core/data/type/OpCode.java new file mode 100644 index 000000000..8617b2bc7 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/OpCode.java @@ -0,0 +1,42 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.lang.AOp; +import convex.core.lang.ops.Do; + +/** + * Type that represents CVM Long values + */ +@SuppressWarnings("rawtypes") +public final class OpCode extends AStandardType { + /** + * Singleton runtime instance + */ + public static final OpCode INSTANCE = new OpCode(); + + private OpCode() { + super(AOp.class); + } + + @Override + public boolean check(ACell value) { + return value instanceof AOp; + } + + @Override + public String toString () { + return "Op"; + } + + @Override + public AOp defaultValue() { + return Do.EMPTY; + } + + @Override + public AOp implicitCast(ACell a) { + if (a instanceof AOp) return (AOp)a; + return null; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/type/Record.java b/convex-core/src/main/java/convex/core/data/type/Record.java new file mode 100644 index 000000000..22e2cf86d --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Record.java @@ -0,0 +1,38 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.ARecord; + +/** + * Type that represents any CVM collection + */ +public class Record extends AStandardType { + + public static final Record INSTANCE = new Record(); + + private Record() { + super(ARecord.class); + } + + @Override + public boolean check(ACell value) { + return (value instanceof ARecord); + } + + @Override + public String toString() { + return "Record"; + } + + @Override + public ARecord defaultValue() { + return ARecord.DEFAULT_VALUE; + } + + @Override + public ARecord implicitCast(ACell a) { + if (a instanceof ARecord) return (ARecord)a; + return null; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/type/Sequence.java b/convex-core/src/main/java/convex/core/data/type/Sequence.java new file mode 100644 index 000000000..bd93884c4 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Sequence.java @@ -0,0 +1,40 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.ASequence; +import convex.core.data.AVector; +import convex.core.data.Vectors; + +/** + * Type that represents any CVM sequence + */ +@SuppressWarnings("rawtypes") +public class Sequence extends AStandardType { + + public static final Sequence INSTANCE = new Sequence(); + + private Sequence() { + super (ASequence.class); + } + + @Override + public boolean check(ACell value) { + return (value instanceof ASequence); + } + + @Override + public String toString() { + return "Sequence"; + } + + @Override + public AVector defaultValue() { + return Vectors.empty(); + } + + @Override + public ASequence implicitCast(ACell a) { + if (a instanceof ASequence) return (ASequence)a; + return null; + } +} diff --git a/convex-core/src/main/java/convex/core/data/type/Set.java b/convex-core/src/main/java/convex/core/data/type/Set.java new file mode 100644 index 000000000..97035f399 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Set.java @@ -0,0 +1,39 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.ASet; +import convex.core.data.Sets; + +/** + * Type that represents any CVM collection + */ +@SuppressWarnings("rawtypes") +public class Set extends AStandardType { + + public static final Set INSTANCE = new Set(); + + private Set() { + super(ASet.class); + } + + @Override + public boolean check(ACell value) { + return (value instanceof ASet); + } + + @Override + public String toString() { + return "Set"; + } + + @Override + public ASet defaultValue() { + return Sets.empty(); + } + + @Override + public ASet implicitCast(ACell a) { + if (a instanceof ASet) return (ASet)a; + return null; + } +} diff --git a/convex-core/src/main/java/convex/core/data/type/StringType.java b/convex-core/src/main/java/convex/core/data/type/StringType.java new file mode 100644 index 000000000..db0fec637 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/StringType.java @@ -0,0 +1,41 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.AString; +import convex.core.data.Strings; + +/** + * Type that represents CVM Byte values + */ +public final class StringType extends AStandardType { + /** + * Singleton runtime instance + */ + public static final StringType INSTANCE = new StringType(); + + private StringType() { + super (AString.class); + } + + @Override + public boolean check(ACell value) { + return value instanceof AString; + } + + @Override + public String toString () { + return "String"; + } + + + @Override + public AString defaultValue() { + return Strings.EMPTY; + } + + @Override + public AString implicitCast(ACell a) { + if (a instanceof AString) return (AString)a; + return null; + } +} diff --git a/convex-core/src/main/java/convex/core/data/type/SymbolType.java b/convex-core/src/main/java/convex/core/data/type/SymbolType.java new file mode 100644 index 000000000..2dd7e9d61 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/SymbolType.java @@ -0,0 +1,41 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.Symbol; +import convex.core.lang.Symbols; + +/** + * Type that represents CVM Byte values + */ +public final class SymbolType extends AStandardType { + + /** + * Singleton runtime instance + */ + public static final SymbolType INSTANCE = new SymbolType(); + + private SymbolType() { + super(Symbol.class); + } + + @Override + public boolean check(ACell value) { + return value instanceof Symbol; + } + + @Override + public String toString () { + return "Symbol"; + } + + @Override + public Symbol defaultValue() { + return Symbols.FOO; + } + + @Override + public Symbol implicitCast(ACell a) { + if (a instanceof Symbol) return (Symbol)a; + return null; + } +} diff --git a/convex-core/src/main/java/convex/core/data/type/SyntaxType.java b/convex-core/src/main/java/convex/core/data/type/SyntaxType.java new file mode 100644 index 000000000..97a1985e7 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/SyntaxType.java @@ -0,0 +1,40 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.Syntax; + +/** + * Type that represents CVM Syntax Object values + */ +public final class SyntaxType extends AStandardType { + + /** + * Singleton runtime instance + */ + public static final SyntaxType INSTANCE = new SyntaxType(); + + private SyntaxType() { + super(Syntax.class); + } + + @Override + public boolean check(ACell value) { + return value instanceof Syntax; + } + + @Override + public String toString () { + return "Syntax"; + } + + @Override + public Syntax defaultValue() { + return Syntax.EMPTY; + } + + @Override + public Syntax implicitCast(ACell a) { + if (a instanceof Syntax) return (Syntax)a; + return null; + } +} diff --git a/convex-core/src/main/java/convex/core/data/type/Transaction.java b/convex-core/src/main/java/convex/core/data/type/Transaction.java new file mode 100644 index 000000000..036d7af50 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Transaction.java @@ -0,0 +1,29 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Invoke; + +public class Transaction extends AStandardType{ + + protected Transaction() { + super(ATransaction.class); + } + + public static final Transaction INSTANCE = new Transaction(); + + public static final ATransaction DEFAULT = Invoke.create(Address.create(0), 0, (ACell)null); + + + @Override + public ATransaction defaultValue() { + return DEFAULT; + } + + @Override + public String toString() { + return "Transaction"; + } + +} diff --git a/convex-core/src/main/java/convex/core/data/type/Types.java b/convex-core/src/main/java/convex/core/data/type/Types.java new file mode 100644 index 000000000..14d8be44c --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Types.java @@ -0,0 +1,87 @@ +package convex.core.data.type; + +/** + * Static base class for Type system functionality + * + * NOTE: Currently Types are not planned for support in 1.0 runtime, but included here to support testing + * + */ +public class Types { + // Fundamental types + public static final Nil NIL=Nil.INSTANCE; + public static final Any ANY = Any.INSTANCE; + + // Collection types + public static final Collection COLLECTION=Collection.INSTANCE; + public static final Vector VECTOR=Vector.INSTANCE; + public static final List LIST=List.INSTANCE; + public static final Set SET=Set.INSTANCE; + + // Numeric types + public static final Long LONG=Long.INSTANCE; + public static final Byte BYTE = Byte.INSTANCE; + public static final Double DOUBLE = Double.INSTANCE; + public static final Number NUMBER = Number.INSTANCE; + + // Atomic types + public static final Boolean BOOLEAN = Boolean.INSTANCE; + public static final CharacterType CHARACTER = CharacterType.INSTANCE; + + // Named types + public static final KeywordType KEYWORD = KeywordType.INSTANCE; + public static final SymbolType SYMBOL = SymbolType.INSTANCE; + public static final StringType STRING = StringType.INSTANCE; + + // Data Structures + public static final DataStructure DATA_STRUCTURE = DataStructure.INSTANCE; + public static final Record RECORD = Record.INSTANCE; + public static final Map MAP = Map.INSTANCE; + public static final Sequence SEQUENCE = Sequence.INSTANCE; + + public static final BlobMapType BLOBMAP = BlobMapType.INSTANCE; + + public static final Blob BLOB = Blob.INSTANCE; + public static final AddressType ADDRESS = AddressType.INSTANCE; + + + public static final Function FUNCTION = Function.INSTANCE; + public static final OpCode OP = OpCode.INSTANCE; + public static final SyntaxType SYNTAX=SyntaxType.INSTANCE; + + public static final Transaction TRANSACTION=Transaction.INSTANCE; + + + + public static AType[] ALL_TYPES=new AType[] { + NIL, + ANY, + + COLLECTION, + DATA_STRUCTURE, + RECORD, + SEQUENCE, + VECTOR, + MAP, + LIST, + SET, + BLOBMAP, + + NUMBER, + LONG, + BYTE, + DOUBLE, + + BOOLEAN, + CHARACTER, + STRING, + KEYWORD, + SYMBOL, + + BLOB, + ADDRESS, + + FUNCTION, + OP, + SYNTAX, + }; +} diff --git a/convex-core/src/main/java/convex/core/data/type/Vector.java b/convex-core/src/main/java/convex/core/data/type/Vector.java new file mode 100644 index 000000000..ec859ca60 --- /dev/null +++ b/convex-core/src/main/java/convex/core/data/type/Vector.java @@ -0,0 +1,40 @@ +package convex.core.data.type; + +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.Vectors; + +/** + * Type that represents any CVM collection + */ +@SuppressWarnings("rawtypes") +public class Vector extends AStandardType { + + public static final Vector INSTANCE = new Vector(); + + private Vector() { + super(AVector.class); + } + + @Override + public boolean check(ACell value) { + return (value instanceof AVector); + } + + + @Override + public String toString() { + return "Vector"; + } + + @Override + public AVector defaultValue() { + return Vectors.empty(); + } + + @Override + public AVector implicitCast(ACell a) { + if (a instanceof AVector) return (AVector)a; + return null; + } +} diff --git a/convex-core/src/main/java/convex/core/exceptions/BadFormatException.java b/convex-core/src/main/java/convex/core/exceptions/BadFormatException.java new file mode 100644 index 000000000..a1e1e7311 --- /dev/null +++ b/convex-core/src/main/java/convex/core/exceptions/BadFormatException.java @@ -0,0 +1,21 @@ +package convex.core.exceptions; + +/** + * Class representing errors in format encountered when trying to read data from + * a serialised form. + * + * + * + */ +@SuppressWarnings("serial") +public class BadFormatException extends ValidationException { + + public BadFormatException(String message) { + super(message); + } + + public BadFormatException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/convex-core/src/main/java/convex/core/exceptions/BadSignatureException.java b/convex-core/src/main/java/convex/core/exceptions/BadSignatureException.java new file mode 100644 index 000000000..500f32d43 --- /dev/null +++ b/convex-core/src/main/java/convex/core/exceptions/BadSignatureException.java @@ -0,0 +1,21 @@ +package convex.core.exceptions; + +import convex.core.data.ACell; +import convex.core.data.SignedData; + +@SuppressWarnings("serial") +public class BadSignatureException extends ValidationException { + + private SignedData sig; + + public BadSignatureException(String message, SignedData sig) { + super(message); + this.sig = sig; + } + + @SuppressWarnings("unchecked") + public SignedData getSignature() { + return (SignedData) sig; + } + +} diff --git a/convex-core/src/main/java/convex/core/exceptions/BaseException.java b/convex-core/src/main/java/convex/core/exceptions/BaseException.java new file mode 100644 index 000000000..1f7bc4278 --- /dev/null +++ b/convex-core/src/main/java/convex/core/exceptions/BaseException.java @@ -0,0 +1,27 @@ +package convex.core.exceptions; + +/** + * Abstract base class for exceptions that we expect to encounter and need to + * handle. + * + * "If you don’t handle [exceptions], we shut your application down. That + * dramatically increases the reliability of the system.” - Anders Hejlsberg + * + */ +@SuppressWarnings("serial") +public abstract class BaseException extends Exception { + + public BaseException(String message) { + super(message); + } + + public BaseException(String message, Throwable cause) { + super(message, cause); + } + + @Override + public Throwable fillInStackTrace() { + return super.fillInStackTrace(); + // return this; // possible optimisation?? + } +} diff --git a/convex-core/src/main/java/convex/core/exceptions/InvalidDataException.java b/convex-core/src/main/java/convex/core/exceptions/InvalidDataException.java new file mode 100644 index 000000000..4f193b752 --- /dev/null +++ b/convex-core/src/main/java/convex/core/exceptions/InvalidDataException.java @@ -0,0 +1,21 @@ +package convex.core.exceptions; + +/** + * Class representing errors encountered during data validation. + * + * In general, InvalidDataException occurs if the data format is correct, but + * the data fails to satisfy a validation invariant. + */ +@SuppressWarnings("serial") +public class InvalidDataException extends ValidationException { + private final Object data; + + public InvalidDataException(String message, Object data) { + super(message); + this.data = data; + } + + public Object getData() { + return data; + } +} diff --git a/convex-core/src/main/java/convex/core/exceptions/MissingDataException.java b/convex-core/src/main/java/convex/core/exceptions/MissingDataException.java new file mode 100644 index 000000000..c5ddfc72b --- /dev/null +++ b/convex-core/src/main/java/convex/core/exceptions/MissingDataException.java @@ -0,0 +1,37 @@ +package convex.core.exceptions; + +import convex.core.data.Hash; +import convex.core.store.Stores; + +/** + * Exception thrown when an attempt is made to dereference a value that is not + * present in the current data store. + * + * Normally shouldn't be caught / referenced directly. Requires special handling + * by Peers. + * + */ +@SuppressWarnings("serial") +public class MissingDataException extends RuntimeException { + + private Hash hash; + + private MissingDataException(String message, Hash hash) { + super(message); + this.hash = hash; + } + + public MissingDataException(Hash hash) { + // TODO: remove inefficiency + this("Missing " + hash + " in store " + Stores.current().toString(), hash); + } + +// @Override +// public Throwable fillInStackTrace() { +// return this; +// } + + public Hash getMissingHash() { + return hash; + } +} diff --git a/convex-core/src/main/java/convex/core/exceptions/ParseException.java b/convex-core/src/main/java/convex/core/exceptions/ParseException.java new file mode 100644 index 000000000..aed2352be --- /dev/null +++ b/convex-core/src/main/java/convex/core/exceptions/ParseException.java @@ -0,0 +1,17 @@ +package convex.core.exceptions; + +/** + * Class for reader parse exceptions + * + */ +@SuppressWarnings("serial") +public class ParseException extends Error { + + public ParseException(String message) { + super(message); + } + + public ParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/convex-core/src/main/java/convex/core/exceptions/TODOException.java b/convex-core/src/main/java/convex/core/exceptions/TODOException.java new file mode 100644 index 000000000..9ce4317f4 --- /dev/null +++ b/convex-core/src/main/java/convex/core/exceptions/TODOException.java @@ -0,0 +1,18 @@ +package convex.core.exceptions; + +@SuppressWarnings("serial") +public class TODOException extends RuntimeException { + + public TODOException(String message) { + super("TODO: "+message); + } + + public TODOException() { + this("TODO"); + } + + public TODOException(Exception e) { + super("TODO: "+e.getMessage(),e); + } + +} diff --git a/convex-core/src/main/java/convex/core/exceptions/ValidationException.java b/convex-core/src/main/java/convex/core/exceptions/ValidationException.java new file mode 100644 index 000000000..d789ff439 --- /dev/null +++ b/convex-core/src/main/java/convex/core/exceptions/ValidationException.java @@ -0,0 +1,16 @@ +package convex.core.exceptions; + +/** + * Class representing a validation failure + * + */ +@SuppressWarnings("serial") +public class ValidationException extends BaseException { + public ValidationException(String message) { + super(message); + } + + public ValidationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/convex-core/src/main/java/convex/core/init/AInitConfig.java b/convex-core/src/main/java/convex/core/init/AInitConfig.java new file mode 100644 index 000000000..7079f0256 --- /dev/null +++ b/convex-core/src/main/java/convex/core/init/AInitConfig.java @@ -0,0 +1,79 @@ +package convex.core.init; + + +import convex.core.crypto.AKeyPair; +import convex.core.crypto.Ed25519KeyPair; +import convex.core.data.Address; + + +public class AInitConfig { + + protected AKeyPair userKeyPairs[]; + protected AKeyPair peerKeyPairs[]; + + + public int getUserCount() { + return userKeyPairs.length; + } + public int getPeerCount() { + return peerKeyPairs.length; + } + + public AKeyPair getUserKeyPair(int index) { + return userKeyPairs[index]; + } + public AKeyPair getPeerKeyPair(int index) { + return peerKeyPairs[index]; + } + + public AKeyPair[] getPeerKeyPairs() { + return peerKeyPairs; + } + + public Address getUserAddress(int index) { + return Init.calcUserAddress(index); + } + + public Address getPeerAddress(int index) { + return Init.calcPeerAddress(getUserCount(), index); + } + + public Address[] getPeerAddressList() { + Address result[] = new Address[getPeerCount()]; + for (int index = 0; index < getPeerCount(); index ++) { + result[index] = getPeerAddress(index); + } + return result; + } + + public static int DEFAULT_PEER_COUNT = 8; + public static int DEFAULT_USER_COUNT = 2; + + + protected AInitConfig(AKeyPair userKeyPairs[], AKeyPair peerKeyPairs[]) { + this.userKeyPairs = userKeyPairs; + this.peerKeyPairs = peerKeyPairs; + } + + public static AInitConfig create() { + return create(DEFAULT_USER_COUNT, DEFAULT_PEER_COUNT); + } + + public static AInitConfig create(int userCount, int peerCount) { + AKeyPair userKeyPairs[] = new Ed25519KeyPair[userCount]; + AKeyPair peerKeyPairs[] = new Ed25519KeyPair[peerCount]; + + for (int i = 0; i < userCount; i++) { + AKeyPair kp = Ed25519KeyPair.createSeeded(543212345 + i); + userKeyPairs[i] = kp; + } + + for (int i = 0; i < peerCount; i++) { + AKeyPair kp = Ed25519KeyPair.createSeeded(123454321 + i); + peerKeyPairs[i] = kp; + } + + return new AInitConfig(userKeyPairs, peerKeyPairs); + } + +} diff --git a/convex-core/src/main/java/convex/core/init/Init.java b/convex-core/src/main/java/convex/core/init/Init.java new file mode 100644 index 000000000..dcb4a016d --- /dev/null +++ b/convex-core/src/main/java/convex/core/init/Init.java @@ -0,0 +1,350 @@ +package convex.core.init; + +import java.io.IOException; +import java.util.List; + +import convex.core.Coin; +import convex.core.Constants; +import convex.core.State; +import convex.core.data.ACell; +import convex.core.data.AList; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.core.data.BlobMap; +import convex.core.data.BlobMaps; +import convex.core.data.PeerStatus; +import convex.core.data.Vectors; +import convex.core.lang.Context; +import convex.core.lang.Core; +import convex.core.lang.RT; +import convex.core.lang.Reader; +import convex.core.lang.Symbols; +import convex.core.util.Utils; + +/** + * Static class for generating the initial Convex State + * + * "The beginning is the most important part of the work." - Plato, The Republic + */ +public class Init { + + // Standard accounts numbers + public static final Address NULL_ADDRESS = Address.create(0); + public static final Address INIT_ADDRESS = Address.create(1); + + // Governance accounts and funding pools + public static final Address RESERVED_ADDRESS = Address.create(2); + public static final Address MAINBANK_ADDRESS = Address.create(3); + public static final Address ROOTFUND_ADDRESS = Address.create(4); + public static final Address MAINPOOL_ADDRESS = Address.create(5); + public static final Address LIVEPOOL_ADDRESS = Address.create(6); + + // Built-in special accounts + public static final Address MEMORY_EXCHANGE_ADDRESS = Address.create(7); + public static final Address CORE_ADDRESS = Address.create(8); + public static final Address REGISTRY_ADDRESS = Address.create(9); + public static final Address TRUST_ADDRESS = Address.create(10); + + // Base for user-specified addresses + public static final Address GENESIS_ADDRESS = Address.create(11); + + + public static State createBaseState(List genesisKeys) { + // accumulators for initial state maps + BlobMap peers = BlobMaps.empty(); + AVector accts = Vectors.empty(); + + long supply = Constants.MAX_SUPPLY; + + // Initial accounts + accts = addGovernanceAccount(accts, NULL_ADDRESS, 0L); // Null account + accts = addGovernanceAccount(accts, INIT_ADDRESS, 0L); // Initialisation Account + + // Reserved fund + long reserved = 100*Coin.EMERALD; + accts = addGovernanceAccount(accts, RESERVED_ADDRESS, reserved); // 75% for investors + supply-=reserved; + + // Foundation governance fund + long governance = 240*Coin.EMERALD; + accts = addGovernanceAccount(accts, MAINBANK_ADDRESS, governance); // 24% Foundation + supply -= governance; + + // Pools for network rewards + long rootFund = 8 * Coin.EMERALD; // 0.8% Long term net rewards + accts = addGovernanceAccount(accts, ROOTFUND_ADDRESS, rootFund); + supply -= rootFund; + + long mainPool = 1 * Coin.EMERALD; // 0.1% distribute 5% / year ~= 0.0003% /day + accts = addGovernanceAccount(accts, MAINPOOL_ADDRESS, mainPool); + supply -= mainPool; + + long livePool = 5 * Coin.DIAMOND; // 0.0005% = approx 2 days of mainpool feed + accts = addGovernanceAccount(accts, LIVEPOOL_ADDRESS, 5 * Coin.DIAMOND); + supply -= livePool; + + // Set up memory exchange. Initially 1GB available at 1000 per byte. (one diamond coin liquidity) + { + long memoryCoins = 1 * Coin.DIAMOND; + accts = addMemoryExchange(accts, MEMORY_EXCHANGE_ADDRESS, memoryCoins, 1000000000L); + supply -= memoryCoins; + } + + // Always have at least one user and one peer setup + int keyCount = genesisKeys.size(); + assert(keyCount > 0); + + // Core library at static address: CORE_ADDRESS + accts = addCoreLibrary(accts, CORE_ADDRESS); + // Core Account should now be fully initialised + // BASE_USER_ADDRESS = accts.size(); + + // Build globals + AVector globals = Constants.INITIAL_GLOBALS; + + // Create the inital state + State s = State.create(accts, peers, globals, BlobMaps.empty()); + + // Add the static defined libraries at addresses: TRUST_ADDRESS, REGISTRY_ADDRESS + s = createStaticLibraries(s, TRUST_ADDRESS, REGISTRY_ADDRESS); + + // Reload accounts with the libraries + accts = s.getAccounts(); + + // Set up initial user accounts + assert(accts.count() == GENESIS_ADDRESS.longValue()); + { + long userFunds = (long)(supply*0.8); // 80% to user accounts + supply -= userFunds; + + // Genesis user gets half of all user funds + long genFunds = userFunds/2; + accts = addAccount(accts, GENESIS_ADDRESS, genesisKeys.get(0), genFunds); + userFunds -= genFunds; + + for (int i = 0; i < keyCount; i++) { + // TODO: construct peer controller addresses + Address address = Address.create(accts.count()); + assert(address.longValue() == accts.count()); + AccountKey key = genesisKeys.get(i); + long userBalance = userFunds / (keyCount-i); + accts = addAccount(accts, address, key, userBalance); + userFunds -= userBalance; + } + assert(userFunds == 0L); + } + + // Finally add peers + // Set up initial peers + + // BASE_PEER_ADDRESS = accts.size(); + { + long peerFunds = supply; + supply -= peerFunds; + for (int i = 0; i < keyCount; i++) { + AccountKey peerKey = genesisKeys.get(i); + Address peerController = getGenesisPeerAddress(i); + + // set a staked fund such that the first peer starts with super-majority + long peerStake = peerFunds / (keyCount-i); + + // split peer funds between stake and account + peers = addPeer(peers, peerKey, peerController, peerStake); + peerFunds -= peerStake; + } + assert(peerFunds == 0L); + } + + + // Add the new accounts to the state + s = s.withAccounts(accts); + // Add peers to the state + s = s.withPeers(peers); + + { // Test total funds after creating user / peer accounts + long total = s.computeTotalFunds(); + if (total != Constants.MAX_SUPPLY) throw new Error("Bad total amount: " + total); + } + + return s; + } + + public static State createStaticLibraries(State s, Address trustAddress, Address registryAddress) { + + // At this point we have a raw initial state with no user or peer accounts + + s = doActorDeploy(s, "convex/registry.cvx"); + s = doActorDeploy(s, "convex/trust.cvx"); + + { // Register core libraries now that registry exists + Context ctx = Context.createFake(s, INIT_ADDRESS); + ctx = ctx.eval(Reader.read("(call *registry* (cns-update 'convex.core " + CORE_ADDRESS + "))")); + + s = ctx.getState(); + s = register(s, CORE_ADDRESS, "Convex Core Library", "Core utilities accessible by default in any account."); + s = register(s, MEMORY_EXCHANGE_ADDRESS, "Memory Exchange Pool", "Automated exchange following the Convex memory allowance model."); + } + + /* + * This test below does not correctly calculate the total funds of the state, since + * the peers have not yet been added. + * + { // Test total funds after creating core libraries + long total = s.computeTotalFunds(); + if (total != Constants.MAX_SUPPLY) throw new Error("Bad total amount: " + total + " should be " + Constants.MAX_SUPPLY); + } + */ + + return s; + } + + public static State createState(List genesisKeys) { + try { + State s=createBaseState(genesisKeys); + + // ============================================================ + // Standard library deployment + s = doActorDeploy(s, "convex/fungible.cvx"); + s = doActorDeploy(s, "convex/trusted-oracle/actor.cvx"); + s = doActorDeploy(s, "convex/trusted-oracle.cvx"); + s = doActorDeploy(s, "convex/asset.cvx"); + s = doActorDeploy(s, "torus/exchange.cvx"); + s = doActorDeploy(s, "asset/nft/simple.cvx"); + s = doActorDeploy(s, "asset/nft/tokens.cvx"); + s = doActorDeploy(s, "asset/box/actor.cvx"); + s = doActorDeploy(s, "asset/box.cvx"); + s = doActorDeploy(s, "convex/play.cvx"); + + { // Deploy Currencies + @SuppressWarnings("unchecked") + AVector> table = (AVector>) Reader + .readResourceAsData("torus/currencies.cvx"); + for (AVector row : table) { + s = doCurrencyDeploy(s, row); + } + } + + // Final funds check + long finalTotal = s.computeTotalFunds(); + if (finalTotal != Constants.MAX_SUPPLY) + throw new Error("Bad total funds in init state amount: " + finalTotal); + + return s; + + } catch (Throwable e) { + e.printStackTrace(); + throw Utils.sneakyThrow(e); + } + } + + public static Address calcPeerAddress(int userCount, int index) { + return Address.create(GENESIS_ADDRESS.longValue() + userCount + index); + } + + public static Address calcUserAddress(int index) { + return Address.create(GENESIS_ADDRESS.longValue() + index); + } + + // A CVX file contains forms which must be wrapped in a `(do ...)` and deployed as an actor. + // First form is the name that must be used when registering the actor. + // + private static State doActorDeploy(State s, String resource) { + Context
ctx = Context.createFake(s, INIT_ADDRESS); + + try { + AList forms = Reader.readAll(Utils.readResourceAsString(resource)); + + ctx = ctx.deployActor(forms.next().cons(Symbols.DO)); + if (ctx.isExceptional()) throw new Error("Error deploying actor:" + ctx.getValue()); + + ctx = ctx.eval(Reader.read("(call *registry* (cns-update " + forms.get(0) + " " + ctx.getResult() + "))")); + if (ctx.isExceptional()) throw new Error("Error while registering actor:" + ctx.getValue()); + + return ctx.getState(); + } catch (IOException e) { + throw Utils.sneakyThrow(e); + } + } + + private static State doCurrencyDeploy(State s, AVector row) { + String symbol = row.get(0).toString(); + double usdValue = RT.jvm(row.get(6)); + long decimals = RT.jvm(row.get(5)); + + // Currency liquidity in lowest currency division + double liquidity = (Long) RT.jvm(row.get(4)) * Math.pow(10, decimals); + + // CVX price for unit + double price = usdValue * 1000; + double cvx = price * liquidity / Math.pow(10, decimals); + + long supply = 1000000000000L; + Context
ctx = Context.createFake(s, MAINBANK_ADDRESS); + ctx = ctx.eval(Reader + .read("(do (import convex.fungible :as fun) (deploy (fun/build-token {:supply " + supply + "})))")); + Address addr = ctx.getResult(); + ctx = ctx.eval(Reader.read("(do (import torus.exchange :as torus) (torus/add-liquidity " + addr + " " + + liquidity + " " + cvx + "))")); + if (ctx.isExceptional()) throw new Error("Error adding market liquidity: " + ctx.getValue()); + ctx = ctx.eval(Reader.read("(call *registry* (cns-update 'currency." + symbol + " " + addr + "))")); + if (ctx.isExceptional()) throw new Error("Error registering currency in CNS: " + ctx.getValue()); + return ctx.getState(); + } + + private static State register(State state, Address origin, String name, String description) { + Context ctx = Context.createFake(state, origin); + ctx = ctx.eval(Reader.read("(call *registry* (register {:description \"" + description + "\" :name \"" + name + "\"}))")); + return ctx.getState(); + } + + public static Address getGenesisAddress() { + return GENESIS_ADDRESS; + } + + public static Address getGenesisPeerAddress(int index) { + return GENESIS_ADDRESS.offset(index+1); + } + + private static BlobMap addPeer(BlobMap peers, AccountKey peerKey, + Address owner, long initialStake) { + PeerStatus ps = PeerStatus.create(owner, initialStake, null); + return peers.assoc(peerKey, ps); + } + + private static AVector addGovernanceAccount(AVector accts, Address a, long balance) { + if (accts.count() != a.longValue()) throw new Error("Incorrect initialisation address: " + a); + AccountStatus as = AccountStatus.createGovernance(balance); + accts = accts.conj(as); + return accts; + } + + private static AVector addMemoryExchange(AVector accts, Address a, long balance, long allowance) { + if (accts.count() != a.longValue()) throw new Error("Incorrect memory exchange address: " + a); + AccountStatus as = AccountStatus.createGovernance(balance).withMemory(allowance); + accts = accts.conj(as); + return accts; + } + + private static AVector addCoreLibrary(AVector accts, Address a) { + if (accts.count() != a.longValue()) throw new Error("Incorrect core library address: " + a); + + AccountStatus as = AccountStatus.createActor(); + as=as.withEnvironment(Core.ENVIRONMENT); + as=as.withMetadata(Core.METADATA); + accts = accts.conj(as); + return accts; + } + + private static AVector addAccount(AVector accts, Address a, AccountKey key, + long balance) { + if (accts.count() != a.longValue()) throw new Error("Incorrect account address: " + a); + AccountStatus as = AccountStatus.create(0L, balance, key); + as = as.withMemory(Constants.INITIAL_ACCOUNT_ALLOWANCE); + accts = accts.conj(as); + return accts; + } + + +} diff --git a/convex-core/src/main/java/convex/core/lang/AFn.java b/convex-core/src/main/java/convex/core/lang/AFn.java new file mode 100644 index 000000000..ec5301680 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/AFn.java @@ -0,0 +1,60 @@ +package convex.core.lang; + +import convex.core.data.ACell; +import convex.core.data.IRefFunction; +import convex.core.data.Tag; +import convex.core.data.type.AType; +import convex.core.data.type.Types; + +/** + * Base class for functions expressed as values + * + * "You know what's web-scale? The Web. And you know what it is? Dynamically + * typed." - Stuart Halloway + * + * @param Return type of functions. + */ +public abstract class AFn extends ACell implements IFn { + + @Override + public abstract Context invoke(Context context, ACell[] args); + + @Override + public abstract AFn updateRefs(IRefFunction func); + + @Override + public final AType getType() { + return Types.FUNCTION; + } + + /** + * Tests if this function supports the given argument list + * + * By default, checks if the function supports the given arity only. + * + * TODO: intention is to override this to include dynamic type checks etc. + * @param args Array of arguments + * @return true if function supports the specified args array + */ + public boolean supportsArgs(ACell[] args) { + return hasArity(args.length); + } + + /** + * Tests if this function supports the given arity. + * @param arity Arity to check + * @return true if function supports the given arity, false otherwise + */ + public abstract boolean hasArity(int arity); + + @Override + public boolean isCVMValue() { + return true; + } + + @Override + public byte getTag() { + return Tag.FN; + } + +} diff --git a/convex-core/src/main/java/convex/core/lang/AOp.java b/convex-core/src/main/java/convex/core/lang/AOp.java new file mode 100644 index 000000000..19221d706 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/AOp.java @@ -0,0 +1,91 @@ +package convex.core.lang; + +import convex.core.data.ACell; +import convex.core.data.Format; +import convex.core.data.IRefFunction; +import convex.core.data.Tag; +import convex.core.data.type.AType; +import convex.core.data.type.Types; + +/** + * Abstract base class for operations + * + * "...that was the big revelation to me when I was in graduate school—when I + * finally understood that the half page of code on the bottom of page 13 of the + * Lisp 1.5 manual was Lisp in itself. These were “Maxwell’s Equations of + * Software!” This is the whole world of programming in a few lines that I can + * put my hand over." + * - Alan Kay + * + * @param the type of the operation return value + */ +public abstract class AOp extends ACell { + + /** + * Executes this op with the given context. Must preserve depth unless an + * exceptional is returned. + * + * @param Type of Context + * @param context Initial Context + * @return The updated Context after executing this operation + * + */ + public abstract Context execute(Context context); + + @Override + public final AType getType() { + return Types.OP; + } + + @Override + public int estimatedEncodingSize() { + return 10+Format.MAX_EMBEDDED_LENGTH; + } + + @Override + public boolean isCanonical() { + return true; + } + + @Override + public ACell toCanonical() { + return this; + } + + @Override public final boolean isCVMValue() { + return true; + } + + /** + * Returns the opcode for this op + * + * @return Opcode as a byte + */ + public abstract byte opCode(); + + @Override + public final int encode(byte[] bs, int pos) { + bs[pos++]=Tag.OP; + bs[pos++]=opCode(); + return encodeRaw(bs,pos); + } + + /** + * Writes the raw data for this Op to the specified bytebuffer. Assumes Op tag + * and opcode already written. + * + * @param bs Byte array to write to + * @param pos Position to write in byte array + * @return The updated position + */ + @Override + public abstract int encodeRaw(byte[] bs, int pos); + + @Override + public abstract AOp updateRefs(IRefFunction func); + + @Override + public byte getTag() { + return Tag.OP; + } +} \ No newline at end of file diff --git a/convex-core/src/main/java/convex/core/lang/Compiler.java b/convex-core/src/main/java/convex/core/lang/Compiler.java new file mode 100644 index 000000000..3fad35d3e --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/Compiler.java @@ -0,0 +1,868 @@ +package convex.core.lang; + +import java.util.Map; + +import convex.core.ErrorCodes; +import convex.core.data.ABlob; +import convex.core.data.ACell; +import convex.core.data.AList; +import convex.core.data.AMap; +import convex.core.data.ASequence; +import convex.core.data.ASet; +import convex.core.data.AVector; +import convex.core.data.Address; +import convex.core.data.Keyword; +import convex.core.data.List; +import convex.core.data.MapEntry; +import convex.core.data.Maps; +import convex.core.data.Sets; +import convex.core.data.Symbol; +import convex.core.data.Syntax; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMLong; +import convex.core.data.type.Types; +import convex.core.lang.Context.CompilerState; +import convex.core.lang.impl.AClosure; +import convex.core.lang.impl.CoreFn; +import convex.core.lang.impl.MultiFn; +import convex.core.lang.ops.Cond; +import convex.core.lang.ops.Constant; +import convex.core.lang.ops.Def; +import convex.core.lang.ops.Do; +import convex.core.lang.ops.Invoke; +import convex.core.lang.ops.Lambda; +import convex.core.lang.ops.Let; +import convex.core.lang.ops.Local; +import convex.core.lang.ops.Lookup; +import convex.core.lang.ops.Query; +import convex.core.lang.ops.Special; +import convex.core.util.Utils; + +/** + * Compiler class responsible for transforming forms (code as data) into an + * Op tree for execution. + * + * Phases in complete evaluation: + *
    + *
  1. Expansion (form -> AST)
  2. + *
  3. Compile (AST -> op)
  4. + *
  5. Execute (op -> result)
  6. + *
+ * + * Expanded form follows certain rules: - No remaining macros / expanders in + * leading list positions + * + * TODO: consider including typechecking in expansion phase as per: + * http://www.ccs.neu.edu/home/stchang/pubs/ckg-popl2017.pdf + * + * "A language that doesn't affect the way you think about programming is not + * worth knowing." ― Alan Perlis + */ +public class Compiler { + + /** + * Expands and compiles a form. Equivalent to running expansion followed by + * compile. Should not be used directly, intended for use via + * Context.expandCompile(...) + * + * @param + * @param form A form, either raw or wrapped in a Syntax Object + * @param context Compilation context + * @return Context with compiled op as result + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + static Context> expandCompile(ACell form, Context context) { + // expand phase starts with initial expander + AFn ex = INITIAL_EXPANDER; + + // Use initial expander both as current and continuation expander + // call expand via context to get correct depth and exception handling + final Context ctx = context.invoke(ex, form,ex); + + if (ctx.isExceptional()) return (Context>) (Object) ctx; + ACell c=ctx.getResult(); + + return ctx.compile(c); + } + + /** + * Compiles a single form. Should not be used directly, intended for use via + * Context.compile(...) + * + * Updates context with result, juice consumed + * + * @param Type of op result + * @param expandedForm A fully expanded form expressed as a Syntax Object + * @param context + * @return Context with compiled Op as result + */ + @SuppressWarnings("unchecked") + static Context> compile(ACell form, Context context) { + if (form==null) return compileConstant(context,null); + + if (form instanceof AList) return compileList((AList) form, context); + + if (form instanceof Syntax) return compileSyntax((Syntax) form, context); + if (form instanceof AVector) return compileVector((AVector) form, context); + if (form instanceof AMap) return compileMap((AMap) form, context); + if (form instanceof ASet) return compileSet((ASet) form, context); + + if ((form instanceof Keyword) || (form instanceof ABlob)) { + return compileConstant(context, form); + } + + if (form instanceof Symbol) { + return compileSymbol((Symbol) form, context); + } + + if (form instanceof AOp) { + // already compiled, just return as constant + return context.withResult(Juice.COMPILE_CONSTANT, (AOp)form); + } + + // return as a constant literal + return compileConstant(context,form); + } + + /** + * Compiles a sequence of forms, returning a vector of ops in the updated + * context. Equivalent to calling compile sequentially on each form. + * + * @param + * @param + * @param forms + * @param context + * @return Context with Vector of compiled ops as result + */ + @SuppressWarnings("unchecked") + static Context>> compileAll(ASequence forms, Context context) { + if (forms == null) return context.withResult(Vectors.empty()); // consider null as empty list + int n = forms.size(); + AVector> obs = Vectors.empty(); + for (int i = 0; i < n; i++) { + ACell form = forms.get(i); + context = context.compile(form); + if (context.isExceptional()) return (Context>>) context; + obs = obs.conj((AOp) context.getResult()); + } + return context.withResult(obs); + } + + @SuppressWarnings("unchecked") + private static > Context compileSyntax(Syntax s, Context context) { + context=compile(s.getValue(),context); + return (Context) context; + } + + @SuppressWarnings("unchecked") + private static > Context compileSymbol(Symbol sym, Context context) { + // First check for lexically defined Symbols + CompilerState cs=context.getCompilerState(); + if (cs!=null) { + CVMLong position=cs.getPosition(sym); + if (position!=null) { + Local op=Local.create(position.longValue()); + return (Context) context.withResult(Juice.COMPILE_LOOKUP,op); + } + } + + // Next check for special values + Special maybeSpecial=Special.forSymbol(sym); + if (maybeSpecial!=null) { + return context.withResult(maybeSpecial); + } + + // Get address of compilation environment to use for lookup resolution. + Address address=context.getAddress(); + + Lookup lookUp=Lookup.create(Constant.of(address),sym); + return (Context) context.withResult(Juice.COMPILE_LOOKUP, lookUp); + } + + @SuppressWarnings("unchecked") + private static > Context compileSetBang(AList list, Context context) { + if (list.count()!=3) return context.withArityError("set! requires two arguments, a symbol and an expression"); + + ACell a1=list.get(1); + if (!(a1 instanceof Symbol)) return context.withCompileError("set! requires a symbol as first argument"); + Symbol sym=(Symbol)a1; + + CompilerState cs=context.getCompilerState(); + CVMLong position=(cs==null)?null:context.getCompilerState().getPosition(sym); + if (position==null) return context.withCompileError("Trying to set! an undeclared symbol: "+sym); + + context=context.compile(list.get(2)); + if (context.isExceptional()) return (Context)context; + AOp exp=(AOp) context.getResult(); + + T op=(T) convex.core.lang.ops.Set.create(position.longValue(), exp); + return context.withResult(Juice.COMPILE_NODE,op); + } + + @SuppressWarnings("unchecked") + private static > Context compileLookup(AList list, Context context) { + long n=list.count(); + if ((n<2)||(n>3)) return context.withArityError("lookup requires one or two arguments, an optional expression and a Symbol"); + + AOp
exp=null; + if (n==3) { + context=context.compile(list.get(1)); + if (context.isExceptional()) return (Context)context; + exp=(AOp
) context.getResult(); + } + + ACell a1=list.get(n-1); + if (!(a1 instanceof Symbol)) return context.withCompileError("lookup requires a Symbol as last argument"); + Symbol sym=(Symbol)a1; + + T op=(T) Lookup.create(exp,sym); + return context.withResult(Juice.COMPILE_NODE,op); + } + + + private static > Context compileMap(AMap form, Context context) { + int n = form.size(); + ACell[] vs = new ACell[1 + n * 2]; + vs[0] = Symbols.HASH_MAP; + for (int i = 0; i < n; i++) { + MapEntry me = form.entryAt(i); + vs[1 + i * 2] = me.getKey(); + vs[1 + i * 2 + 1] = me.getValue(); + } + return compileList(List.create(vs), context); + } + + private static > Context compileSet(ASet form, Context context) { + AVector vs = Vectors.empty(); + for (ACell o : form) { + vs = vs.conj(o); + } + vs = vs.conj(Symbols.HASH_SET); + return compileList(List.reverse(vs), context); + } + + @SuppressWarnings("unchecked") + private static > Context compileVector(AVector vec, Context context) { + int n = vec.size(); + if (n == 0) return (Context) context.withResult(Juice.COMPILE_CONSTANT, Constant.EMPTY_VECTOR); + + context = context.compileAll(vec); + AVector> obs = (AVector>) context.getResult(); + + // return a 'vector' call - note function arg is a constant, we don't want to + // lookup on the 'vector' symbol + Constant fn = Constant.create(Core.VECTOR); + return (Context) context.withResult(Juice.COMPILE_NODE, Invoke.create(fn, obs)); + } + + @SuppressWarnings("unchecked") + private static Context compileConstant(Context context, ACell value) { + return (Context) context.withResult(Juice.COMPILE_CONSTANT, Constant.create(value)); + } + + /** + * Compiles a quoted form, returning an op that will produce a data structure + * after evaluation of all unquotes. + * + * @param + * @param context + * @param form Quoted form. May be a regular value or Syntax object. + * @return Context with complied op as result + */ + @SuppressWarnings("unchecked") + private static Context> compileQuasiQuoted(Context context, ACell aForm, int depth) { + ACell form; + + // Check if form is a Syntax Object and unwrap if necessary + boolean isSyntax; + if (aForm instanceof Syntax) { + form= Syntax.unwrap(aForm); + isSyntax=true; + } else { + form=aForm; + isSyntax=false; + } + + if (form instanceof ASequence) { + ASequence seq = (ASequence) form; + int n = seq.size(); + if (n == 0) { + return compileConstant(context, aForm); + } + + if (isListStarting(Symbols.UNQUOTE, form)) { + if (depth==1) { + if (n != 2) return context.withArityError("unquote requires 1 argument"); + ACell unquoted=seq.get(1); + //if (!(unquoted instanceof Syntax)) return context.withCompileError("unquote expects an expanded Syntax Object"); + Context> opContext = expandCompile(unquoted, context); + return opContext; + } else { + depth-=1; + } + } else if (isListStarting(Symbols.QUASIQUOTE, form)) { + depth+=1; + } + + // compile quoted elements + context = compileAllQuasiQuoted(context, seq, depth); + ASequence> rSeq = (ASequence>) context.getResult(); + + ACell fn = (seq instanceof AList) ? Core.LIST : Core.VECTOR; + AOp inv = Invoke.create( Constant.create(fn), rSeq); + if (isSyntax) { + inv=wrapSyntaxBuilder(inv,(Syntax)aForm); + } + return context.withResult(Juice.COMPILE_NODE, inv); + } else if (form instanceof AMap) { + AMap map = (AMap) form; + AVector rSeq = Vectors.empty(); + for (Map.Entry me : map.entrySet()) { + rSeq = rSeq.append(me.getKey()); + rSeq = rSeq.append(me.getValue()); + } + + // compile quoted elements + context = compileAllQuasiQuoted(context, rSeq,depth); + ASequence> cSeq = (ASequence>) context.getResult(); + + ACell fn = Core.HASHMAP; + AOp inv = Invoke.create(Constant.create(fn), cSeq); + if (isSyntax) { + inv=wrapSyntaxBuilder(inv,(Syntax)aForm); + } + + return context.withResult(Juice.COMPILE_NODE, inv); + } else if (form instanceof ASet) { + ASet set = (ASet) form; + AVector rSeq = set.toVector(); + + // compile quoted elements + context = compileAllQuasiQuoted(context, rSeq,depth); + ASequence> cSeq = (ASequence>) context.getResult(); + + ACell fn = Core.HASHSET; + AOp inv = Invoke.create(Constant.create(fn), cSeq); + if (isSyntax) { + inv=wrapSyntaxBuilder(inv,(Syntax)aForm); + } + + return context.withResult(Juice.COMPILE_NODE, inv); + }else { + return compileConstant(context, aForm); + } + } + + private static AOp wrapSyntaxBuilder(AOp op, Syntax source) { + return Invoke.create(Constant.create(Core.SYNTAX), op, Constant.create(source.getMeta())); + } + + /** + * Compiles a sequence of quoted forms + * + * @param + * @param context + * @param form + * @return Context with complied sequence of ops as result + */ + @SuppressWarnings("unchecked") + private static Context>> compileAllQuasiQuoted(Context context, ASequence forms, int depth) { + int n = forms.size(); + // create a list of ops producing each sub-element + ASequence> rSeq = Vectors.empty(); + for (int i = 0; i < n; i++) { + ACell subSyntax = forms.get(i); + ACell subForm = Syntax.unwrap(subSyntax); + if (isListStarting(Symbols.UNQUOTE_SPLICING, subForm)) { + AList subList = (AList) subForm; + int sn = subList.size(); + if (sn != 2) return context.withArityError("unquote-splicing requires 1 argument"); + // unquote-splicing looks like it needs flatmap + return context.withError(ErrorCodes.TODO,"unquote-splicing not yet supported"); + } else { + Context> rctx= compileQuasiQuoted(context, subSyntax,depth); + rSeq = (ASequence>) (rSeq.conj(rctx.getResult())); + } + } + return context.withResult(rSeq); + } + + /** + * Returns true if the form is a List starting with value equal to the + * the specified element + * + * @param element + * @param form + * @return True if form is a list starting with a Syntax Object wrapping the + * specified element, false otherwise. + */ + @SuppressWarnings("unchecked") + private static boolean isListStarting(Symbol element, ACell form) { + if (!(form instanceof AList)) return false; + AList list = (AList) form; + if (list.count() == 0) return false; + ACell firstElement=list.get(0); + return Utils.equals(element, Syntax.unwrap(firstElement)); + } + + @SuppressWarnings("unchecked") + private static > Context compileList(AList list, Context context) { + int n = list.size(); + if (n == 0) return (Context) context.withResult(Juice.COMPILE_CONSTANT, Constant.EMPTY_LIST); + + // first entry in list should be syntax + ACell first = list.get(0); + ACell head = Syntax.unwrap(first); + + if (head instanceof Symbol) { + Symbol sym = (Symbol) head; + + if (sym.equals(Symbols.DO)) { + context = context.compileAll(list.next()); + if (context.isExceptional()) return (Context) context; + Do op = Do.create((AVector>) context.getResult()); + return (Context) context.withResult(Juice.COMPILE_NODE, op); + } + + if (sym.equals(Symbols.LET)) return compileLet(list, context, false); + + if (sym.equals(Symbols.COND)) { + context = context.compileAll(list.next()); + if (context.isExceptional()) return (Context) context; + Cond op = Cond.create((AVector>) (context.getResult())); + return (Context) context.withResult(Juice.COMPILE_NODE, op); + } + + if (sym.equals(Symbols.DEF)) return compileDef(list, context); + if (sym.equals(Symbols.FN)) return compileFn(list, context); + + if (sym.equals(Symbols.QUOTE)) { + if (list.size() != 2) return context.withCompileError(sym + " expects one argument."); + return compileConstant(context,list.get(1)); + } + + if (sym.equals(Symbols.QUASIQUOTE)) { + if (list.size() != 2) return context.withCompileError(sym + " expects one argument."); + return (Context) compileQuasiQuoted(context, list.get(1),1); + } + + if (sym.equals(Symbols.UNQUOTE)) { + // execute the unquoted code directly to get a form to compile + if (list.size() != 2) return context.withCompileError(Symbols.UNQUOTE + " expects one argument."); + context = context.expandCompile(list.get(1)); + if (context.isExceptional()) return (Context) context; + AOp quotedOp = (AOp) context.getResult(); + + Context rctx = context.execute(quotedOp); + if (rctx.isExceptional()) return (Context) rctx; + + Syntax resultForm = Syntax.create(rctx.getResult()); + // need to expand and compile here, since we just created a raw form + return (Context) expandCompile(resultForm, context); + } + + if (sym.equals(Symbols.QUERY)) { + context = context.compileAll(list.next()); + if (context.isExceptional()) return (Context) context; + Query op = Query.create((AVector>) context.getResult()); + return (Context) context.withResult(Juice.COMPILE_NODE, op); + } + + if (sym.equals(Symbols.LOOP)) return compileLet(list, context, true); + if (sym.equals(Symbols.SET_BANG)) return compileSetBang(list, context); + + if (sym.equals(Symbols.LOOKUP)) return compileLookup(list, context); + + } + + // must be a regular function call + context = context.compileAll(list); + if (context.isExceptional()) return (Context) context; + Invoke op = Invoke.create((AVector>) context.getResult()); + + return (Context) context.withResult(Juice.COMPILE_NODE, op); + } + + + @SuppressWarnings("unchecked") + private static > Context compileLet(ASequence list, Context context, + boolean isLoop) { + // list = (let [...] a b c ...) + int n=list.size(); + if (n<2) return context.withCompileError(list.get(0) + " requires a binding form vector at minimum"); + + ACell bo = list.get(1); + + if (!(bo instanceof AVector)) + return context.withCompileError(list.get(0) + " requires a vector of binding forms but got: " + bo); + AVector bv = (AVector) bo; + int bn = bv.size(); + if ((bn & 1) != 0) return context.withCompileError( + list.get(0) + " requires a binding vector with an even number of forms but got: " + bn); + + AVector bindingForms = Vectors.empty(); + AVector> ops = Vectors.empty(); + + for (int i = 0; i < bn; i += 2) { + // Get corresponding op + context = context.expandCompile(bv.get(i + 1)); + if (context.isExceptional()) return (Context) context; + AOp op = (AOp) context.getResult(); + ops = ops.conj(op); + + // Get a binding form. Note binding happens *after* op + ACell bf = bv.get(i); + context=compileBinding(bf,context); + if (context.isExceptional()) return (Context) context; + bindingForms = bindingForms.conj(context.getResult()); + } + int exs = n - 2; // expressions in let after binding vector + for (int i = 2; i < 2 + exs; i++) { + context = context.expandCompile(list.get(i)); + if (context.isExceptional()) return (Context) context; + AOp op = (AOp) context.getResult(); + ops = ops.conj(op); + } + AOp op = Let.create(bindingForms, ops, isLoop); + + return (Context) context.withResult(Juice.COMPILE_NODE, op); + } + + /** + * Compiles a binding form. Updates the current CompilerState. Should save compiler state if used + * @param bindingForm + * @param context + * @return + */ + private static Context compileBinding(ACell bindingForm,Context context) { + CompilerState cs=context.getCompilerState(); + if (cs==null) cs=CompilerState.EMPTY; + + cs=updateBinding(bindingForm,cs); + if (cs==null) return context.withCompileError("Bad binding form"); + + context=context.withCompilerState(cs); + return context.withResult(bindingForm); + } + + @SuppressWarnings("unchecked") + private static CompilerState updateBinding(ACell bindingForm,CompilerState cs) { + if (bindingForm instanceof Symbol) { + Symbol sym=(Symbol)bindingForm; + if (!sym.equals(Symbols.UNDERSCORE)) { + cs=cs.define(sym, null); // TODO: metadata? + } + } else if (bindingForm instanceof AVector) { + AVector v=(AVector)bindingForm; + boolean foundAmpersand=false; + long vcount=v.count(); // count of binding form symbols (may include & etc.) + for (long i=0; i=(vcount-1)) return null; // trailing ampersand + foundAmpersand=true; + bf=v.get(i+1); + i++; + } + cs=updateBinding(bf,cs); + } + } else { + cs=null; + } + return cs; + } + + /** + * Compiles a lambda function form "(fn [...] ...)" to create a Lambda op. + * + * @param + * @param + * @param list + * @param context + * @return Context with compiled op as result. + */ + @SuppressWarnings("unchecked") + private static > Context compileFn(AList list, Context context) { + // list.get(0) is presumably fn + int n = list.size(); + if (n < 2) return context.withArityError("fn requires parameter vector and body in form: " + list); + + // check if we have a vector, in which case we have a single function definition + ACell firstObject = Syntax.unwrap(list.get(1)); + if (firstObject instanceof AVector) { + AVector paramsVector=(AVector) firstObject; + AList bodyList=list.drop(2); + return compileFnInstance(paramsVector,bodyList,context); + } + + return compileMultiFn(list.drop(1),context); + } + + @SuppressWarnings({ "unchecked"}) + private static > Context compileMultiFn(AList list, Context context) { + AVector> fns=Vectors.empty(); + + int num=list.size(); + for (int i=0; i) o,context); + if (context.isExceptional()) return (Context) context; + + AClosure compiledFn=((Lambda) context.getResult()).getFunction(); + fns=fns.conj(compiledFn); + } + + MultiFn mf=MultiFn.create(fns); + Lambda op = Lambda.create(mf); + return (Context) context.withResult(Juice.COMPILE_NODE, op); + } + + /** + * Compiles a function instance function form "([...] ...)" to create a Lambda op. + * + * @param + * @param + * @param list + * @param context + * @return Context with compiled op as result. + */ + @SuppressWarnings("unchecked") + private static > Context compileFnInstance(AList list, Context context) { + int n = list.size(); + if (n < 1) return context.withArityError("fn requires parameter vector and body in form: " + list); + + ACell firstObject = Syntax.unwrap(list.get(0)); + if (firstObject instanceof AVector) { + AVector paramsVector=(AVector) firstObject; + AList bodyList=list.drop(1); + return compileFnInstance(paramsVector,bodyList,context); + } + + return context.withError(ErrorCodes.COMPILE, + "fn instance requires a vector of parameters but got form: " + list); + } + + @SuppressWarnings("unchecked") + private static > Context compileFnInstance(AVector paramsVector, AList bodyList,Context context) { + // need to save compiler state, since we are compiling bindings + CompilerState savedCompilerState=context.getCompilerState(); + + context=compileBinding(paramsVector,context); + if (context.isExceptional()) return context.withCompilerState(savedCompilerState); // restore before return + paramsVector=(AVector) context.getResult(); + + context = context.compileAll(bodyList); + if (context.isExceptional()) return context.withCompilerState(savedCompilerState); // restore before return + + int n=bodyList.size(); + AOp body; + if (n == 0) { + // no body, so function just returns nil + body = Constant.nil(); + } else if (n == 1) { + // one body element, so just unwrap from list + body = ((ASequence>) context.getResult()).get(0); + } else { + // wrap multiple expressions in implicit do + body = Do.create(((ASequence>) context.getResult())); + } + + Lambda op = Lambda.create(paramsVector, body); + context=context.withCompilerState(savedCompilerState); + return (Context) context.withResult(Juice.COMPILE_NODE, op); + } + + @SuppressWarnings("unchecked") + private static > Context compileDef(AList list, Context context) { + int n = list.size(); + if (n != 3) return context.withCompileError("def requires a symbol and an expression, but got: " + list); + + ACell symArg=list.get(1); + + {// check we are actually defining a symbol + ACell sym = Syntax.unwrapAll(symArg); + if (!(sym instanceof Symbol)) return context.withCompileError("def requires a Symbol as first argument but got: " + RT.getType(sym)); + } + + ACell exp=list.get(2); + + // move metadata from expression. TODO: do we need to expand this first? + if (exp instanceof Syntax) { + symArg=Syntax.create(symArg).mergeMeta(((Syntax)exp).getMeta()); + exp=Syntax.unwrap(exp); + } + + context = context.compile(exp); + if (context.isExceptional()) return (Context) context; + + Def op = Def.create(symArg, (AOp) context.getResult()); + return (Context) context.withResult(Juice.COMPILE_NODE, op); + } + + + + + /** + * Initial expander used for expansion of forms prior to compilation. + * + * Should work on both raw forms and syntax objects. + * + * Follows the "Expansion-Passing Style" approach of Dybvig, Friedman, and Haynes + */ + public static final AFn INITIAL_EXPANDER =new CoreFn(Symbols.STAR_INITIAL_EXPANDER) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context,ACell[] args ) { + if (args.length!=2) return context.withArityError(exactArityMessage(2, args.length)); + ACell x = args[0]; + AFn cont=RT.ensureFunction(args[1]); + if (cont==null) return context.withCastError(1, args,Types.FUNCTION); + + // If x is a Syntax object, need to compile the datum + // TODO: check interactions with macros etc. + if (x instanceof Syntax) { + Syntax sx=(Syntax)x; + ACell[] nargs=args.clone(); + nargs[0]=sx.getValue(); + context=context.invoke(this, nargs); + if (context.isExceptional()) return context; + ACell expanded=context.getResult(); + Syntax result=Syntax.mergeMeta(expanded,sx); + return context.withResult(Juice.EXPAND_CONSTANT, result); + } + + ACell form = x; + + // First check for sequences. This covers most cases. + if (form instanceof ASequence) { + + // first check for List + if (form instanceof AList) { + AList listForm = (AList) form; + int n = listForm.size(); + // consider length 0 lists as constant + if (n == 0) return context.withResult(Juice.EXPAND_CONSTANT, x); + + // we need to check if the form itself starts with an expander + ACell first = Syntax.unwrap(listForm.get(0)); + + // check for macro / expander in initial position. + // Note that 'quote' is handled by this, via QUOTE_EXPANDER + AFn expander = context.lookupExpander(first); + if (expander!=null) { + return context.expand(expander,x, cont); // (exp x cont) + } + } + + // need to recursively expand collection elements + // OK for vectors and lists + ASequence seq = (ASequence) form; + if (seq.isEmpty()) return context.withResult(Juice.EXPAND_CONSTANT, x); + Context[] ct = new Context[] { context }; + + ASequence updated; + + updated = seq.map(elem -> { + Context ctx = ct[0]; + if (ctx.isExceptional()) return null; + + // Expand like: (cont x cont) + ctx = ctx.expand(cont,elem, cont); + + if (ctx.isExceptional()) { + ct[0] = ctx; + return null; + } + ACell newElement = ctx.getResult(); + ct[0] = ctx; + return newElement; + }); + Context rctx = ct[0]; + if (context.isExceptional()) return rctx; + return rctx.withResult(Juice.EXPAND_SEQUENCE, updated); + } + + if (form instanceof ASet) { + @SuppressWarnings("rawtypes") + Context ctx = (Context)context; + ASet updated = Sets.empty(); + for (ACell elem : ((ASet) form)) { + ctx = ctx.expand(cont, elem, cont); + if (ctx.isExceptional()) return ctx; + + ACell newElement = ctx.getResult(); + updated = updated.conj(newElement); + } + return ctx.withResult(Juice.EXPAND_SEQUENCE, updated); + } + + if (form instanceof AMap) { + @SuppressWarnings("rawtypes") + Context ctx = (Context)context; + AMap updated = Maps.empty(); + for (Map.Entry me : ((AMap) form).entrySet()) { + // get new key + ctx = ctx.expand(cont,me.getKey(), cont); + if (ctx.isExceptional()) return ctx; + + ACell newKey = ctx.getResult(); + + // get new value + ctx = ctx.expand(cont,me.getValue(), cont); + if (ctx.isExceptional()) return ctx; + ACell newValue = ctx.getResult(); + + updated = updated.assoc(newKey, newValue); + } + return ctx.withResult(Juice.EXPAND_SEQUENCE, updated); + } + + // Return the Syntax Object directly for anything else + // Remember to preserve metadata on symbols in particular! + return context.withResult(Juice.EXPAND_CONSTANT, x); + } + }; + + /** + * Expander used for expansion of `quote` forms. + * + * Should work on both raw forms and syntax objects. + * + * Follows the "Expansion-Passing Style" approach of Dybvig, Friedman, and Haynes + */ + public static final AFn QUOTE_EXPANDER =new CoreFn(Symbols.QUOTE) { + @Override + public Context invoke(Context context,ACell[] args ) { + if (args.length!=2) return context.withArityError(exactArityMessage(2, args.length)); + ACell x = args[0]; + + return context.withResult(Juice.EXPAND_CONSTANT,x); + } + }; + + /** + * Expander used for expansion of `quasiquote` forms. + * + * Should work on both raw forms and syntax objects. + * + * Follows the "Expansion-Passing Style" approach of Dybvig, Friedman, and Haynes + */ + public static final AFn QUASIQUOTE_EXPANDER =new CoreFn(Symbols.QUASIQUOTE) { + @Override + public Context invoke(Context context,ACell[] args ) { + if (args.length!=2) return context.withArityError(exactArityMessage(2, args.length)); + ACell x = args[0]; + + return context.withResult(Juice.EXPAND_CONSTANT,x); + } + }; + + +} diff --git a/convex-core/src/main/java/convex/core/lang/Context.java b/convex-core/src/main/java/convex/core/lang/Context.java new file mode 100644 index 000000000..4ad002ceb --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/Context.java @@ -0,0 +1,2262 @@ +package convex.core.lang; + +import convex.core.Constants; +import convex.core.ErrorCodes; +import convex.core.State; +import convex.core.data.ABlobMap; +import convex.core.data.ACell; +import convex.core.data.AHashMap; +import convex.core.data.AList; +import convex.core.data.AMap; +import convex.core.data.AObject; +import convex.core.data.ASequence; +import convex.core.data.AString; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.core.data.Blob; +import convex.core.data.Hash; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.MapEntry; +import convex.core.data.Maps; +import convex.core.data.PeerStatus; +import convex.core.data.Strings; +import convex.core.data.Symbol; +import convex.core.data.Syntax; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMLong; +import convex.core.data.type.AType; +import convex.core.exceptions.TODOException; +import convex.core.init.Init; +import convex.core.lang.impl.AExceptional; +import convex.core.lang.impl.ATrampoline; +import convex.core.lang.impl.ErrorValue; +import convex.core.lang.impl.HaltValue; +import convex.core.lang.impl.RecurValue; +import convex.core.lang.impl.Reduced; +import convex.core.lang.impl.ReturnValue; +import convex.core.lang.impl.RollbackValue; +import convex.core.lang.impl.TailcallValue; +import convex.core.util.Economics; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * Representation of CVM execution context. + *

+ * + * Execution context includes: + * - The current on-Chain state, including the defined execution environment for each Address + * - Local lexical bindings for the current execution position + * - The identity (as an Address) for the origin, caller and currently executing actor + * - Juice and execution depth current status for + * - Result of the last operation executed (which may be exceptional) + *

+ * Interestingly, this behaves like Scala's ZIO[Context-Stuff, AExceptional, T] + *

+ * Contexts maintain checks on execution depth and juice to control against arbitrary on-chain + * execution. Coupled with limits on total juice and limits on memory allocation + * per unit juice, this places an upper bound on execution time and space. + *

+ * Contexts also support returning exceptional values. Exceptional results may come + * from arbitrary nested depth (which requires a bit of complexity to reset depth when + * catching exceptional values). We avoid using Java exceptions here, because exceptionals + * are "normal" in the context of on-chain execution, and we'd like to avoid the overhead + * of exception handling - may be especially important in DoS scenarios. + *

+ * "If you have a procedure with 10 parameters, you probably missed some" + * - Alan Perlis + * + * @param Result type of Context + */ +public class Context extends AObject { + private static final long INITIAL_JUICE = 0; + + // Default values + private static final AVector> DEFAULT_LOG = null; + private static int DEFAULT_DEPTH = 0; + private static final AExceptional DEFAULT_EXCEPTION = null; + private static final long DEFAULT_OFFER = 0L; + public static final AVector EMPTY_BINDINGS=Vectors.empty(); + // private static final Logger log=Logger.getLogger(Context.class.getName()); + + /* + * Frequently changing fields during execution. + * + * While these are mutable, it is also very cheap to just fork() short-lived Contexts + * because the JVM generational GC will just sweep them up shortly afterwards. + */ + + private long juice; + private T result; + private AExceptional exception; + private int depth; + private AVector localBindings; + private ChainState chainState; + + /** + * Local log is a [vector of [address values] entries] + */ + private AVector> log; + private CompilerState compilerState; + + + /** + * Inner class compiler state. + * + * Maintains a mapping of Symbols to positions in a definition vector corresponding to lexical scope. + * + */ + public static final class CompilerState { + public static final CompilerState EMPTY = new CompilerState(Vectors.empty(),Maps.empty()); + + private AVector definitions; + private AHashMap mappings; + + private CompilerState(AVector definitions, AHashMap mappings) { + this.definitions=definitions; + this.mappings=mappings; + } + + public CompilerState define(Symbol sym, Syntax syn) { + long position=definitions.count(); + AVector newDefs=definitions.conj(syn); + AHashMap newMaps=mappings.assoc(sym, CVMLong.create(position)); + return new CompilerState(newDefs,newMaps); + } + + public CVMLong getPosition(Symbol sym) { + return mappings.get(sym); + } + } + + /** + * Inner class for less-frequently changing state related to Actor execution + * Should save some allocation / GC on average, since it will change less + * frequently than the surrounding Context and can be cheaply copied by reference. + * + * SECURITY: security critical, since it determines the current *address* and *caller* + * which in turn controls access to most account resources and rights. + */ + private static final class ChainState { + private final State state; + private final Address origin; + private final Address caller; + private final Address address; + private final long offer; + + /** + * Cached copy of the current environment. Avoid looking up via Address each time. + */ + private final AHashMap environment; + private final AHashMap> metadata; + + private ChainState(State state, Address origin,Address caller, Address address,AHashMap environment, AHashMap> metadata, long offer) { + this.state=state; + this.origin=origin; + this.caller=caller; + this.address=address; + this.environment=environment; + this.metadata=metadata; + this.offer=offer; + } + + public static ChainState create(State state, Address origin, Address caller, Address address, long offer) { + AHashMap environment=Core.ENVIRONMENT; + AHashMap> metadata=Core.METADATA; + if (address!=null) { + AccountStatus as=state.getAccount(address); + if (as!=null) { + environment=as.getEnvironment(); + metadata=as.getMetadata(); + } + } + return new ChainState(state,origin,caller,address,environment,metadata,offer); + } + + public ChainState withStateOffer(State newState,long newOffer) { + if ((state==newState)&&(offer==newOffer)) return this; + return create(newState,origin,caller,address,newOffer); + } + + private ChainState withState(State newState) { + if (state==newState) return this; + return create(newState,origin,caller,address,offer); + } + + private long getOffer() { + return offer; + } + + /** + * Gets the current defined environment + * @return + */ + private AHashMap getEnvironment() { + if (environment==null) return Maps.empty(); + return environment; + } + + private ChainState withEnvironment(AHashMap newEnvironment) { + if (environment==newEnvironment) return this; + AccountStatus as=state.getAccount(address); + AccountStatus nas=as.withEnvironment(newEnvironment); + State newState=state.putAccount(address,nas); + return withState(newState); + } + + public ChainState withEnvironment(AHashMap newEnvironment, + AHashMap> newMeta) { + if ((environment==newEnvironment)&&(metadata==newMeta)) return this; + AccountStatus as=state.getAccount(address); + AccountStatus nas=as.withEnvironment(newEnvironment).withMetadata(newMeta); + State newState=state.putAccount(address,nas); + return withState(newState); + } + + private ChainState withAccounts(AVector newAccounts) { + return withState(state.withAccounts(newAccounts)); + } + + public AHashMap> getMetadata() { + if (metadata==null) return Maps.empty(); + return metadata; + } + + + } + + private Context(ChainState chainState, long juice, AVector localBindings2, T result,int depth, AExceptional exception, AVector> log, CompilerState comp) { + this.chainState=chainState; + this.juice=juice; + this.localBindings=localBindings2; + this.result=result; + this.depth=depth; + this.exception=exception; + this.log=log; + this.compilerState=comp; + } + + @SuppressWarnings("unchecked") + private static Context create(ChainState cs, long juice, AVector localBindings, ACell result, int depth,AVector> log, CompilerState comp) { + if (juice<0) throw new IllegalArgumentException("Negative juice! "+juice); + return new Context(cs,juice,localBindings,(T)result,depth,DEFAULT_EXCEPTION,log,comp); + } + + private static Context create(State state, long juice,AVector localBindings, T result, int depth, Address origin,Address caller, Address address, long offer, AVector> log, CompilerState comp) { + ChainState chainState=ChainState.create(state,origin,caller,address,offer); + return create(chainState,juice,localBindings,result,depth,log,comp); + } + + /** + * Creates an execution context with a default actor address. + * + * Useful for Testing + * + * @param state State to use for this Context + * @return Fake context + */ + public static Context createFake(State state) { + return createFake(state,Init.CORE_ADDRESS); + } + + /** + * Creates a "fake" execution context for the given address. + * + * Not valid for use in real transactions, but can be used to + * compute stuff off-chain "as-if" the actor made the call. + * + * @param state State to use for this Context + * @param origin Origin address to use + * @return Fake context + */ + public static Context createFake(State state, Address origin) { + if (origin==null) throw new IllegalArgumentException("Null address!"); + return create(state,Constants.MAX_TRANSACTION_JUICE,EMPTY_BINDINGS,null,0,origin,null,origin, 0, DEFAULT_LOG,null); + } + + /** + * Creates an initial execution context with the specified actor as origin, and reserving the appropriate + * amount of juice. + * + * Juice reserve is extracted from the actor's current balance. + * + * @param state Initial State for Context + * @param origin Origin Address for Context + * @param juice Initial juice for Context + * @return Initial execution context with reserved juice. + */ + public static Context createInitial(State state, Address origin,long juice) { + AccountStatus as=state.getAccount(origin); + if (as==null) { + // no account + return Context.createFake(state).withError(ErrorCodes.NOBODY); + } + + long balance=as.getBalance(); + long juicePrice=state.getJuicePrice().longValue(); + + // reduce juice if insufficient balance + juice=Math.min(juice,balance/juicePrice); + long reserve=juicePrice*juice; + + assert (reserve<=balance) : "Reserve calculation failed!"; + + long newBalance=balance-reserve; + as=as.withBalance(newBalance); + state=state.putAccount(origin, as); + return create(state,juice,EMPTY_BINDINGS,null,DEFAULT_DEPTH,origin,null,origin,INITIAL_JUICE,DEFAULT_LOG,null); + } + + + + + /** + * Performs key actions at the end of a transaction: + *

    + *
  • Refunds juice
  • + *
  • Accumulates used juice fees in globals
  • + *
+ * + * @param initialState State before transaction execution (after prepare) + * @param initialJuice total juice reserved at start of transaction + * @return Updated context + */ + public Context completeTransaction(State initialState, long initialJuice) { + // get state at end of transaction application + State state=getState(); + + long remainingJuice=Math.max(0L, juice); + long usedJuice=initialJuice-remainingJuice; + long juicePrice=initialState.getJuicePrice().longValue(); + assert(usedJuice>=0); + + long refund=0L; + + // maybe refund remaining juice + if (remainingJuice>0L) { + // Compute refund. Shouldn't be possible to overflow? + // But do a paranoid checked multiply just in case + refund+=Math.multiplyExact(remainingJuice,juicePrice); + } + + // compute memory delta + Address address=getAddress(); + AccountStatus account=state.getAccount(address); + long memUsed=state.getMemorySize()-initialState.getMemorySize(); + long allowance=account.getMemory(); + long balanceLeft=account.getBalance(); + boolean memoryFailure=false; + + long memorySpend=0L; // usually zero + if (memUsed>0) { + long allowanceUsed=Math.min(allowance, memUsed); + if (allowanceUsed>0) { + account=account.withMemory(allowance-allowanceUsed); + } + + // compute additional memory purchase requirement beyond allowance + long purchaseNeeded=memUsed-allowanceUsed; + if (purchaseNeeded>0) { + AccountStatus pool=state.getAccount(Init.MEMORY_EXCHANGE_ADDRESS); + // we do memory purchase if pool exists + if (pool!=null) { + long poolBalance=pool.getBalance(); + long poolAllowance=pool.getMemory(); + memorySpend=Economics.swapPrice(purchaseNeeded, poolAllowance, poolBalance); + + if ((refund+balanceLeft)>=memorySpend) { + // enough to cover memory price, so automatically buy from pool + // System.out.println("Buying "+purchaseNeeded+" memory for: "+price); + pool=pool.withBalances(poolBalance+memorySpend, poolAllowance-purchaseNeeded); + state=state.putAccount(Init.MEMORY_EXCHANGE_ADDRESS,pool); + } else { + // Insufficient memory, so need to roll back state to before transaction + // origin should still pay transaction fees, but no memory costs + memorySpend=0L; + state=initialState; + account=state.getAccount(address); + memoryFailure=true; + } + } + } + } else { + // credit any unused memory back to allowance (may be zero) + long allowanceCredit=-memUsed; + account=account.withMemory(allowance+allowanceCredit); + } + + // Make balance changes if needed for refund and memory purchase + account=account.addBalance(refund-memorySpend); + + // update Account + state=state.putAccount(address,account); + + // maybe add used juice to miner fees + if (usedJuice>0L) { + long transactionFees = usedJuice*juicePrice; + long oldFees=state.getGlobalFees().longValue(); + long newFees=oldFees+transactionFees; + state=state.withGlobalFees(CVMLong.create(newFees)); + } + + // final state update and result reporting + Context rctx=this.withState(state); + if (memoryFailure) { + rctx=rctx.withError(ErrorCodes.MEMORY, "Unable to allocate additional memory required for transaction ("+memUsed+" bytes)"); + } + return rctx; + } + + @SuppressWarnings("unchecked") + public Context withState(State newState) { + return (Context) this.withChainState(chainState.withState(newState)); + } + + /** + * Get the latest state from this Context + * @return State instance + */ + public State getState() { + return chainState.state; + } + + /** + * Get the juice available in this Context + * @return Juice available + */ + public long getJuice() { + return juice; + } + + /** + * Get the current offer from this Context + * @return Offered amount in Convex copper + */ + public long getOffer() { + return chainState.getOffer(); + } + + /** + * Gets the current Environment + * @return Environment map + */ + public AHashMap getEnvironment() { + return chainState.getEnvironment(); + } + + /** + * Gets the compiler state + * @return CompilerState instance + */ + public CompilerState getCompilerState() { + return compilerState; + } + + /** + * Gets the metadata for the current Account + * @return Metadata map + */ + public AHashMap> getMetadata() { + return chainState.getMetadata(); + } + + /** + * Consumes juice, returning an updated context if sufficient juice remains or an exceptional JUICE error. + * @param Result type + * @param gulp Amount of jjuice to consume + * @return Updated context with juice consumed + */ + @SuppressWarnings("unchecked") + public Context consumeJuice(long gulp) { + if (gulp<=0) throw new Error("Juice gulp must be positive!"); + if(!checkJuice(gulp)) return withJuiceError(); + juice=juice-gulp; + return (Context) this; + // return new Context(chainState,newJuice,localBindings,(R) result,depth,isExceptional); + } + + /** + * Checks if there is sufficient juice for a given gulp of consumption. Does not alter context in any way. + * + * @param gulp Amount of juice to be consumed. + * @return true if juice is sufficient, false otherwise. + */ + public boolean checkJuice(long gulp) { + return (juice>=gulp); + } + + /** + * Looks up a symbol's value in the current execution context, without any effect on the Context (no juice consumed etc.) + * + * @param Type of value associated with the given symbol + * @param symbol Symbol to look up. May be qualified + * @return Context with the result of the lookup (may be an undeclared exception) + */ + public Context lookup(Symbol symbol) { + // try lookup in dynamic environment + return lookupDynamic(symbol); + } + + /** + * Looks up a value in the dynamic environment. Consumes no juice. + * + * Returns an UNDECLARED exception if the symbol cannot be resolved. + * + * @param Result type + * @param symbol Symbol to look up + * @return Updated Context + */ + public Context lookupDynamic(Symbol symbol) { + Address address=getAddress(); + return lookupDynamic(address,symbol); + } + + /** + * Looks up a value in the dynamic environment. Consumes no juice. + * Returns an UNDECLARED exception if the symbol cannot be resolved. + * Returns a NOBODY exception if the specified Account does not exist + * + * @param Type of value result + * @param address Address of account in which to look up value + * @param symbol Symbol to look up + * @return Updated Context + */ + @SuppressWarnings("unchecked") + public Context lookupDynamic(Address address, Symbol symbol) { + AccountStatus as=getAccountStatus(address); + if (as==null) return withError(ErrorCodes.NOBODY,"No account found for: "+symbol.toString()); + MapEntry envEntry=lookupDynamicEntry(as,symbol); + + // if not found, return UNDECLARED error + if (envEntry==null) { + return withError(ErrorCodes.UNDECLARED,symbol.toString()); + } + + // Result is whatever is defined as the datum value in the environment entry + ACell result = envEntry.getValue(); + return (Context) withResult(result); + } + + /** + * Looks up Metadata for the given symbol in this context + * @param sym Symbol to look up + * @return Metadata for given symbol (may be empty) or null if undeclared + */ + public AHashMap lookupMeta(Symbol sym) { + AHashMap env=getEnvironment(); + if (env.containsKey(sym)) { + return getMetadata().get(sym,Maps.empty()); + } + AccountStatus as = getAliasedAccount(env); + if (as==null) return null; + + env=as.getEnvironment(); + if (env.containsKey(sym)) { + return as.getMetadata().get(sym,Maps.empty()); + } + return null; + } + + /** + * Looks up Metadata for the given symbol in this context + * @param address Address to use for lookup (may pass null for current environment) + * @param sym Symbol to look up + * @return Metadata for given symbol (may be empty) or null if undeclared + */ + public AHashMap lookupMeta(Address address,Symbol sym) { + if (address==null) return lookupMeta(sym); + AccountStatus as=getAccountStatus(address); + if (as==null) return null; + AHashMap env=as.getEnvironment(); + if (env.containsKey(sym)) { + return as.getMetadata().get(sym,Maps.empty()); + } + return null; + } + + /** + * Looks up the account the defines a given Symbol + * @param sym Symbol to look up + * @param address Address to look up in first instance (null for current address). + * @return AccountStatus for given symbol (may be empty) or null if undeclared + */ + public AccountStatus lookupDefiningAccount(Address address,Symbol sym) { + AccountStatus as=(address==null)?getAccountStatus():getAccountStatus(address); + if (as==null) return null; + AHashMap env=as.getEnvironment(); + if (env.containsKey(sym)) { + return as; + } + + as = getAliasedAccount(env); + if (as==null) return null; + + env=as.getEnvironment(); + if (env.containsKey(sym)) { + return as; + } + return null; + } + + /** + * Looks up value for the given symbol in this context + * @param sym Symbol to look up + * @return Value for the given symbol or null if undeclared + */ + public ACell lookupValue(Symbol sym) { + AHashMap env=getEnvironment(); + + // Lookup in current environment first + MapEntry me=env.getEntry(sym); + if (me!=null) { + return me.getValue(); + } + + AccountStatus as = getAliasedAccount(env); + if (as==null) return null; + return as.getEnvironment().get(sym); + } + + /** + * Looks up value for the given symbol in this context + * @param address Address to look up in (may be null for current environment) + * @param sym Symbol to look up + * @return Value for the given symbol or null if undeclared + */ + public ACell lookupValue(Address address,Symbol sym) { + if (address==null) return lookupValue(sym); + AccountStatus as=getAccountStatus(address); + if (as==null) return null; + AHashMap env=as.getEnvironment(); + return env.get(sym); + } + + /** + * Looks up an environment entry for a specific address without consuming juice. + * + * @param address Address of Account in which to look up entry + * @param sym Symbol to look up + * @return Environment entry + */ + public MapEntry lookupDynamicEntry(Address address,Symbol sym) { + AccountStatus as=getAccountStatus(address); + if (as==null) return null; + return lookupDynamicEntry(as,sym); + } + + + + private MapEntry lookupDynamicEntry(AccountStatus as,Symbol sym) { + // Get environment for Address, or default to initial environment + AHashMap env = (as==null)?Core.ENVIRONMENT:as.getEnvironment(); + + + MapEntry result=env.getEntry(sym); + + if (result==null) { + AccountStatus aliasAccount=getAliasedAccount(env); + result = lookupAliasedEntry(aliasAccount,sym); + } + return result; + } + + private MapEntry lookupAliasedEntry(AccountStatus as,Symbol sym) { + if (as==null) return null; + AHashMap env = as.getEnvironment(); + return env.getEntry(sym); + } + + /** + * Gets the account status for the current Address + * + * @return AccountStatus object, or null if not found + */ + public AccountStatus getAccountStatus() { + Address a=getAddress(); + + // Possible we don't have an Address (e.g. in a Query) + if (a==null) return null; + + return chainState.state.getAccount(a); + } + + /** + * Looks up the account for an Symbol alias in the given environment. + * @param env + * @param path An alias path + * @return AccountStatus for the alias, or null if not present + */ + private AccountStatus getAliasedAccount(AHashMap env) { + // TODO: alternative core accounts + return getCoreAccount(); + } + + private AccountStatus getCoreAccount() { + return getState().getAccount(Init.CORE_ADDRESS); + } + + /** + * Gets the holdings map for the current account. + * @return Map of holdings, or null if the current account does not exist. + */ + public ABlobMap getHoldings() { + AccountStatus as=getAccountStatus(getAddress()); + if (as==null) return null; + return as.getHoldings(); + } + + public long getBalance() { + return getBalance(getAddress()); + } + + public long getBalance(Address address) { + AccountStatus as=getAccountStatus(address); + if (as==null) return 0L; + return as.getBalance(); + } + + /** + * Gets the caller of the currently executing context. + * + * Will be null if this context was not called from elsewhere (e.g. is an origin context) + * @return Caller of the currently executing context + */ + public Address getCaller() { + return chainState.caller; + } + + /** + * Gets the address of the currently executing Account. May be the current actor, or the address of the + * account that executed this transaction if no Actors have been called. + * + * @return Address of the current account, cannot be null, must be a valid existing account + */ + public Address getAddress() { + return chainState.address; + } + + /** + * Gets the result from this context. Throws an Error if the context return value is exceptional. + * + * @return Result value from this Context. + */ + public T getResult() { + if (exception!=null) { + String msg = "Can't get result with exceptional value: "+exception; + if (exception instanceof ErrorValue) { + ErrorValue ev=(ErrorValue)exception; + msg=msg+"\n"+ev.getTrace(); + } + throw new Error(msg); + } + return (T) result; + } + + /** + * Gets the resulting value from this context. May be either exceptional or a normal result. + * @return Either the normal result, or an AExceptional instance + */ + public Object getValue() { + if (exception!=null) return exception; + return result; + } + + /** + * Gets the exceptional value from this context. Throws an Error is the context return value is normal. + * @return an AExceptional instance + */ + public AExceptional getExceptional() { + if (exception==null) throw new Error("Can't get exceptional value for context with result: "+exception); + return exception; + } + + /** + * Returns a context updated with the specified result. + * + * Context may become exceptional depending on the result type. + * + * @param Result type + * @param value Value + * @return Context updated with the specified result. + */ + @SuppressWarnings("unchecked") + public Context withResult(ACell value) { + result=(T)value; + exception=null; + return (Context) this; + } + + /** + * Updates this context with a given value, which may either be a normal result or exceptional value + * @param Result type + * @param value Value + * @return Context updated with the specified result value. + */ + @SuppressWarnings("unchecked") + public Context withValue(Object value) { + if (value instanceof AExceptional) { + exception=(AExceptional)value; + result=null; + } else { + result = (T)value; + exception=null; + } + return (Context) this; + } + + public Context withResult(long gulp,R value) { + if (!checkJuice(gulp)) return withJuiceError(); + juice=juice-gulp; + + return withResult(value); + } + + /** + * Returns this context with a JUICE error, consuming all juice. + * @param Result type + * @return Exceptional Context signalling JUICE error. + */ + public Context withJuiceError() { + // set juice to zero. Can't consume more that we have! + this.juice=0; + return withError(ErrorCodes.JUICE,"Out of juice!"); + } + + @SuppressWarnings("unchecked") + public Context withException(AExceptional exception) { + //return (Context) new Context(chainState,juice,localBindings,exception,depth,true); + this.exception=exception; + this.result=null; + return (Context) this; + } + + public Context withException(long gulp,AExceptional value) { + if (!checkJuice(gulp)) return withJuiceError(); + juice=juice-gulp; + return withException(value); + //if ((this.result==value)&&(this.juice==newJuice)) return (Context) this; + //return (Context) new Context(chainState,newJuice,localBindings,value,depth,true); + } + + /** + * Updates the environment of this execution context. This changes the environment stored in the + * state for the current Address. + * + * @param newEnvironment + * @return Updated Context with the given dynamic environment + */ + private Context withEnvironment(AHashMap newEnvironment) { + ChainState cs=chainState.withEnvironment(newEnvironment); + return withChainState(cs); + } + + /** + * Updates the environment of this execution context. This changes the environment stored in the + * state for the current Address. + * + * @param newEnvironment + * @return Updated Context with the given dynamic environment + */ + private Context withEnvironment(AHashMap newEnvironment, AHashMap> newMeta) { + ChainState cs=chainState.withEnvironment(newEnvironment,newMeta); + return withChainState(cs); + } + + private Context withChainState(ChainState newChainState) { + //if (chainState==newChainState) return this; + //return create(newChainState,juice,localBindings,result,depth); + chainState=newChainState; + return this; + } + + /** + * Executes an Op within this context, returning an updated context. + * + * @param Return type of the Op + * @param op Op to execute + * @return Updated Context + */ + @SuppressWarnings("unchecked") + public Context execute(AOp op) { + // execute op with adjusted depth + int savedDepth=getDepth(); + Context> ctx =this.withDepth(savedDepth+1); + if (ctx.isExceptional()) return (Context) ctx; // depth error, won't have modified depth + + Context rctx=op.execute(ctx); + + // reset depth after execution. + rctx=rctx.withDepth(savedDepth); + return rctx; + } + + /** + * Executes an Op at the top level in a new forked Context. Handles top level halt, recur and return. + * + * Returning an updated context containing the result or an exceptional error. + * + * @param Return type of the Op + * @param op Op to execute + * @return Updated Context + */ + public Context run(AOp op) { + // Security: run in fork + Context ctx=fork().execute(op); + + // must handle state results like halt, rollback etc. + return handleStateResults(ctx,false); + } + + /** + * Executes a form at the top level in a new forked Context. Handles top level halt, recur and return. + * + * Returning an updated context containing the result or an exceptional error. + * + * @param Return type of the Op + * @param code Code to execute + * @return Updated Context + */ + public Context run(ACell code) { + Context ctx=fork().eval(code); + + // must handle state results like halt, rollback etc. + return handleStateResults(ctx,false); + } + + + /** + * Invokes a function within this context, returning an updated context. + * + * Handles function recur and return values. + * + * Keeps depth constant upon return. + * + * @param Return type of the function + * @param fn Function to execute + * @param args Arguments for function + * @return Updated Context + */ + @SuppressWarnings("unchecked") + public Context invoke(AFn fn, ACell... args) { + // Note: we don't adjust depth here because execute(...) does it for us in the function body + Context ctx = fn.invoke((Context) this,args); + + if (ctx.isExceptional()) { + // Need an Object because maybe mutating later + Object v=ctx.getExceptional(); + + // recur as many times as needed + while (v instanceof ATrampoline) { + // don't recur if this is the recur function itself + + if (v instanceof RecurValue) { + if (fn==Core.RECUR) break; + RecurValue rv = (RecurValue) v; + ACell[] newArgs = rv.getValues(); + ctx = fn.invoke((Context) ctx,newArgs); + v = ctx.getValue(); + } else if (v instanceof TailcallValue) { + if (fn==Core.TAILCALL_STAR) break; + TailcallValue rv=(TailcallValue)v; + ACell[] newArgs = rv.getValues(); + + // redirect function and invoke + fn = (AFn) rv.getFunction(); + ctx = fn.invoke((Context) ctx,newArgs); + v = ctx.getValue(); + } + } + + // unwrap return value if necessary + if ((v instanceof ReturnValue)&&(!(fn==Core.RETURN))) { + v = ((ReturnValue) v).getValue(); + + // unwrap result + return ctx.withResult((R)v); + } + + if (v instanceof ErrorValue) { + ErrorValue ev=(ErrorValue)v; + ev.addTrace("In function: "+RT.str(fn)); + } + } + return ctx; + } + + /** + * Execute an op, and bind the result to the given binding form in the lexical environment + * + * Binding form may be a destructuring form + * @param bindingForm Binding form + * @param op Op to execute to get binding values + * + * @param Result type of Context + * @return Context with local bindings updated + */ + @SuppressWarnings("unchecked") + public Context executeLocalBinding(ACell bindingForm, AOp op) { + Context ctx=this.execute(op); + if (ctx.isExceptional()) return ctx; + return (Context) ctx.updateBindings(bindingForm, ctx.getResult()); + } + + /** + * Updates local bindings with a given binding form + * + * @param Result type of Context + * @param bindingForm Binding form + * @param args Arguments to bind + * @return Non-exceptional Context with local bindings updated, or an exceptional result if bindings fail + */ + @SuppressWarnings("unchecked") + public Context updateBindings(ACell bindingForm, Object args) { + // Clear any exceptional status + Context ctx=this.withValue(null); + + if (bindingForm instanceof Symbol) { + Symbol sym=(Symbol)bindingForm; + if (sym.equals(Symbols.UNDERSCORE)) return ctx; + // TODO: confirm must be an ACell at this point? + return withLocalBindings(localBindings.conj((ACell)args)); + } else if (bindingForm instanceof AVector) { + AVector v=(AVector)bindingForm; + long vcount=v.count(); // count of binding form symbols (may include & etc.) + + // Count the arguments, exit with a CAST error if args are not sequential + Long argCount=RT.count(args); + if (argCount==null) return ctx.withError(ErrorCodes.CAST, "Trying to destructure an argument that is not a sequential collection"); + + boolean foundAmpersand=false; + for (long i=0; i rest=RT.vec(args).slice(i,consumeCount); // TODO: cost of this? + ctx= ctx.updateBindings(v.get(i+1), rest); + if(ctx.isExceptional()) return ctx; + + // mark ampersand as found, and skip to next binding form (i.e. past the variadic symbol following &) + foundAmpersand=true; + i++; + } else { + // just a regular binding + long argIndex=foundAmpersand?(argCount-(vcount-i)):i; + if (argIndex>=argCount) return ctx.withArityError("Insufficient arguments ("+argCount+") for binding form: "+bindingForm); + ctx=ctx.updateBindings(bf,RT.nth(args, argIndex)); + if(ctx.isExceptional()) return ctx; + } + } + + // at this point, should have consumed all bindings + if (!foundAmpersand) { + if (vcount!=argCount) { + return ctx.withArityError("Expected "+vcount+" arguments but got "+argCount+" for binding form: "+bindingForm); + } + } + } else { + return ctx.withCompileError("Don't understand binding form of type: "+RT.getType(bindingForm)); + } + // return + return ctx; + } + + @Override + public void print(StringBuilder sb) { + sb.append("{"); + sb.append(":juice "+juice); + sb.append(','); + sb.append(":result "); + Utils.print(sb,result); + sb.append(','); + sb.append(":state "); + getState().print(sb); + sb.append("}"); + } + + public AVector getLocalBindings() { + return localBindings; + } + + /** + * Updates this Context with new local bindings. Doesn't affact result state (exceptional or otherwise) + * @param Return type of Context + * @param newBindings New local bindings map to use. + * @return Updated context + */ + @SuppressWarnings("unchecked") + public Context withLocalBindings(AVector newBindings) { + //if (localBindings==newBindings) return (Context) this; + //return create(chainState,juice,newBindings,(R)result,depth); + localBindings=newBindings; + return (Context) this; + } + + /** + * Gets the account status record, or null if not found + * + * @param address Address of account + * @return AccountStatus for the specified address, or null if the account does not exist + */ + public AccountStatus getAccountStatus(Address address) { + return getState().getAccount(address); + } + + public int getDepth() { + return depth; + } + + public Address getOrigin() { + return chainState.origin; + } + + /** + * Defines a value in the environment of the current address + * @param key Symbol of the mapping to create + * @param value Value to define + * @return Updated context with symbol defined in environment + */ + public Context define(Symbol key, ACell value) { + AHashMap env = getEnvironment(); + AHashMap newEnvironment = env.assoc(key, value); + + return withEnvironment(newEnvironment); + } + + /** + * Defines a value in the environment of the current address, updating the metadata + * + * @param syn Syntax Object to define, containing a Symbol value + * @param value Value to set of the given Symbol + * @return Updated context with symbol defined in environment + */ + public Context defineWithSyntax(Syntax syn, ACell value) { + Symbol key=syn.getValue(); + AHashMap env = getEnvironment(); + AHashMap newEnvironment = env.assoc(key, value); + AHashMap> newMeta = getMetadata().assoc(key, syn.getMeta()); + + return withEnvironment(newEnvironment,newMeta); + } + + + /** + * Removes a definition mapping in the environment of the current address + * @param key Symbol of the environment mapping to remove + * @return Updated context with symbol definition removed from the environment, or this context if unchanged + */ + public Context undefine(Symbol key) { + AHashMap m = getEnvironment(); + AHashMap newEnvironment = m.dissoc(key); + AHashMap> newMeta = getMetadata().dissoc(key); + + return withEnvironment(newEnvironment,newMeta); + } + + /** + * Expand and compile a form in this Context. + * + * @param Return type of compiled op + * @param form Form to expand and compile + * @return Updated Context with compiled Op as result + */ + public Context> expandCompile(ACell form) { + // run compiler with adjusted depth + int saveDepth=getDepth(); + Context> rctx =this.withDepth(saveDepth+1); + if (rctx.isExceptional()) return rctx; // depth error, won't have modified depth + + // EXPAND AND COMPILE + rctx = Compiler.expandCompile(form, rctx); + + // reset depth after expansion and compilation, unless there is an error + rctx=rctx.withDepth(saveDepth); + + return rctx; + } + + /** + * Compile a form in this Context. Form must already be fully expanded to a Syntax Object + * + * @param Return type of compiled op + * @param expandedForm Form to compile + * @return Updated Context with compiled Op as result + */ + public Context> compile(ACell expandedForm) { + // Save an adjust depth + int saveDepth=getDepth(); + Context> rctx =this.withDepth(saveDepth+1); + if (rctx.isExceptional()) return rctx; // depth error + + // Save Compiler state + CompilerState savedCompilerState=getCompilerState(); + + // COMPILE + rctx = Compiler.compile(expandedForm, rctx); + + if (rctx.isExceptional()) { + AExceptional ex=rctx.getExceptional(); + if (ex instanceof ErrorValue) { + ErrorValue ev=(ErrorValue)ex; + // TODO: SECURITY: DoS limits + //String msg = "Compiling: Syntax Object with datum of type "+Utils.getClassName(expandedForm); + String msg = "Compiling:"+ expandedForm; + //String msg = "Compiling: "+expandedForm; + ev.addTrace(msg); + } + } + + // restore depth and return + rctx=rctx.withDepth(saveDepth); + rctx=rctx.withCompilerState(savedCompilerState); + return rctx; + } + + /** + * Executes a form in the current context. + * + * Ops are executed directly. + * Other forms will be expanded and compiled before execution, unless *lang* is set, in which case they will + * be executed via the *lang* function. + * + * @param Return type of evaluation + * @param form Form to evaluate + * @return Context containing the result of evaluating the specified form + */ + @SuppressWarnings("unchecked") + public Context eval(ACell form) { + Context> ctx=(Context>) this; + AOp op; + + if (form instanceof AOp) { + op=(AOp)form; + } else { + AFn lang=RT.ensureFunction(lookupValue(Symbols.STAR_LANG)); + if (lang!=null) { + // Execute *lang* function, but increment depth just in case + int saveDepth=ctx.getDepth(); + ctx=ctx.withDepth(saveDepth+1); + if (ctx.isExceptional()) return (Context) ctx; + Context rctx = ctx.invoke(lang,form); + return rctx.withDepth(saveDepth); + } else { + ctx=expandCompile(form); + if (ctx.isExceptional()) return (Context) ctx; + op=ctx.getResult(); + ctx=ctx.withResult(null); // clear result for execution + } + } + return ctx.execute(op); + } + + /** + * Evaluates a form as another Address. + * + * Causes TRUST error if the Address is not controlled by the current address. + * @param Result type + * @param address Address of Account in which to evaluate + * @param form Form to evaluate + * @return Updated Context + */ + public Context evalAs(Address address, ACell form) { + Address caller=getAddress(); + if (caller.equals(address)) return eval(form); + AccountStatus as=this.getAccountStatus(address); + if (as==null) return withError(ErrorCodes.NOBODY,"Address does not exist: "+address); + + Address controller=as.getController(); + if (controller==null) return withError(ErrorCodes.TRUST,"Cannot control address with nil controller set: "+address); + + boolean canControl=false; + + // Run eval in a forked context + Context ctx=this.fork(); + if (controller.equals(getAddress())) { + canControl=true; + } else { + AccountStatus controlAccount=this.getAccountStatus(controller); + if (controlAccount==null) return ctx.withError(ErrorCodes.TRUST,"Cannot control address because controller does not exist: "+controller); + if (controlAccount.isActor()) { + // (call target amount (receive-coin source amount nil)) + ctx=ctx.actorCall(controller,DEFAULT_OFFER,Symbols.CHECK_TRUSTED_Q,caller,null,address); + if (ctx.isExceptional()) return ctx; + canControl=RT.bool(ctx.getResult()); + } + } + + if (!canControl) return ctx.withError(ErrorCodes.TRUST,"Cannot control address: "+address); + + // SECURITY: eval with a context switch + final Context exContext=Context.create(getState(), juice, EMPTY_BINDINGS, null, depth+1, getOrigin(),caller, address,0,log,null); + + final Context rContext=exContext.eval(form); + // SECURITY: must handle results as if returning from an actor call + return handleStateResults(rContext,false); + } + + /** + * Executes code as if run in the current account, but always discarding state changes. + * @param Result type + * @param form Code to execute. + * @return Context updated with only query result and juice consumed + */ + public Context query(ACell form) { + Context ctx=this.fork(); + + // adjust depth. May be exceptional if depth limit exceeded + ctx=ctx.withDepth(depth+1); + + // eval in current account if everything OK + if (!ctx.isExceptional()) { + ctx=ctx.eval(form); + } + + // handle results including state rollback. Will propagate any errors. + return handleQueryResult(ctx); + } + + /** + * Executes code as if run in the specified account, but always discarding state changes. + * @param Result type + * @param address Address of Account in which to execute the query + * @param form Code to execute. + * @return Context updated with only query result and juice consumed + */ + public Context queryAs(Address address, ACell form) { + // chainstate with the target address as origin. + ChainState cs=ChainState.create(getState(),address,null,address,DEFAULT_OFFER); + Context ctx=Context.create(cs, juice, EMPTY_BINDINGS, null, depth,log,null); + ctx=ctx.evalAs(address, form); + return handleQueryResult(ctx); + } + + /** + * Just take result and juice from query. Log and state not kept. + * @param + * @param ctx + * @return + */ + protected Context handleQueryResult(Context ctx) { + this.juice=ctx.getJuice(); + return this.withValue(ctx.result); + } + + /** + * Compiles a sequence of forms in the current context. + * Returns a vector of ops in the updated Context. + * + * Maintains depth. + * + * @param Return type of compiled op. + * @param forms A sequence of forms to compile + * @return Updated context with vector of compiled forms + */ + public Context>> compileAll(ASequence forms) { + Context>> rctx = Compiler.compileAll(forms, this); + return rctx; + } + +// public Context adjustDepth(int delta) { +// int newDepth=Math.addExact(depth,delta); +// return withDepth(newDepth); +// } + + /** + * Changes the depth of this context. Returns exceptional result if depth limit exceeded. + * @param Result type + * @param newDepth New depth value + * @return Updated context with new depth set + */ + @SuppressWarnings("unchecked") + Context withDepth(int newDepth) { + if (newDepth==depth) return (Context) this; + if ((newDepth<0)||(newDepth>Constants.MAX_DEPTH)) return withError(ErrorCodes.DEPTH,"Invalid depth: "+newDepth); + depth=newDepth; + return (Context)this; + } + + @SuppressWarnings("unchecked") + public Context withJuice(long newJuice) { + juice=newJuice; + return (Context) this; + } + + @SuppressWarnings("unchecked") + public Context withCompilerState(CompilerState comp) { + compilerState=comp; + return (Context) this; + } + + /** + * Tests if this Context holds an exceptional result. + * + * Ops should cancel and return exceptional results unchanged, unless they can handle them. + * @return true if context has an exceptional value, false otherwise + */ + public boolean isExceptional() { + return exception!=null; + } + + /** + * Tests if an Address is valid, i.e. refers to an existing Account + * + * @param address Address to check. May be null + * @return true if Account exists, false otherwise + */ + public boolean isValidAccount(Address address) { + if (address==null) return false; + return getAccountStatus(address)!=null; + } + + /** + * Tests if this Context's current status contains an Error. Errors are an uncatchable subset of Exceptions. + * + * @return true if context has an Error value, false otherwise + */ + public boolean isError() { + return (exception!=null)&&(exception instanceof ErrorValue); + } + + /** + * Transfers funds from the current address to the target. + * + * Uses no juice + * + * @param target Target Address, will be created if does not already exist. + * @param amount Amount to transfer, must be between 0 and Amount.MAX_VALUE inclusive + * @return Context with sent amount if the transaction succeeds, or an exceptional value if the transfer fails + */ + public Context transfer(Address target, long amount) { + if (amount<0) return withError(ErrorCodes.ARGUMENT,"Can't transfer a negative amount"); + if (amount>Constants.MAX_SUPPLY) return withError(ErrorCodes.ARGUMENT,"Can't transfer an amount beyond maximum limit"); + + AVector accounts=getState().getAccounts(); + + Address source=getAddress(); + long sourceIndex=source.longValue(); + AccountStatus sourceAccount=accounts.get(sourceIndex); + + long currentBalance=sourceAccount.getBalance(); + if (currentBalance=accounts.count()) { + return this.withError(ErrorCodes.NOBODY,"Target account for transfer "+target+" does not exist"); + } + AccountStatus targetAccount=accounts.get(targetIndex); + + if (targetAccount.isActor()) { + // (call target amount (receive-coin source amount nil)) + // SECURITY: actorCall must do fork to preserve this + Context actx=this.fork(); + actx=actorCall(target,amount,Symbols.RECEIVE_COIN,source,CVMLong.create(amount),null); + if (actx.isExceptional()) return actx; + + // TODO: Should return value be change in balance? or amount offered? + Long sent=currentBalance-actx.getBalance(source); + return actx.withResult(CVMLong.create(sent)); + } else { + // must be a user account + long oldTargetBalance=targetAccount.getBalance(); + long newTargetBalance=oldTargetBalance+amount; + AccountStatus newTargetAccount=targetAccount.withBalance(newTargetBalance); + accounts=accounts.assoc(targetIndex, newTargetAccount); + + // SECURITY: new context with updated accounts + Context result=withChainState(chainState.withAccounts(accounts)).withResult(CVMLong.create(amount)); + + return result; + } + + } + + /** + * Transfers memory allowance from the current address to the target. + * + * Uses no juice + * + * @param target Target Address, must already exist + * @param amountToSend Amount of memory to transfer, must be between 0 and Amount.MAX_VALUE inclusive + * @return Context with a null result if the transaction succeeds, or an exceptional value if the transfer fails + */ + public Context transferMemoryAllowance(Address target, CVMLong amountToSend) { + long amount=amountToSend.longValue(); + if (amount<0) return withError(ErrorCodes.ARGUMENT,"Can't transfer a negative aloowance amount"); + if (amount>Constants.MAX_SUPPLY) return withError(ErrorCodes.ARGUMENT,"Can't transfer an allowance amount beyond maximum limit"); + + AVector accounts=getState().getAccounts(); + + Address source=getAddress(); + long sourceIndex=source.longValue(); + AccountStatus sourceAccount=accounts.get(sourceIndex); + + long currentBalance=sourceAccount.getMemory(); + if (currentBalance=accounts.count()) { + return withError(ErrorCodes.NOBODY,"Cannot transfer memory allowance to non-existent account: "+target); + } + AccountStatus targetAccount=accounts.get(targetIndex); + + long newTargetBalance=targetAccount.getMemory()+amount; + AccountStatus newTargetAccount=targetAccount.withMemory(newTargetBalance); + accounts=accounts.assoc(targetIndex, newTargetAccount); + + // SECURITY: new context with updated accounts + Context result=withChainState(chainState.withAccounts(accounts)).withResult(amountToSend); + return result; + } + + /** + * Sets the memory allowance for the current account, buying / selling from the pool as necessary to + * ensure the correct final allowance + * @param allowance New memory allowance + * @return Context indicating the price paid for the allowance change (may be zero or negative for refund) + */ + public Context setMemory(long allowance) { + AVector accounts=getState().getAccounts(); + if (allowance<0) return withError(ErrorCodes.ARGUMENT,"Can't transfer a negative aloowance amount"); + if (allowance>Constants.MAX_SUPPLY) return withError(ErrorCodes.ARGUMENT,"Can't transfer an allowance amount beyond maximum limit"); + + Address source=getAddress(); + long sourceIndex=source.longValue(); + AccountStatus sourceAccount=accounts.get(sourceIndex); + + long current=sourceAccount.getMemory(); + long balance=sourceAccount.getBalance(); + long delta=allowance-current; + if (delta==0L) return this.withResult(CVMLong.ZERO); + + AccountStatus pool=getState().getAccount(Init.MEMORY_EXCHANGE_ADDRESS); + + try { + long poolAllowance=pool.getMemory(); + long poolBalance=pool.getBalance(); + long price = Economics.swapPrice(delta, poolAllowance,poolBalance); + if (price>balance) { + return withError(ErrorCodes.FUNDS,"Cannot afford allowance, would cost: "+price); + } + sourceAccount=sourceAccount.withBalances(balance-price, allowance); + pool=pool.withBalances(poolBalance+price, poolAllowance-delta); + + // Update accounts + AVector newAccounts=accounts.assoc(sourceIndex, sourceAccount); + newAccounts=newAccounts.assoc(Init.MEMORY_EXCHANGE_ADDRESS.longValue(),pool); + + return withChainState(chainState.withAccounts(newAccounts)).withResult(null); + } catch (IllegalArgumentException e) { + return withError(ErrorCodes.FUNDS,"Cannot trade allowance: "+e.getMessage()); + } + } + + /** + * Accepts offered funds for the given address. + * + * STATE error if offered amount is insufficient. ARGUMENT error if acceptance is negative. + * + * @param Type of result + * @param amount Amount to accept + * @return Updated context, with long amount accepted as result + */ + @SuppressWarnings("unchecked") + public Context acceptFunds(long amount) { + if (amount<0L) return this.withError(ErrorCodes.ARGUMENT,"Negative accept argument"); + if (amount==0L) return this.withResult(Juice.ACCEPT, (R)CVMLong.ZERO); + + long offer=getOffer(); + if (amount>offer) return this.withError(ErrorCodes.STATE,"Insufficient offered funds"); + + State state=getState(); + Address addr=getAddress(); + long balance=state.getBalance(addr); + state=state.withBalance(addr,balance+amount); + + // need to update both state and offer + ChainState cs=chainState.withStateOffer(state,offer-amount); + Context ctx=this.withChainState(cs); + + return (Context) ctx.withResult(Juice.ACCEPT, CVMLong.create(amount)); + } + + /** + * Executes a call to an Actor. Utility function which convert a java String function name + * + * @param Return type of Actor call + * @param target Target Actor address + * @param offer Amount of Convex Coins to offer in Actor call + * @param functionName Symbol of function name defined by Actor + * @param args Arguments to Actor function invocation + * @return Context with result of Actor call (may be exceptional) + */ + public Context actorCall(Address target, long offer, String functionName, ACell... args) { + return actorCall(target,offer,Symbol.create(functionName),args); + } + + /** + * Executes a call to an Actor. + * + * @param Return type of Actor call + * @param target Target Actor address + * @param offer Amount of Convex Coins to offer in Actor call + * @param functionName Symbol of function name defined by Actor + * @param args Arguments to Actor function invocation + * @return Context with result of Actor call (may be exceptional) + */ + public Context actorCall(Address target, long offer, ACell functionName, ACell... args) { + // SECURITY: set up state for actor call + State state=getState(); + Symbol sym=RT.ensureSymbol(functionName); + AccountStatus as=state.getAccount(target); + if (as==null) return this.withError(ErrorCodes.NOBODY,"Actor Account does not exist: "+target); + + // Handling for non-zero offers. + // SECURITY: Subtract offer from balance first so we don't have double-spend issues! + if (offer>0L) { + Address senderAddress=getAddress(); + AccountStatus cas=state.getAccount(senderAddress); + long balance=cas.getBalance(); + if (balance fn = as.getCallableFunction(sym); + + if (fn == null) { + return this.withError(ErrorCodes.STATE, "Value defined in account " + target + " is not a callable function: " + sym); + } + + // Ensure we create a forked Context for the Actor call + final Context exContext=forkActorCall(state, target, offer); + + // INVOKE ACTOR FUNCTION + final Context rctx=exContext.invoke(fn,args); + + ErrorValue ev=rctx.getError(); + if (ev!=null) { + ev.addTrace("Calling Actor "+target+" with function ("+sym+" ...)"); + } + + // SECURITY: must handle state transitions in results correctly + // calling handleStateReturns on 'this' to ensure original values are restored + return handleStateResults(rctx,false); + } + + /** + * Create new forked Context for execution of Actor call. + * SECURITY: Increments depth, will be restored in handleStateResults + * SECURITY: Must change address to the target Actor address. + * SECURITY: Must change caller to current address. + * @param + * @param state for forked context. + * @param target Target actor call address, will become new *address* for context + * @param offer Offer amount for actor call. Must have been pre-subtracted from caller account. + * @return + */ + private Context forkActorCall(State state, Address target, long offer) { + return Context.create(state, juice, EMPTY_BINDINGS, (R)null, depth+1, getOrigin(),getAddress(), target,offer, log,null); + } + + /** + * Handle results at the end of an execution boundary (actor call, transaction etc.) + * @param + * @param returnContext + * @param rollback + * @return + */ + @SuppressWarnings("unchecked") + private Context handleStateResults(Context returnContext, boolean rollback) { + /** Return value */ + Object rv; + if (returnContext.isExceptional()) { + // SECURITY: need to handle exceptional states correctly + AExceptional ex=returnContext.getExceptional(); + if (ex instanceof RollbackValue) { + // roll back state to before Actor call + // Note: this will also refund unused offer. + rollback=true; + rv=((RollbackValue)ex).getValue(); + } else if (ex instanceof HaltValue) { + rv=((HaltValue)ex).getValue(); + } else if (ex instanceof ErrorValue) { + // OK to pass through error, but need to roll back state changes + rollback=true; + rv=ex; + } else if (ex instanceof ReturnValue) { + // Normally doesn't happen (invoke catches this) + // but might in a user transaction. Treat as a Halt. + rv=((ReturnValue)ex).getValue(); + } else { + rollback=true; + String msg; + if (ex instanceof ATrampoline) { + msg="attempt to recur or tail call outside of a function body"; + } if (ex instanceof Reduced) { + msg="reduced used outside of a reduce operation"; + } else { + msg="Unhandled Exception with Code:"+ex.getCode(); + } + rv=ErrorValue.create(ErrorCodes.EXCEPTION, msg); + } + } else { + rv=returnContext.getResult(); + } + + final Address address=getAddress(); // address we are returning to + State returnState; + + if (rollback) { + returnState=this.getState(); + } else { + // take state from the returning context + returnState=returnContext.getState(); + + // Take log from returning context + log=returnContext.getLog(); + + // Refund offer + // Not necessary if rolling back to initial context before offer was subtracted + long refund=returnContext.getOffer(); + if (refund>0) { + // we need to refund caller + AccountStatus cas=returnState.getAccount(address); + long balance=cas.getBalance(); + cas=cas.withBalance(balance+refund); + returnState=returnState.putAccount(address, cas); + } + } + // Rebuild context for the current execution + // SECURITY: must restore origin,depth,caller,address,local bindings, offer + + Context result=this.withState(returnState); + result.juice=returnContext.juice; + result=this.withValue(rv); + return result; + } + + /** + * Deploys an Actor in this context. + * + * Argument argument must be an Actor generation code, which will be evaluated in the new Actor account + * to initialise the Actor + * + * Result will contain the new Actor address if successful, an exception otherwise. + * + * @param code Actor initialisation code + * @return Updated Context with Actor deployed, or an exceptional result + */ + public Context
deployActor(ACell code) { + final State initialState=getState(); + + // deploy initial contract state to next address + Address address=initialState.nextAddress(); + State stateSetup=initialState.tryAddActor(); + + // Deployment execution context with forked context and incremented depth + final Context
exContext=Context.create(stateSetup, juice, EMPTY_BINDINGS, null, depth+1, getOrigin(),getAddress(), address,DEFAULT_OFFER,log,null); + final Context
rctx=exContext.eval(code); + + Context
result=this.handleStateResults(rctx,false); + if (result.isExceptional()) return result; + + return result.withResult(Juice.DEPLOY_CONTRACT,address); + } + + /** + * Create a new Account with a given AccountKey (may be null for actors etc.) + * @param key New Account Key + * @return Updated context with new Account added + */ + public Context
createAccount(AccountKey key) { + final State initialState=getState(); + Address address=initialState.nextAddress(); + AVector accounts=initialState.getAccounts(); + AccountStatus as=AccountStatus.create(0L, key); + accounts=accounts.conj(as); + final State newState=initialState.withAccounts(accounts); + Context
rctx=this.withState(newState); + return rctx.withResult(address); + } + + @SuppressWarnings("unchecked") + public Context withError(Keyword error) { + return (Context) withError(ErrorValue.create(error)); + } + + public Context withError(Keyword errorCode,String message) { + return withError(ErrorValue.create(errorCode,Strings.create(message))); + } + + @SuppressWarnings("unchecked") + public Context withError(ErrorValue error) { + error.addLog(log); + return (Context) withException(error); + } + + + public Context withArityError(String message) { + return withError(ErrorCodes.ARITY,message); + } + + public Context withCompileError(String message) { + return withError(ErrorCodes.COMPILE,message); + } + + public Context withBoundsError(long index) { + return withError(ErrorCodes.BOUNDS,"Index: "+index); + } + + public Context withCastError(int argIndex, AType klass) { + return withError(ErrorCodes.CAST,"Can't convert argument at position "+(argIndex+1)+" to type "+klass); + } + + public Context withCastError(int argIndex, ACell[] args, AType klass) { + return withError(ErrorCodes.CAST,"Can't convert argument at position "+(argIndex+1)+" (with type "+RT.getType(args[argIndex])+ ") to type "+klass); + } + + public Context withCastError(ACell a, AType klass) { + return withError(ErrorCodes.CAST,"Can't convert value of type "+RT.getType(a)+ " to type "+klass); + } + + public Context withCastError(AType klass) { + return withError(ErrorCodes.CAST,"Can't convert value(s) to type "+klass); + } + + public Context withCastError(ACell a, String message) { + return withError(ErrorCodes.CAST,message); + } + + /** + * Gets the error code of this context's return value + * + * @return The ErrorType of the current exceptional value, or null if there is no error. + */ + public ACell getErrorCode() { + if (exception!=null) { + return exception.getCode(); + } + return null; + } + + /** + * Gets the Error from this Context, or null if not an Error + * + * @return The ErrorType of the current exceptional value, or null if there is no error. + */ + public ErrorValue getError() { + if (exception instanceof ErrorValue) { + return (ErrorValue)exception; + } + return null; + } + + public Context withAssertError(String message) { + return withError(ErrorCodes.ASSERT,message); + } + + public Context withFundsError(String message) { + return withError(ErrorCodes.FUNDS,message); + } + + public Context withArgumentError(String message) { + return withError(ErrorCodes.ARGUMENT,message); + } + + /** + * Gets the current timestamp for this context. The timestamp is the greatest timestamp + * of all blocks in consensus (including the currently executing block). + * + * @return Timestamp in milliseconds since UNIX epoch + */ + public CVMLong getTimeStamp() { + return getState().getTimeStamp(); + } + + /** + * Schedules an operation for the specified future timestamp. + * Handles integrity checks and schedule juice. + * + * @param time Timestamp at which to schedule the op. + * @param op Operation to schedule. + * @return Updated context, with scheduled time as the result + */ + public Context schedule(long time, AOp op) { + // check vs current timestamp + long timestamp=getTimeStamp().longValue(); + if (timestamp<0L) return withError(ErrorCodes.ARGUMENT); + if (time ctx=this.withChainState(chainState.withState(s)); + + return ctx.withResult(juice,CVMLong.create(time)); + } + + /** + * Sets the delegated stake on a specified peer to the specified level. + * May set to zero to remove stake. Stake will be capped by current balance. + * + * @param peerKey Peer Account key on which to stake + * @param newStake Amount to stake + * @return Context with amount of coins transferred to Peer as result (may be negative if stake withdrawn) + */ + @SuppressWarnings("unchecked") + public Context setDelegatedStake(AccountKey peerKey, long newStake) { + State s=getState(); + PeerStatus ps=s.getPeer(peerKey); + if (ps==null) return withError(ErrorCodes.STATE,"Peer does not exist for account key: "+peerKey); + if (newStake<0) return this.withArgumentError("Cannot set a negative stake"); + if (newStake>Constants.MAX_SUPPLY) return this.withArgumentError("Target stake out of valid Amount range"); + + Address myAddress=getAddress(); + long balance=getBalance(myAddress); + long currentStake=ps.getDelegatedStake(myAddress); + long delta=newStake-currentStake; + + if (delta==0) return (Context) this; // no change + + // need to check sufficient balance if increasing stake + if (delta>balance) return this.withFundsError("Insufficient balance ("+balance+") to increase Delegated Stake to "+newStake); + + // Final updates. Hopefully everything balances. SECURITY: test this. A lot. + PeerStatus updatedPeer=ps.withDelegatedStake(myAddress, newStake); + s=s.withBalance(myAddress, balance-delta); // adjust own balance + s=s.withPeer(peerKey, updatedPeer); // adjust peer + return withState(s).withResult(CVMLong.create(delta)); + } + + /** + * Sets the stake for a given Peer, transferring coins from the current address. + * @param peerKey Peer Account Key for which to update Stake + * @param newStake New stake for Peer + * @return Updated Context + */ + @SuppressWarnings("unchecked") + public Context setPeerStake(AccountKey peerKey, long newStake) { + State s=getState(); + PeerStatus ps=s.getPeer(peerKey); + if (ps==null) return withError(ErrorCodes.STATE,"Peer does not exist for account key: "+peerKey); + if (newStake<0) return this.withArgumentError("Cannot set a negative stake"); + if (newStake>Constants.MAX_SUPPLY) return this.withArgumentError("Target stake out of valid Amount range"); + + Address myAddress=getAddress(); + if (!ps.getController().equals(myAddress)) return withError(ErrorCodes.STATE,"Current address "+myAddress+" is not the controller of this peer account"); + + long balance=getBalance(myAddress); + long currentStake=ps.getPeerStake(); + long delta=newStake-currentStake; + + if (delta==0) return (Context) this; // no change + + // need to check sufficient balance if increasing stake + if (delta>balance) return this.withFundsError("Insufficient balance ("+balance+") to increase Peer Stake to "+newStake); + + // Final updates assuming everything OK. Hopefully everything balances. SECURITY: test this. A lot. + PeerStatus updatedPeer=ps.withPeerStake(newStake); + s=s.withBalance(myAddress, balance-delta); // adjust own balance + s=s.withPeer(peerKey, updatedPeer); // adjust peer + + return withState(s).withResult(CVMLong.create(delta)); + } + + + /** + * Creates a new peer with the specified stake. + * The accountKey must not be in the list of peers. + * The accountKey must be assigend to the current transaction address + * Stake must be greater than 0. + * Stake must be less than to the account balance. + * + * @param accountKey Peer Account key to create the PeerStatus + * @param initialStake Initial stake amount + * @return Context with final take set + */ + public Context createPeer(AccountKey accountKey, long initialStake) { + State s=getState(); + PeerStatus ps=s.getPeer(accountKey); + if (ps!=null) return withError(ErrorCodes.STATE,"Peer already exists for this account key: "+accountKey.toChecksumHex()); + if (initialStake<0) return this.withArgumentError("Cannot set a negative stake"); + if (initialStake == 0) return this.withArgumentError("Cannot create a peer with zero stake"); + if (initialStake>Constants.MAX_SUPPLY) return this.withArgumentError("Target stake out of valid Amount range"); + + Address myAddress=getAddress(); + + // TODO: SECURITY fix + // AccountStatus as=getAccountStatus(myAddress); + // if (!as.getAccountKey().equals(accountKey)) return this.withArgumentError("Cannot create a peer with a different account-key"); + + long balance=getBalance(myAddress); + if (initialStake>balance) return this.withFundsError("Insufficient balance ("+balance+") to assign an initial stake of "+initialStake); + + PeerStatus newPeerStatus = PeerStatus.create(myAddress, initialStake); + + // Final updates. Hopefully everything balances. SECURITY: test this. A lot. + s=s.withBalance(myAddress, balance-initialStake); // adjust own balance + s=s.withPeer(accountKey, newPeerStatus); // add peer + return withState(s); + } + + /** + * Sets peer data. + * + * @param peerKey Peer to set data for + * @param data Map of data to set for the peer + * @return Context with final peer data set + */ + @SuppressWarnings("unchecked") + public Context setPeerData(AccountKey peerKey, AMap data) { + State s=getState(); + + // get the callers account and account status + Address address = getAddress(); + AccountStatus as = getAccountStatus(address); + + AccountKey ak = as.getAccountKey(); + if (ak == null) return withError(ErrorCodes.STATE,"The account signing this transaction must have a public key"); + PeerStatus ps=s.getPeer(ak); + if (ps==null) return withError(ErrorCodes.STATE,"Peer does not exist for this account and account key: "+ak.toChecksumHex()); + if (!ps.getController().equals(address)) return withError(ErrorCodes.STATE,"Current address "+address+" is not the controller of this peer account"); + + Hash lastStateHash = s.getHash(); + // at the moment only :url is used in the data map + for (ACell key: data.keySet()) { + if (Keywords.URL.equals((Keyword) key)) { + AString url = (AString) data.get(Keywords.URL); + PeerStatus updatedPeer=ps.withHostname(url); + s=s.withPeer(ak, updatedPeer); // adjust peer + } + else { + return withArityError("invalid key name " + key.toString()); + } + } + // if no change just return the current context + if (lastStateHash.equals(s.getHash())){ + return (Context) this; + } + return withState(s); + } + + + + /** + * Sets the holding for a specified target account. Returns NOBODY exception if account does not exist. + * @param targetAddress Account address at which to set the holding + * @param value Value to set for the holding. + * @return Updated context + */ + public Context setHolding(Address targetAddress, ACell value) { + AccountStatus as=getAccountStatus(targetAddress); + if (as==null) return withError(ErrorCodes.NOBODY,"No account in which to set holding"); + as=as.withHolding(getAddress(), value); + return withAccountStatus(targetAddress,as); + } + + /** + * Sets the controller for the current Account + * @param Result type + * @param address New controller Address + * @return Context with current Account controller set + */ + public Context setController(Address address) { + AccountStatus as=getAccountStatus(); + as=as.withController(address); + return withAccountStatus(getAddress(),as); + + } + + /** + * Sets the public key for the current account + * @param Result type + * @param publicKey New Account Public Key + * @return Context with current Account Key set + */ + public Context setAccountKey(AccountKey publicKey) { + AccountStatus as=getAccountStatus(); + as=as.withAccountKey(publicKey); + return withAccountStatus(getAddress(),as); + } + + protected Context withAccountStatus(Address target, AccountStatus accountStatus) { + return withState(getState().putAccount(target, accountStatus)); + } + + /** + * Switches the context to a new address, creating a new execution context. Suitable for testing. + * @param Result type + * @param newAddress New Address to use. + * @return Result type of new Context + */ + public Context forkWithAddress(Address newAddress) { + return createFake(getState(),newAddress); + } + + /** + * Forks this context, creating a new copy of all local state + * @param Result type of new Context + * @return A new forked Context + */ + public Context fork() { + return new Context(chainState, juice, localBindings, null,depth, null,log,compilerState); + } + + @Override + public Blob createEncoding() { + throw new TODOException(); + } + + /** + * Appends a log entry for the current address. + * @param values Values to log + * @return Updated Context + */ + public Context appendLog(AVector values) { + Address addr=getAddress(); + AVector> log=this.log; + if (log==null) { + log=Vectors.empty(); + } + AVector entry = Vectors.of(addr,values); + log=log.conj(entry); + + this.log=log; + return this; + } + + /** + * Gets the log map for the current context. + * + * @return BlobMap of addresses to log entries created in the course of current execution context. + */ + public AVector> getLog() { + if (log==null) return Vectors.empty(); + return log; + } + + public Context lookupCNS(String name) { + Context ctx=this.fork(); + ctx=this.actorCall(Init.REGISTRY_ADDRESS, 0, Symbols.CNS_RESOLVE, Symbol.create(name)); + + return ctx; + } + + /** + * Expands a form with the default *initial-expander* + * @param form Form to expand + * @return Syntax Object resulting from expansion. + */ + public Context expand(ACell form) { + return expand(Core.INITIAL_EXPANDER, form, Core.INITIAL_EXPANDER); + } + + @SuppressWarnings("unchecked") + public Context expand(AFn expander, ACell form, AFn cont) { + // execute with adjusted depth + int savedDepth=getDepth(); + Context ctx =(Context) this.withDepth(savedDepth+1); + if (ctx.isExceptional()) return ctx; // depth error, won't have modified depth + + //AVector savedEnv=getLocalBindings(); + + Context rctx= (Context)invoke(expander, form, cont); + + // reset depth after execution. + //rctx=rctx.withLocalBindings(savedEnv); + rctx=rctx.withDepth(savedDepth); + return rctx; + } + + /** + * Looks up an expander from a form in this context + * @param form Form which might be an expander reference + * @return Expander instance, or null if no expander found + */ + public AFn lookupExpander(ACell form) { + /** + * MapEntry for Expander metadata lookup + */ + AHashMap me = null; + Address addr; + Symbol sym; + + if (form instanceof Symbol) { + sym = (Symbol)form; + me = this.lookupMeta(sym); + addr = null; + } else if (form instanceof AList) { + // Need to check for (lookup ....) as this could reference an expander + @SuppressWarnings("unchecked") + AList listForm = (AList)form; + int n = listForm.size(); + if (n <= 1) return null; + if (!Symbols.LOOKUP.equals(listForm.get(0))) return null; + ACell maybeSym = listForm.get(n-1); + if (!(maybeSym instanceof Symbol)) return null; + sym = (Symbol)maybeSym; + if (n == 2) { + addr = null; + me = lookupMeta(sym); + } else if (n == 3) { + ACell maybeAddress = listForm.get(1); + if (maybeAddress instanceof Symbol) { + // one lookup via Environment for alias + maybeAddress = lookupValue((Symbol)maybeAddress); + } + if (!(maybeAddress instanceof Address)) return null; + addr = (Address)maybeAddress; + me = lookupMeta((Address)maybeAddress,sym); + } else { + return null; + } + } else { + return null; + } + + if (me == null) return null; + + // TODO: examine syntax object for expander details? + ACell expBool = me.get(Keywords.EXPANDER_Q); + if (RT.bool(expBool)) { + // expand form using specified expander and continuation expander + ACell v = lookupValue(addr,sym); + AFn expander = RT.castFunction(v); + if (expander != null) return expander; + } + return null; + } + + + +} diff --git a/convex-core/src/main/java/convex/core/lang/Core.java b/convex-core/src/main/java/convex/core/lang/Core.java new file mode 100644 index 000000000..0042f1281 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/Core.java @@ -0,0 +1,2469 @@ +package convex.core.lang; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; + +import convex.core.Constants; +import convex.core.ErrorCodes; +import convex.core.State; +import convex.core.data.ABlob; +import convex.core.data.ABlobMap; +import convex.core.data.ACell; +import convex.core.data.ADataStructure; +import convex.core.data.AHashMap; +import convex.core.data.AList; +import convex.core.data.AMap; +import convex.core.data.ASequence; +import convex.core.data.ASet; +import convex.core.data.AString; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.core.data.BlobMaps; +import convex.core.data.Format; +import convex.core.data.Hash; +import convex.core.data.INumeric; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.List; +import convex.core.data.MapEntry; +import convex.core.data.Maps; +import convex.core.data.Sets; +import convex.core.data.Symbol; +import convex.core.data.Syntax; +import convex.core.data.Vectors; +import convex.core.data.prim.APrimitive; +import convex.core.data.prim.CVMBool; +import convex.core.data.prim.CVMByte; +import convex.core.data.prim.CVMChar; +import convex.core.data.prim.CVMDouble; +import convex.core.data.prim.CVMLong; +import convex.core.data.type.Types; +import convex.core.lang.impl.AExceptional; +import convex.core.lang.impl.CoreFn; +import convex.core.lang.impl.CorePred; +import convex.core.lang.impl.ErrorValue; +import convex.core.lang.impl.HaltValue; +import convex.core.lang.impl.RecurValue; +import convex.core.lang.impl.Reduced; +import convex.core.lang.impl.ReturnValue; +import convex.core.lang.impl.RollbackValue; +import convex.core.lang.impl.TailcallValue; +import convex.core.util.Utils; + +/** + * This class builds the core runtime environment at startup. Core runtime + * functions are required to implement basic language features such as: + *
    + *
  • Numerics
  • + *
  • Data structures
  • + *
  • Interaction with on-chain state and execution context
  • + *
+ * + * In general, core functions defined in this class are thin Java wrappers over + * functions available in the CVM implementation, but also need to account for: + *
    + *
  • Argument checking
  • + *
  • Exceptional case handling
  • + *
  • Appropriate juice costs
  • + *
+ * + * Where possible, we implement core functions in Convex Lisp itself, see + * resources/lang/core.cvx + * + * "Java is the most distressing thing to hit computing since MS-DOS." - Alan + * Kay + */ +@SuppressWarnings("rawtypes") +public class Core { + + /** + * Default initial environment importing core namespace + */ + public static final AHashMap ENVIRONMENT; + + /** + * Default initial core metadata + */ + public static final AHashMap> METADATA; + + + /** + * Symbol for core namespace + */ + public static final Symbol CORE_SYMBOL = Symbol.create("convex.core"); + + private static final HashSet tempReg = new HashSet(); + + private static T reg(T o) { + tempReg.add(o); + return o; + } + + public static final CoreFn> VECTOR = reg(new CoreFn<>(Symbols.VECTOR) { + @Override + public Context> invoke(Context context, ACell[] args) { + // Need to charge juice on per-element basis + long juice = Juice.BUILD_DATA + args.length * Juice.BUILD_PER_ELEMENT; + + // Check juice before building a big vector. + // OK to fail early since will fail with JUICE anyway if vector is too big. + if (!context.checkJuice(juice)) return context.withJuiceError(); + + // Build and return requested vector + AVector result = Vectors.create(args); + return context.withResult(juice, result); + } + }); + + public static final CoreFn> CONCAT = reg(new CoreFn<>(Symbols.CONCAT) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + ASequence result = null; + int n=args.length; + + // initial juice is a load of null + long juice = Juice.CONSTANT; + for (int ix=0; ix seq = RT.sequence(a); + if (seq == null) return context.withCastError(ix,args, Types.SEQUENCE); + + // check juice per element of concatenated sequences + juice += Juice.BUILD_DATA+ seq.count() * Juice.BUILD_PER_ELEMENT; + if (!context.checkJuice(juice)) return context.withJuiceError(); + result = RT.concat(result, seq); + } + return context.withResult(juice, result); + } + }); + + public static final CoreFn> VEC = reg(new CoreFn<>(Symbols.VEC) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + // Arity 1 exactly + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + ACell o = args[0]; + + // Need to compute juice before building potentially big vector + Long n = RT.count(o); + if (n == null) return context.withCastError(0,args, Types.VECTOR); + + long juice = Juice.BUILD_DATA + n * Juice.BUILD_PER_ELEMENT; + if (!context.checkJuice(juice)) return context.withJuiceError(); + + AVector result = RT.castVector(o); + return context.withResult(juice, result); + } + }); + + public static final CoreFn> REVERSE = reg(new CoreFn<>(Symbols.REVERSE) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + // Arity 1 exactly + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + ACell o = args[0]; + + // Need to compute juice before building potentially big vector + ASequence seq = RT.ensureSequence(o); + if (seq == null) return context.withCastError(0,args, Types.SEQUENCE); + + long juice = Juice.BUILD_DATA; + + ASequence result = seq.reverse(); + return context.withResult(juice, result); + } + }); + + public static final CoreFn> SET = reg(new CoreFn<>(Symbols.SET) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + ACell o = args[0]; + + // Need to compute juice before building a potentially big set + Long n = RT.count(o); + if (n == null) return context.withCastError(0,args, Types.SEQUENCE); + long juice = Juice.addMul(Juice.BUILD_DATA ,n,Juice.BUILD_PER_ELEMENT); + if (!context.checkJuice(juice)) return context.withJuiceError(); + + ASet result = RT.castSet(o); + if (result == null) return context.withCastError(0,args, Types.SET); + + return context.withResult(juice, result); + } + }); + + public static final CoreFn> UNION = reg(new CoreFn<>(Symbols.UNION) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + int n=args.length; + ASet result=Sets.empty(); + + long juice=Juice.BUILD_DATA; + + for (int i=0; i set=RT.ensureSet(arg); + if (set==null) return context.withCastError(i,args, Types.SET); + + // check juice before expensive operation + long size=set.count(); + juice = Juice.addMul(juice, size, Juice.BUILD_PER_ELEMENT); + if (!context.checkJuice(juice)) return context.withJuiceError(); + + result=result.includeAll(set); + } + + return context.withResult(juice, result); + } + }); + + public static final CoreFn> INTERSECTION = reg(new CoreFn<>(Symbols.INTERSECTION) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + if (args.length <1) return context.withArityError(minArityMessage(1, args.length)); + + int n=args.length; + ACell arg0=(ACell) args[0]; + ASet result=(arg0==null)?Sets.empty():RT.ensureSet(arg0); + if (result==null) return context.withCastError(0,args, Types.SET); + + long juice=Juice.BUILD_DATA; + + for (int i=1; i set=(arg==null)?Sets.empty():RT.ensureSet(args[i]); + if (set==null) return context.withCastError(i,args, Types.SET); + long size=set.count(); + + juice = Juice.addMul(juice, size, Juice.BUILD_PER_ELEMENT); + if (!context.checkJuice(juice)) return context.withJuiceError(); + + result=result.intersectAll(set); + } + + return context.withResult(juice, result); + } + }); + + public static final CoreFn> DIFFERENCE = reg(new CoreFn<>(Symbols.DIFFERENCE) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + if (args.length <1) return context.withArityError(minArityMessage(1, args.length)); + + int n=args.length; + ACell arg0=args[0]; + ASet result=(arg0==null)?Sets.empty():RT.ensureSet(arg0); + if (result==null) return context.withCastError(0,args, Types.SET); + + long juice=Juice.BUILD_DATA; + + for (int i=1; i set=RT.ensureSet(arg); + if (set==null) return context.withCastError(i,args, Types.SET); + long size=set.count(); + + juice = Juice.addMul(juice, size, Juice.BUILD_PER_ELEMENT); + if (!context.checkJuice(juice)) return context.withJuiceError(); + + result=result.excludeAll(set); + } + + return context.withResult(juice, result); + } + }); + + + + public static final CoreFn> LIST = reg(new CoreFn<>(Symbols.LIST) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + // Any arity is OK + + // Need to compute juice before building a potentially big list + long juice = Juice.BUILD_DATA + args.length * Juice.BUILD_PER_ELEMENT; + if (!context.checkJuice(juice)) return context.withJuiceError(); + + AList result = List.create(args); + return context.withResult(juice, result); + } + }); + + public static final CoreFn STR = reg(new CoreFn<>(Symbols.STR) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + // TODO: pre-check juice? String rendering definitions? + AString result = RT.str(args); + if (result==null) return context.withCastError(Types.STRING); + + long juice = Juice.STR + result.length() * Juice.STR_PER_CHAR; + return context.withResult(juice, result); + } + }); + + public static final CoreFn NAME = reg(new CoreFn<>(Symbols.NAME) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + // Arity 1 + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + // Check can get as a String name + ACell arg = args[0]; + AString result = RT.name(arg); + if (result == null) return context.withCastError(0,args, Types.STRING); + + long juice = Juice.SIMPLE_FN; + return context.withResult(juice, result); + } + }); + + public static final CoreFn KEYWORD = reg(new CoreFn<>(Symbols.KEYWORD) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + // Arity 1 + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell arg=args[0]; + if (arg instanceof Keyword) return context.withResult(Juice.KEYWORD, arg); + + // Check argument is valid name + AString name = RT.name(arg); + if (name == null) return context.withCastError(0,args, Types.KEYWORD); + + // Check name converts to Keyword + Keyword result = Keyword.create(name); + if (result == null) return context.withArgumentError("Invalid Keyword name, must be between 1 and "+Constants.MAX_NAME_LENGTH+ " characters"); + + return context.withResult(Juice.KEYWORD, result); + } + }); + + public static final CoreFn SYMBOL = reg(new CoreFn(Symbols.SYMBOL) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int n = args.length; + if (n!=1) return context.withArityError(exactArityMessage(1,args.length)); + + ACell maybeName=args[n-1]; + + // Fast path for existing Symbol + if (maybeName instanceof Symbol) { + Symbol sym=(Symbol)maybeName; + return context.withResult(Juice.SYMBOL, sym); + } + + // Check argument is valid name for a Symbol + AString name = RT.name(maybeName); + if (name == null) return context.withCastError(0, args, Types.SYMBOL); + + Symbol sym = Symbol.create(name); + if (sym == null) return context.withArgumentError("Invalid Symbol name, must be between 1 and " + Constants.MAX_NAME_LENGTH + " characters"); + + long juice = Juice.SYMBOL; + return context.withResult(juice, sym); + } + }); + + public static final CoreFn> COMPILE = reg(new CoreFn<>(Symbols.COMPILE) { + + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + ACell form = (ACell) args[0]; + // note: compiler takes care of Juice for us + return context.expandCompile(form); + } + + }); + + public static final CoreFn EVAL = reg(new CoreFn<>(Symbols.EVAL) { + + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell form = (ACell) args[0]; + Context rctx = context.eval(form); + return rctx.consumeJuice(Juice.EVAL); + } + + }); + + public static final CoreFn EVAL_AS = reg(new CoreFn<>(Symbols.EVAL_AS) { + + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); + + Address address = RT.ensureAddress(args[0]); + if (address==null) return context.withCastError(0,args, Types.ADDRESS); + + ACell form = (ACell) args[1]; + Context rctx = context.evalAs(address,form); + return rctx.consumeJuice(Juice.EVAL); + } + }); + + public static final CoreFn SCHEDULE_STAR = reg(new CoreFn<>(Symbols.SCHEDULE_STAR) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int n = args.length; + if (n != 2) return context.withArityError(this.exactArityMessage(3, n)); + + // get timestamp target + CVMLong tso = RT.ensureLong(args[0]); + if (tso==null) return context.withCastError(0,args,Types.LONG); + long scheduleTimestamp = tso.longValue(); + + // get operation + ACell opo = args[1]; + if (!(opo instanceof AOp)) return context.withCastError(1,args,Types.OP); + AOp op = (AOp) opo; + + return context.schedule(scheduleTimestamp, op); + } + }); + + public static final CoreFn SYNTAX = reg(new CoreFn<>(Symbols.SYNTAX) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int n=args.length; + if (n < 1) return context.withArityError(minArityMessage(1, args.length)); + if (n > 2) return context.withArityError(maxArityMessage(2, args.length)); + + Syntax result; + if (n==1) { + result=Syntax.create((ACell)args[0]); + } else { + AHashMap meta=RT.ensureHashMap(args[1]); + if (meta==null) return context.withCastError(1,args, Types.MAP); + result = Syntax.create((ACell) args[0],meta); + } + + long juice = Juice.SYNTAX; + + return context.withResult(juice, result); + } + }); + + public static final CoreFn UNSYNTAX = reg(new CoreFn<>(Symbols.UNSYNTAX) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + // Arity 1 + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + // Unwrap Syntax. Cannot fail. + ACell result = Syntax.unwrap(args[0]); + + // Return unwrapped value with juice + long juice = Juice.SYNTAX; + return context.withResult(juice, result); + } + }); + + public static final CoreFn> META = reg(new CoreFn<>(Symbols.META) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell a=args[0]; + + AHashMap result; + if (a instanceof Syntax) { + result = ((Syntax) a).getMeta(); + } else { + result= null; + } + + long juice = Juice.META; + return context.withResult(juice, result); + } + }); + + public static final CorePred SYNTAX_Q = reg(new CorePred(Symbols.SYNTAX_Q) { + @Override + public boolean test(ACell val) { + return val instanceof Syntax; + } + }); + + public static final CoreFn EXPAND = reg(new CoreFn<>(Symbols.EXPAND) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int n = args.length; + if ((n<1)||(n>3)) { + return context.withArityError(name() + " requires a form argument, optional expander and optional continuation expander (arity 1, 2 or 2)"); + } + + //context = context.lookup(Symbols.STAR_INITIAL_EXPANDER); + //if (context.isExceptional()) return (Context) context; + //AFn initialExpander=RT.function(context.getResult()); + //if (initialExpander==null) { + // return context.withError(ErrorCodes.CAST,name()+" requires a valid *initial-expander*, not found in environment"); + //} + + AFn expander=Compiler.INITIAL_EXPANDER; + if (n >= 2) { + // use provided expander + ACell exArg = args[1]; + expander=RT.ensureFunction(exArg); + if (expander==null) return context.withCastError(1,args, Types.FUNCTION); + } + + AFn cont=expander; // use passed expander by default + if (n >= 3) { + // use provided continuation expander + ACell contArg = args[2]; + cont=RT.ensureFunction(contArg); + if (cont==null) return context.withCastError(2,args, Types.FUNCTION); + } + + ACell form = args[0]; + Context rctx = context.expand(expander,form, cont); + return rctx; + } + }); + + public static final AFn INITIAL_EXPANDER = reg(Compiler.INITIAL_EXPANDER); + + public static final AFn QUOTE_EXPANDER = reg(Compiler.QUOTE_EXPANDER); + + public static final AFn QUASIQUOTE_EXPANDER = reg(Compiler.QUASIQUOTE_EXPANDER); + + public static final CoreFn CALLABLE_Q = reg(new CoreFn<>(Symbols.CALLABLE_Q) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); + + Address addr = RT.ensureAddress(args[0]); + if (addr == null) return context.withCastError(1,args, Types.ADDRESS); + + Symbol sym = RT.ensureSymbol(args[1]); + if (sym == null) return context.withCastError(1,args, Types.SYMBOL); + + AccountStatus as = context.getState().getAccount(addr); + if (as == null) return context.withResult(Juice.LOOKUP, CVMBool.FALSE); + + AHashMap symMeta = as.getMetadata().get(sym); + + CVMBool result; + + if (symMeta == null) { + result = CVMBool.FALSE; + } else { + result = CVMBool.of(symMeta.get(Keywords.CALLABLE_Q) == CVMBool.TRUE); + } + + return context.withResult(Juice.LOOKUP, result); + } + }); + + public static final CoreFn
DEPLOY = reg(new CoreFn<>(Symbols.DEPLOY) { + @SuppressWarnings("unchecked") + @Override + public Context
invoke(Context context, ACell[] args) { + if (args.length !=1) return context.withArityError(exactArityMessage(1, args.length)); + + return context.deployActor(args[0]); + } + }); + + + public static final CoreFn ACCEPT = reg(new CoreFn<>(Symbols.ACCEPT) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + // Arity 1 + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + // must cast to Long + CVMLong amount = RT.ensureLong(args[0]); + if (amount == null) return context.withCastError(0,args, Types.LONG); + + return context.acceptFunds(amount.longValue()); + } + }); + + public static final CoreFn CALL_STAR = reg(new CoreFn<>(Symbols.CALL_STAR) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length < 3) return context.withArityError(minArityMessage(1, args.length)); + + // consume juice first? + Context ctx = context.consumeJuice(Juice.CALL_OP); + if (ctx.isExceptional()) return ctx; + + Address target = RT.ensureAddress(args[0]); + if (target == null) return ctx.withCastError(0,args, Types.ADDRESS); + + CVMLong sendAmount = RT.ensureLong(args[1]); + if (sendAmount == null) return ctx.withCastError(1,args, Types.LONG); + + Symbol sym = RT.ensureSymbol(args[2]); + if (sym == null) return ctx.withCastError(2,args, Types.SYMBOL); + + // prepare contract call arguments + int arity = args.length - 3; + ACell[] callArgs = Arrays.copyOfRange(args, 3, 3 + arity); + + return ctx.actorCall(target, sendAmount.longValue(), sym, callArgs); + } + }); + + public static final CoreFn LOG = reg(new CoreFn<>(Symbols.LOG) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + // any arity fine + int n=args.length; + long juice = Juice.LOG+Juice.BUILD_DATA+n*Juice.BUILD_PER_ELEMENT; + if (!context.checkJuice(juice)) { + return context.withJuiceError(); + } + AVector values=Vectors.create(args); + + context=context.appendLog(values); + + return context.withResult(juice, values); + } + }); + + public static final CoreFn UNDEF_STAR = reg(new CoreFn<>(Symbols.UNDEF_STAR) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + Symbol sym=RT.ensureSymbol(args[0]); + if (sym == null) return context.withArgumentError("Invalid Symbol name for undef: " + Utils.toString(args[0])); + + Context ctx=(Context) context.undefine(sym); + + // return nil + return ctx.withResult(Juice.DEF, null); + + } + }); + + + public static final CoreFn LOOKUP = reg(new CoreFn<>(Symbols.LOOKUP) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int n=args.length; + if ((n<1)||(n>2)) return context.withArityError(rangeArityMessage(1,2, args.length)); + + // get Address to perform lookup + Address address=(n==1)?context.getAddress():RT.ensureAddress(args[0]); + if (address==null) return context.withCastError(0,args, Types.ADDRESS); + + // ensure argument converts to a Symbol correctly. + ACell symArg=args[n-1]; + Symbol sym = RT.ensureSymbol(symArg); + if (sym == null) return context.withCastError(n-1,args,Types.SYMBOL); + + MapEntry me = context.lookupDynamicEntry(address,sym); + + long juice = Juice.LOOKUP; + ACell result = (me == null) ? null : me.getValue(); + return context.withResult(juice, result); + } + }); + + public static final CoreFn LOOKUP_META = reg(new CoreFn<>(Symbols.LOOKUP_META) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int n=args.length; + if ((n<1)||(n>2)) return context.withArityError(rangeArityMessage(1,2, args.length)); + + // get Address to perform lookup + Address address=null; + if (n>1) { + address=RT.ensureAddress(args[0]); + if (address==null) return context.withCastError(0,args, Types.ADDRESS); + } + + // ensure argument converts to a Symbol correctly. + ACell symArg=args[n-1]; + Symbol sym = RT.ensureSymbol(symArg); + if (sym == null) return context.withCastError(n-1,args,Types.SYMBOL); + + AHashMap result = context.lookupMeta(address,sym); + + long juice = Juice.LOOKUP; + return context.withResult(juice, result); + } + }); + + public static final CoreFn
ADDRESS = reg(new CoreFn<>(Symbols.ADDRESS) { + @SuppressWarnings("unchecked") + @Override + public Context
invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell o = args[0]; + Address address = RT.castAddress(o); + if (address == null) { + if (o instanceof AString) return context.withArgumentError("String not convertible to a valid Address: " + o); + if (o instanceof ABlob) return context.withArgumentError("Blob not convertiable a valid Address: " + o); + return context.withCastError(0,args, Types.ADDRESS); + } + long juice = Juice.ADDRESS; + + return context.withResult(juice, address); + } + }); + + public static final CoreFn BLOB = reg(new CoreFn<>(Symbols.BLOB) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + // TODO: probably need to pre-cost this? + ABlob blob = RT.castBlob(args[0]); + if (blob == null) return context.withCastError(0,args, Types.BLOB); + + long juice = Juice.BLOB + Juice.BLOB_PER_BYTE * blob.count(); + + return context.withResult(juice, blob); + } + }); + + public static final CoreFn ACCOUNT = reg(new CoreFn<>(Symbols.ACCOUNT) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell a0 = args[0]; + Address address = RT.ensureAddress(a0); + if (address == null) return context.withCastError(0,args, Types.ADDRESS); + + // Note: returns null if the argument is not an address + AccountStatus as = context.getAccountStatus(address); + + return context.withResult(Juice.SIMPLE_FN, as); + } + }); + + public static final CoreFn BALANCE = reg(new CoreFn<>(Symbols.BALANCE) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + Address address = RT.ensureAddress(args[0]); + if (address == null) return context.withCastError(0,args, Types.ADDRESS); + + AccountStatus as = context.getAccountStatus(address); + CVMLong balance = (as != null) ? CVMLong.create(as.getBalance()) : null; + + return context.withResult(Juice.BALANCE, balance); + } + }); + + public static final CoreFn TRANSFER = reg(new CoreFn<>(Symbols.TRANSFER) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); + + Address address = RT.ensureAddress(args[0]); + if (address == null) return context.withCastError(0,args, Types.ADDRESS); + + CVMLong amount = RT.ensureLong(args[1]); + if (amount == null) return context.withCastError(1,args, Types.LONG); + + return context.transfer(address, amount.longValue()).consumeJuice(Juice.TRANSFER); + + } + }); + + public static final CoreFn SET_MEMORY = reg(new CoreFn<>(Symbols.SET_MEMORY) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + CVMLong amount = RT.ensureLong(args[0]); + if (amount == null) return context.withCastError(0,args, Types.LONG); + + return context.setMemory(amount.longValue()).consumeJuice(Juice.TRANSFER); + } + }); + + public static final CoreFn TRANSFER_MEMORY = reg(new CoreFn<>(Symbols.TRANSFER_MEMORY) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); + + Address address = RT.ensureAddress(args[0]); + if (address == null) return context.withCastError(0,args, Types.ADDRESS); + + CVMLong amount = RT.ensureLong(args[1]); + if (amount == null) return context.withCastError(1,args, Types.LONG); + + return context.transferMemoryAllowance(address, amount).consumeJuice(Juice.TRANSFER); + } + }); + + public static final CoreFn STAKE = reg(new CoreFn<>(Symbols.STAKE) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); + + AccountKey accountKey = RT.ensureAccountKey(args[0]); + if (accountKey == null) return context.withCastError(0,args, Types.BLOB); + + CVMLong amount = RT.ensureLong(args[1]); + if (amount == null) return context.withCastError(1,args, Types.LONG); + + return context.setDelegatedStake(accountKey, amount.longValue()).consumeJuice(Juice.TRANSFER); + + } + }); + + public static final CoreFn CREATE_PEER = reg(new CoreFn<>(Symbols.CREATE_PEER) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); + + AccountKey accountKey = RT.ensureAccountKey(args[0]); + if (accountKey == null) return context.withCastError(0,args, Types.BLOB); + + CVMLong amount = RT.ensureLong(args[1]); + if (amount == null) return context.withCastError(1,args, Types.LONG); + + return context.createPeer(accountKey, amount.longValue()).consumeJuice(Juice.PEER_UPDATE); + } + }); + + + public static final CoreFn SET_PEER_DATA = reg(new CoreFn<>(Symbols.SET_PEER_DATA) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 2) return context.withArityError(exactArityMessage(1, args.length)); + + AccountKey peerKey=RT.ensureAccountKey(args[0]); + if (peerKey == null) return context.withCastError(0,args, Types.BLOB); + + AMap data = RT.ensureMap(args[1]); + if (data == null) return context.withCastError(1,args, Types.MAP); + + context=context.consumeJuice(Juice.PEER_UPDATE); + if (context.isExceptional()) return context; + + return context.setPeerData(peerKey,data); + } + }); + + public static final CoreFn SET_PEER_STAKE = reg(new CoreFn<>(Symbols.SET_PEER_STAKE) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); + + AccountKey peerKey=RT.ensureAccountKey(args[0]); + if (peerKey == null) return context.withCastError(0,args, Types.BLOB); + + CVMLong newStake = RT.ensureLong(args[1]); + if (newStake == null) return context.withCastError(1,args, Types.LONG); + long targetStake=newStake.longValue(); + + context=context.consumeJuice(Juice.PEER_UPDATE); + if (context.isExceptional()) return context; + + return context.setPeerStake(peerKey,targetStake); + } + }); + + + public static final CoreFn> HASHMAP = reg(new CoreFn<>(Symbols.HASH_MAP) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + int len = args.length; + // specialised arity check since we need even length + if (Utils.isOdd(len)) return context.withArityError(name() + " requires an even number of arguments"); + + long juice = Juice.BUILD_DATA + len * Juice.BUILD_PER_ELEMENT; + return context.withResult(juice, Maps.create(args)); + } + }); + + + public static final CoreFn BLOB_MAP = reg(new CoreFn<>(Symbols.BLOB_MAP) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int len = args.length; + // specialised arity check since we need even length + if (Utils.isOdd(len)) return context.withArityError(name() + " requires an even number of arguments"); + + long juice = Juice.BUILD_DATA + len * Juice.BUILD_PER_ELEMENT; + if (!context.checkJuice(juice)) return context.withJuiceError(); + + ABlobMap r=BlobMaps.empty(); + int n=len/2; + for (int i=0; i> HASHSET = reg(new CoreFn<>(Symbols.HASH_SET) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + // any arity is OK + + long juice = Juice.BUILD_DATA + (args.length * Juice.BUILD_PER_ELEMENT); + if (!context.checkJuice(juice)) return context.withJuiceError(); + + return context.withResult(juice, Sets.of(args)); + } + }); + + public static final CoreFn> KEYS = reg(new CoreFn<>(Symbols.KEYS) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell a = args[0]; + if (!(a instanceof AMap)) return context.withCastError(0,args, Types.MAP); + + AMap m = (AMap) a; + long juice = Juice.BUILD_DATA + m.count() * Juice.BUILD_PER_ELEMENT; + if (!context.checkJuice(juice)) return context.withJuiceError(); + + AVector keys = RT.keys(m); + + return context.withResult(juice, keys); + } + }); + + public static final CoreFn> VALUES = reg(new CoreFn<>(Symbols.VALUES) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell a = args[0]; + if (!(a instanceof AMap)) return context.withCastError(0,args, Types.MAP); + + AMap m = (AMap) a; + long juice = Juice.BUILD_DATA + m.count() * Juice.BUILD_PER_ELEMENT; + if (!context.checkJuice(juice)) return context.withJuiceError(); + + AVector keys = RT.values(m); + + return context.withResult(juice, keys); + } + }); + + public static final CoreFn> ASSOC = reg(new CoreFn<>(Symbols.ASSOC) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + int n = args.length; + if (n < 1) return context.withArityError(minArityMessage(1, n)); + + if (!Utils.isOdd(n)) return context.withArityError(name() + " requires key/value pairs as successive args"); + + long juice = Juice.BUILD_DATA + (n - 1) * Juice.BUILD_PER_ELEMENT; + if (!context.checkJuice(juice)) return context.withJuiceError(); + + ACell o = args[0]; + + // convert to associative data structure. nil-> empty map + ADataStructure result = RT.ensureAssociative(o); + + // values that are non-null but not a data structure are a cast error + if ((o != null) && (result == null)) return context.withCastError(0,args, Types.DATA_STRUCTURE); + + // assoc additional elements. Must produce a valid non-null data structure after + // each assoc + for (int i = 1; i < n; i += 2) { + ACell key=args[i]; + result = RT.assoc(result, key, args[i + 1]); + if (result == null) return context.withError(ErrorCodes.ARGUMENT, "Cannot assoc value - invalid key of type "+RT.getType(key)); + } + + return context.withResult(juice, (ACell) result); + } + }); + + public static final CoreFn ASSOC_IN = reg(new CoreFn<>(Symbols.ASSOC_IN) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 3) return context.withArityError(exactArityMessage(3, args.length)); + + ASequence ixs = RT.ensureSequence(args[1]); + if (ixs == null) return context.withCastError(1,args, Types.SEQUENCE); + + int n = ixs.size(); + long juice = (Juice.GET+Juice.ASSOC) * (1L + n); + ACell data = args[0]; + ACell value= args[2]; + // simply substitute value if key sequence is empty + if (n==0) return context.withResult(juice, value); + + ADataStructure[] ass=new ADataStructure[n]; + ACell[] ks=new ACell[n]; + for (int i = 0; i < n; i++) { + ADataStructure struct = RT.ensureAssociative(data); // nil-> empty map + if (struct == null) return context.withCastError((ACell)struct,Types.DATA_STRUCTURE); // TODO: Associative type? + ass[i]=struct; + ACell k=ixs.get(i); + ks[i]=k; + data=struct.get(k); + } + + for (int i = n-1; i >=0; i--) { + ADataStructure struct=ass[i]; + ACell k=ks[i]; + value=RT.assoc(struct, k, value); + if (value==null) { + // assoc failed, so key or value type must be invlid + return context.withError(ErrorCodes.ARGUMENT,"Invalid key of type "+RT.getType(k)+" or value of type "+RT.getType(value)+" for " +name()); + } + } + return context.withResult(juice, value); + } + }); + + public static final CoreFn GET_HOLDING = reg(new CoreFn<>(Symbols.GET_HOLDING) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int n = args.length; + if (n !=1) return context.withArityError(exactArityMessage(1, n)); + + Address address=RT.ensureAddress(args[0]); + if (address == null) return context.withCastError(args[0], Types.ADDRESS); + + AccountStatus as=context.getAccountStatus(address); + if (as==null) return context.withError(ErrorCodes.NOBODY,"Account with holdings does not exist."); + ABlobMap holdings=as.getHoldings(); + + // we get the target accounts holdings for the currently executing account + ACell result=holdings.get(context.getAddress()); + + return context.withResult(Juice.LOOKUP, result); + } + }); + + public static final CoreFn SET_HOLDING = reg(new CoreFn<>(Symbols.SET_HOLDING) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int n = args.length; + if (n !=2) return context.withArityError(exactArityMessage(2, n)); + + Address address=RT.ensureAddress(args[0]); + if (address == null) return context.withCastError(args[0], Types.ADDRESS); + + // result is specified by second arg + ACell result= args[1]; + + // we set the target account holdings for the currently executing account + // might return NOBODY if account does not exist + context=(Context) context.setHolding(address,result); + if (context.isExceptional()) return (Context) context; + + return context.withResult(Juice.ASSOC, result); + } + }); + + public static final CoreFn SET_CONTROLLER = reg(new CoreFn<>(Symbols.SET_CONTROLLER) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int n = args.length; + if (n !=1) return context.withArityError(exactArityMessage(1, n)); + + // Get requested controller. Must be a valid address or null + ACell arg=args[0]; + Address controller=null; + if (arg!=null) { + controller=RT.ensureAddress(arg); + if (controller == null) return context.withCastError(arg, Types.ADDRESS); + if (context.getAccountStatus(controller)==null) { + return context.withError(ErrorCodes.NOBODY, name()+" must be passed an address for an existing account as controller."); + } + } + + context=(Context) context.setController(controller); + if (context.isExceptional()) return (Context) context; + + return context.withResult(Juice.ASSOC, controller); + } + }); + + public static final CoreFn SET_KEY = reg(new CoreFn<>(Symbols.SET_KEY) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int n = args.length; + if (n !=1) return context.withArityError(exactArityMessage(1, n)); + + ACell arg=args[0]; + + // Check an account key is being used as argument. nil is permitted + AccountKey publicKey=RT.ensureAccountKey(arg); + if ((publicKey == null)&&(arg!=null)) return context.withCastError(arg, Types.BLOB); + + context=(Context) context.setAccountKey(publicKey); + if (context.isExceptional()) return (Context) context; + + return context.withResult(Juice.ASSOC, publicKey); + } + }); + + + public static final CoreFn GET = reg(new CoreFn<>(Symbols.GET) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int n = args.length; + if ((n < 2) || (n > 3)) { + return context.withArityError(name() + " requires exactly 2 or 3 arguments"); + } + + ACell result; + ACell coll = args[0]; + if (coll == null) { + // Treat nil as empty collection with no keys + result = (n == 3) ? (ACell)args[2] : null; + } else if (n == 2) { + ADataStructure gettable = RT.ensureDataStructure(coll); + if (gettable == null) return context.withCastError(coll, Types.DATA_STRUCTURE); + result = gettable.get(args[1]); + } else { + ADataStructure gettable = RT.ensureDataStructure(coll); + if (gettable == null) return context.withCastError(coll, Types.DATA_STRUCTURE); + result = gettable.get(args[1], args[2]); + } + long juice = Juice.GET; + return context.withResult(juice, result); + } + }); + + public static final CoreFn GET_IN = reg(new CoreFn<>(Symbols.GET_IN) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int n = args.length; + if ((n < 2) || (n > 3)) { + return context.withArityError(name() + " requires exactly 2 or 3 arguments"); + } + + ASequence ixs = RT.ensureSequence(args[1]); + if (ixs == null) return context.withCastError(args[1], Types.SEQUENCE); + + ACell notFound=(n<3)?null:args[2]; + + int il = ixs.size(); + long juice = Juice.GET * (1L + il); + ACell result = (ACell) args[0]; + for (int i = 0; i < il; i++) { + if (result == null) { + result=notFound; + break; // gets in nil produce not-found + } + ADataStructure gettable = RT.ensureDataStructure(result); + if (gettable == null) return context.withCastError(result, Types.DATA_STRUCTURE); + + ACell k=ixs.get(i); + if (gettable.containsKey(k)) { + result = gettable.get(k); + } else { + return context.withResult(juice, notFound); + } + + } + return context.withResult(juice, result); + } + }); + + public static final CoreFn CONTAINS_KEY_Q = reg(new CoreFn<>(Symbols.CONTAINS_KEY_Q) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int n = args.length; + if (n != 2) return context.withArityError(exactArityMessage(2, n)); + + CVMBool result; + ACell coll = args[0]; + if (coll == null) { + result = CVMBool.FALSE; // treat nil as empty collection + } else { + ADataStructure gettable = RT.ensureDataStructure(args[0]); + if (gettable == null) return context.withCastError(args[0], Types.DATA_STRUCTURE); + result = CVMBool.of(gettable.containsKey((ACell) args[1])); + } + + long juice = Juice.GET; + return context.withResult(juice, result); + } + }); + + public static final CoreFn SUBSET_Q = reg(new CoreFn<>(Symbols.SUBSET_Q) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int n = args.length; + if (n != 2) return context.withArityError(exactArityMessage(2, n)); + + ASet s0=RT.ensureSet(args[0]); + if (s0==null) return context.withCastError(args[0], Types.SET); + + long juice = Juice.SET_COMPARE_PER_ELEMENT*s0.count(); + if (!context.checkJuice(juice)) return context.withJuiceError(); + + ASet s1=RT.ensureSet(args[1]); + if (s1==null) return context.withCastError(args[1], Types.SET); + + CVMBool result=CVMBool.of(s0.isSubset(s1)); + return context.withResult(juice, result); + } + }); + + public static final CoreFn> DISSOC = reg(new CoreFn<>(Symbols.DISSOC) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + int n = args.length; + if (args.length < 1) return context.withArityError(minArityMessage(1, args.length)); + + AMap result = RT.ensureMap(args[0]); + if (result == null) return context.withCastError(args[0], Types.MAP); + + for (int i = 1; i < n; i++) { + result = result.dissoc((ACell) args[i]); + } + long juice = Juice.BUILD_DATA + (n - 1) * Juice.BUILD_PER_ELEMENT; + return context.withResult(juice, result); + } + }); + + public static final CoreFn> CONJ = reg(new CoreFn<>(Symbols.CONJ) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + int numAdditions = args.length - 1; + if (args.length <= 0) return context.withArityError(name() + " requires a data structure as first argument"); + + // compute juice up front + long juice = Juice.BUILD_DATA + Juice.BUILD_PER_ELEMENT * numAdditions; + if (!context.checkJuice(juice)) return context.withJuiceError(); + + ADataStructure result = RT.castDataStructure(args[0]); + if (result == null) return context.withCastError(0,args, Types.DATA_STRUCTURE); + + for (int i = 0; i < numAdditions; i++) { + int argIndex=i+1; + ACell val = (ACell) args[argIndex]; + result = result.conj(val); + if (result == null) return context.withError(ErrorCodes.ARGUMENT,"Failure to 'conj' argument at position "+argIndex+" (with Type "+RT.getType(val)+"). Probably not a legal value for this data structure?"); // must be a failed map conj? + } + return context.withResult(juice, result); + } + }); + + public static final CoreFn> DISJ = reg(new CoreFn<>(Symbols.DISJ) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + if (args.length < 1) return context.withArityError(minArityMessage(1, args.length)); + + // compute juice up front + int numAdditions = args.length - 1; + long juice = Juice.BUILD_DATA + Juice.BUILD_PER_ELEMENT * numAdditions; + if (!context.checkJuice(juice)) return context.withJuiceError(); + + ASet result = RT.ensureSet(args[0]); + if (result == null) return context.withCastError(0,args, Types.SET); + + + for (int i = 0; i < numAdditions; i++) { + int argIndex=i+1; + ACell val = args[argIndex]; + result = result.exclude(val); + } + + return context.withResult(juice, result); + } + }); + + public static final CoreFn> CONS = reg(new CoreFn<>(Symbols.CONS) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + int n = args.length; + if (args.length < 2) return context.withArityError(minArityMessage(2, args.length)); + + long juice = Juice.BUILD_DATA + Juice.BUILD_PER_ELEMENT * (n - 1); + if (!context.checkJuice(juice)) return context.withJuiceError(); + + // get sequence from last argument + int lastIndex=n-1; + ASequence seq = RT.sequence(args[lastIndex]); + if (seq == null) return context.withCastError(lastIndex,args, Types.SEQUENCE); + + AList list = RT.cons((ACell) args[n - 2], seq); + + for (int i = n - 3; i >= 0; i--) { + list = RT.cons((ACell)args[i], list); + } + return context.withResult(juice, list); + } + }); + + public static final CoreFn FIRST = reg(new CoreFn<>(Symbols.FIRST) { + // note we could define this as (nth coll 0) but this is more efficient + + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell maybeColl = args[0]; + Long n= RT.count(maybeColl); + if (n == null) return context.withCastError(0,args, Types.SEQUENCE); + if (n<1) return context.withBoundsError(0); + ACell result = RT.nth(maybeColl,0); + + long juice = Juice.SIMPLE_FN; + return context.withResult(juice, result); + } + }); + + public static final CoreFn SECOND = reg(new CoreFn<>(Symbols.SECOND) { + // note we could define this as (nth coll 1) but this is more efficient + + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell maybeColl = (ACell) args[0]; + Long n= RT.count(maybeColl); + if (n == null) return context.withCastError(0,args, Types.SEQUENCE); + if (n<2) return context.withBoundsError(1); + ACell result = RT.nth(maybeColl,1); + + long juice = Juice.SIMPLE_FN; + return context.withResult(juice, result); + } + }); + + public static final CoreFn LAST = reg(new CoreFn<>(Symbols.LAST) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell a = args[0]; + + Long n = RT.count(a); + if (n == null) return context.withCastError(0,args, Types.SEQUENCE); + if (n<=0) return context.withBoundsError(-1); + + ACell result = RT.nth(a,n-1); + + long juice = Juice.SIMPLE_FN; + return context.withResult(juice, result); + } + }); + + public static final CoreFn EQUALS = reg(new CoreFn<>(Symbols.EQUALS) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + + // all arities OK, all args OK + CVMBool result = CVMBool.of(RT.allEqual(args)); + return context.withResult(Juice.EQUALS, result); + } + }); + + public static final CoreFn EQ = reg(new CoreFn<>(Symbols.EQ) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + // all arities OK, but need to watch for non-numeric arguments + Boolean result = RT.eq(args); + if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); + + return context.withResult(Juice.NUMERIC_COMPARE, CVMBool.create(result)); + } + }); + + public static final CoreFn GE = reg(new CoreFn<>(Symbols.GE) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + // all arities OK + Boolean result = RT.ge(args); + if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); + + return context.withResult(Juice.NUMERIC_COMPARE, CVMBool.create(result)); + } + }); + + public static final CoreFn GT = reg(new CoreFn<>(Symbols.GT) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + // all arities OK + + Boolean result = RT.gt(args); + if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); + + return context.withResult(Juice.NUMERIC_COMPARE, CVMBool.create(result)); + } + }); + + public static final CoreFn LE = reg(new CoreFn<>(Symbols.LE) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + // all arities OK + + Boolean result = RT.le(args); + if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); + + return context.withResult(Juice.NUMERIC_COMPARE, CVMBool.create(result)); + } + }); + + public static final CoreFn LT = reg(new CoreFn<>(Symbols.LT) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + // all arities OK + + Boolean result = RT.lt(args); + if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); + + return context.withResult(Juice.NUMERIC_COMPARE, CVMBool.create(result)); + } + }); + + public static final CoreFn INC = reg(new CoreFn<>(Symbols.INC) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell a = args[0]; + CVMLong result = RT.inc(a); + if (result == null) return context.withCastError(0,args, Types.LONG); + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn DEC = reg(new CoreFn<>(Symbols.DEC) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell a = args[0]; + CVMLong result = RT.dec(a); + if (result == null) return context.withCastError(0,args, Types.LONG); + + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn BOOLEAN = reg(new CoreFn<>(Symbols.BOOLEAN) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + // Boolean cast always works for any value + CVMBool result = (RT.bool(args[0])) ? CVMBool.TRUE : CVMBool.FALSE; + + return context.withResult(Juice.SIMPLE_FN, result); + } + }); + + public static final CorePred BOOLEAN_Q = reg(new CorePred(Symbols.BOOLEAN_Q) { + @Override + public boolean test(ACell val) { + return RT.isBoolean(val); + } + }); + + public static final CoreFn ENCODING = reg(new CoreFn<>(Symbols.ENCODING) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell a = args[0]; + ABlob encoding=Format.encodedBlob(a); + + long juice=Juice.addMul(Juice.BLOB, encoding.count(), Juice.BLOB_PER_BYTE); + return context.withResult(juice, encoding); + } + }); + + public static final CoreFn LONG = reg(new CoreFn<>(Symbols.LONG) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell a = args[0]; + CVMLong result = RT.castLong(a); + if (result == null) return context.withCastError(0, args,Types.LONG); + + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn DOUBLE = reg(new CoreFn<>(Symbols.DOUBLE) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell a = args[0]; + CVMDouble result = RT.castDouble(a); + if (result == null) return context.withCastError(0, args,Types.DOUBLE); + + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn CHAR = reg(new CoreFn<>(Symbols.CHAR) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell a = args[0]; + CVMChar result = RT.toCharacter(a); + if (result == null) return context.withCastError(0,args, Types.CHARACTER); + + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn BYTE = reg(new CoreFn<>(Symbols.BYTE) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell a = args[0]; + CVMByte result = RT.castByte(a); + if (result == null) return context.withCastError(0,args, Types.BYTE); + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn PLUS = reg(new CoreFn<>(Symbols.PLUS) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + // All arities OK + + APrimitive result = RT.plus(args); + if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn MINUS = reg(new CoreFn<>(Symbols.MINUS) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length < 1) return context.withArityError(minArityMessage(1, args.length)); + APrimitive result = RT.minus(args); + if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn TIMES = reg(new CoreFn<>(Symbols.TIMES) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + // All arities OK + APrimitive result = RT.times(args); + if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn DIVIDE = reg(new CoreFn<>(Symbols.DIVIDE) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length < 1) return context.withArityError(minArityMessage(1, args.length)); + + CVMDouble result = RT.divide(args); + if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn FLOOR = reg(new CoreFn<>(Symbols.FLOOR) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + CVMDouble result = RT.floor(args[0]); + if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + + public static final CoreFn CEIL = reg(new CoreFn<>(Symbols.CEIL) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + CVMDouble result = RT.ceil(args[0]); + if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + + public static final CoreFn SQRT = reg(new CoreFn<>(Symbols.SQRT) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + CVMDouble result = RT.sqrt(args[0]); + if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn ABS = reg(new CoreFn<>(Symbols.ABS) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + APrimitive result = RT.abs(args[0]); + if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn SIGNUM = reg(new CoreFn<>(Symbols.SIGNUM) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + ACell result = RT.signum(args[0]); + if (result == null) return context.withCastError(args[0], Types.NUMBER); + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn MOD = reg(new CoreFn<>(Symbols.MOD) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); + + CVMLong la=RT.ensureLong(args[0]); + CVMLong lb=RT.ensureLong(args[1]); + if ((lb==null)||(la==null)) return context.withCastError(Types.LONG); + + long num = la.longValue(); + long denom = lb.longValue(); + if (denom==0) return context.withArgumentError("Divsion by zero in "+name()); + + long m = num % denom; + if (m<0) m+=Math.abs(denom); // Correct for Euclidean modular function + CVMLong result=CVMLong.create(m); + + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn REM = reg(new CoreFn<>(Symbols.REM) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); + + CVMLong la=RT.ensureLong(args[0]); + CVMLong lb=RT.ensureLong(args[1]); + if ((lb==null)||(la==null)) return context.withCastError(Types.LONG); + + long num = la.longValue(); + long denom = lb.longValue(); + if (denom==0) return context.withArgumentError("Divsion by zero in "+name()); + + long m = num % denom; + CVMLong result=CVMLong.create(m); + + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn QUOT = reg(new CoreFn<>(Symbols.QUOT) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); + + CVMLong la=RT.ensureLong(args[0]); + CVMLong lb=RT.ensureLong(args[1]); + if ((lb==null)||(la==null)) return context.withCastError(Types.LONG); + + long num = la.longValue(); + long denom = lb.longValue(); + if (denom==0) return context.withArgumentError("Divsion by zero in "+name()); + + long m = num / denom; + CVMLong result=CVMLong.create(m); + + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + + public static final CoreFn POW = reg(new CoreFn<>(Symbols.POW) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); + + CVMDouble result = RT.pow(args); + if (result==null) return context.withCastError(Types.DOUBLE); + + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn EXP = reg(new CoreFn<>(Symbols.EXP) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + CVMDouble result = RT.exp(args[0]); + if (result==null) return context.withCastError(0,Types.DOUBLE); + + return context.withResult(Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn NOT = reg(new CoreFn<>(Symbols.NOT) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + CVMBool result = CVMBool.of(!RT.bool(args[0])); + return context.withResult(Juice.SIMPLE_FN, result); + } + }); + + public static final CoreFn HASH = reg(new CoreFn<>(Symbols.HASH) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ABlob blob=RT.ensureBlob(args[0]); + if (blob==null) return context.withCastError(0,args, Types.BLOB); + + Hash result = blob.getContentHash(); + return context.withResult(Juice.HASH, result); + } + }); + + public static final CoreFn COUNT = reg(new CoreFn<>(Symbols.COUNT) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + Long result = RT.count(args[0]); + if (result == null) return context.withCastError(0,args, Types.DATA_STRUCTURE); + + return context.withResult(Juice.SIMPLE_FN, CVMLong.create(result)); + } + }); + + public static final CoreFn EMPTY = reg(new CoreFn<>(Symbols.EMPTY) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell o = args[0]; + + // emptying nil is still nil + if (o == null) return context.withResult(Juice.SIMPLE_FN, null); + + ADataStructure coll = RT.ensureDataStructure(o); + if (coll == null) return context.withCastError(0,args, Types.DATA_STRUCTURE); + + ACell result = coll.empty(); + return context.withResult(Juice.SIMPLE_FN, result); + } + }); + + public static final CoreFn NTH = reg(new CoreFn<>(Symbols.NTH) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + // Arity 2 + if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); + + // First argument must be a countable data structure + ACell arg = (ACell) args[0]; + Long n = RT.count(arg); + if (n == null) return context.withCastError(arg, Types.SEQUENCE); + + // Second argument should be a Long index + CVMLong ix = RT.ensureLong(args[1]); + if (ix == null) return context.withCastError(1,args, Types.LONG); + + long i=ix.longValue(); + + // BOUNDS error if access is out of bounds + if ((i < 0) || (i >= n)) return context.withBoundsError(i); + + // We know the object is a countable collection, so safe to use 'nth' + ACell result = RT.nth(arg, i); + + return context.withResult(Juice.SIMPLE_FN, result); + } + }); + + public static final CoreFn> NEXT = reg(new CoreFn<>(Symbols.NEXT) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ASequence seq = RT.sequence(args[0]); + if (seq == null) return context.withCastError(0,args, Types.SEQUENCE); + + ASequence result = seq.next(); + // TODO: probably needs to cost a lot? + return context.withResult(Juice.SIMPLE_FN, result); + } + }); + + public static final CoreFn RECUR = reg(new CoreFn<>(Symbols.RECUR) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + // any arity OK? + + AExceptional result = RecurValue.wrap(args); + + return context.withException(Juice.RECUR, result); + } + }); + + public static final CoreFn TAILCALL_STAR = reg(new CoreFn<>(Symbols.TAILCALL_STAR) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int n=args.length; + if (n < 1) return context.withArityError(this.minArityMessage(1, n)); + + AFn f=RT.ensureFunction(args[0]); + if (f==null) return context.withCastError(0, args, Types.FUNCTION); + + ACell[] tailArgs=Arrays.copyOfRange(args, 1, args.length); + AExceptional result = TailcallValue.wrap(f,tailArgs); + + return context.withException(Juice.RECUR, result); + } + }); + + public static final CoreFn ROLLBACK = reg(new CoreFn<>(Symbols.ROLLBACK) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + AExceptional result = RollbackValue.wrap((ACell)args[0]); + + return context.withException(Juice.RETURN, result); + } + }); + + public static final CoreFn HALT = reg(new CoreFn<>(Symbols.HALT) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int n = args.length; + if (n > 1) return context.withArityError(this.maxArityMessage(1, n)); + + AExceptional result = HaltValue.wrap((n > 0) ? (ACell)args[0] : null); + + return context.withException(Juice.RETURN, result); + } + }); + + public static final CoreFn RETURN = reg(new CoreFn<>(Symbols.RETURN) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + AExceptional result = ReturnValue.wrap((ACell)args[0]); + return context.withException(Juice.RETURN, result); + } + }); + + public static final CoreFn FAIL = reg(new CoreFn<>(Symbols.FAIL) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int alen = args.length; + if (alen > 2) return context.withArityError(maxArityMessage(2, alen)); + + // default to :ASSERT if no error code provided. Error code cannot be nil. + ACell code = (alen == 2) ? (ACell)args[0] : ErrorCodes.ASSERT; + if (code==null) return context.withError(ErrorCodes.ARGUMENT,"Error code cannot be nil"); + + // get message, or nil if not provided + ACell message = (alen >0) ? (ACell)args[alen-1] : null; + ErrorValue error = ErrorValue.createRaw(code, message); + + return context.withError(error); + } + }); + + public static final CoreFn APPLY = reg(new CoreFn<>(Symbols.APPLY) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + int alen = args.length; + if (alen < 2) return context.withArityError(minArityMessage(2, alen)); + + final AFn fn = RT.castFunction(args[0]); + if (fn==null ) return context.withCastError(0,args, Types.FUNCTION); + + int lastIndex=alen-1; + ACell lastArg = args[lastIndex]; + ASequence coll = RT.ensureSequence(lastArg); + if (coll == null) return context.withCastError(lastIndex,args, Types.SEQUENCE); + + int vlen = coll.size(); // variable arg length + + // Build an array of arguments for the function + // TODO: bounds on number of arguments? + int n = (alen - 2) + vlen; // number of args to pass to function + ACell[] applyArgs; + if (alen > 2) { + applyArgs = new ACell[n]; + for (int i = 0; i < (alen - 2); i++) { + applyArgs[i] = args[i + 1]; + } + int ix = alen - 2; + for (Iterator it = coll.iterator(); it.hasNext();) { + applyArgs[ix++] = it.next(); + } + } else { + applyArgs = coll.toCellArray(); + } + + Context rctx = context.invoke(fn, applyArgs); + return rctx.consumeJuice(Juice.APPLY); + } + }); + + public static final CoreFn> INTO = reg(new CoreFn<>(Symbols.INTO) { + + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); + + ACell a0 = args[0]; + ADataStructure result = RT.ensureDataStructure(a0); + if ((a0 != null) && (result == null)) return context.withCastError(0,args, Types.DATA_STRUCTURE); + + long juice = Juice.BUILD_DATA; + ACell a1 = args[1]; + if (a0 == null) { + // First argument is null. Just keep second arg as complete data structure + result = RT.ensureDataStructure(a1); + if ((a1 != null) && (result == null)) return context.withCastError(a1, Types.DATA_STRUCTURE); + } else { + Long n=RT.count(a1); + if (n == null) return context.withCastError(a1, Types.DATA_STRUCTURE); + + // check juice before running potentially expansive computation + juice += Juice.BUILD_PER_ELEMENT * n; + if (!context.checkJuice(juice)) return context.withJuiceError(); + + ASequence seq = RT.sequence(a1); + if (seq == null) return context.withCastError(a1, Types.DATA_STRUCTURE); + + result = result.conjAll(seq); + if (result == null) return context.withError(ErrorCodes.ARGUMENT,"Invalid element type for 'into'"); + } + + return context.withResult(juice, result); + } + }); + + public static final CoreFn> MERGE = reg(new CoreFn<>(Symbols.MERGE) { + + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + int n=args.length; + if (n==0) return context.withResult(Juice.BUILD_DATA,Maps.empty()); + + // TODO: handle blobmaps? + + ACell arg0=args[0]; + AHashMap result=RT.ensureHashMap(arg0); + if (result == null) return context.withCastError(arg0, Types.MAP); + + long juice=Juice.BUILD_DATA; + for (int i=1; i argMap=RT.ensureHashMap(argi); + if (argMap == null) return context.withCastError(argi, Types.MAP); + + long size=argMap.count(); + juice=Juice.addMul(juice,size,Juice.BUILD_PER_ELEMENT); + + if (!context.checkJuice(juice)) return context.withJuiceError(); + + result=result.merge(argMap); + } + + return context.withResult(juice, result); + } + + }); + + public static final CoreFn> MAP = reg(new CoreFn<>(Symbols.MAP) { + @SuppressWarnings("unchecked") + @Override + public Context> invoke(Context context, ACell[] args) { + if (args.length < 2) return context.withArityError(minArityMessage(2, args.length)); + + // check and cast first argument to a function + ACell fnArg = args[0]; + AFn f = RT.castFunction(fnArg); + if (f == null) return context.withCastError(fnArg, Types.FUNCTION); + + // remaining arguments determine function arity to use + int fnArity = args.length - 1; + ACell[] xs = new ACell[fnArity]; + ASequence[] seqs = new ASequence[fnArity]; + + int length = Integer.MAX_VALUE; + for (int i = 0; i < fnArity; i++) { + ACell maybeSeq = args[1 + i]; + ASequence seq = RT.sequence(maybeSeq); + if (seq == null) return context.withCastError(maybeSeq, Types.SEQUENCE); + seqs[i] = seq; + length = Math.min(length, seq.size()); + } + + final long juice = Juice.addMul(Juice.MAP, Juice.BUILD_DATA , length); + if (!context.checkJuice(juice)) return context.withJuiceError(); + + ArrayList al = new ArrayList<>(); + for (int i = 0; i < length; i++) { + for (int j = 0; j < fnArity; j++) { + xs[j] = seqs[j].get(i); + } + context = (Context) context.invoke(f, xs); + if (context.isExceptional()) return (Context>) context; + ACell r = context.getResult(); + al.add(r); + } + + ASequence result = Vectors.create(al); + return context.withResult(juice, result); + } + }); + + public static final CoreFn REDUCE = reg(new CoreFn<>(Symbols.REDUCE) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context ctx, ACell[] args) { + int ac=args.length; + if ((ac<2)||(ac > 3)) return ctx.withArityError(exactArityMessage(3, ac)); + + // check and cast first argument to a function + ACell fnArg = args[0]; + AFn fn = RT.castFunction(fnArg); + if (fn == null) return ctx.withCastError(0,args, Types.FUNCTION); + + + // last arg must be a data structure + ACell maybeSeq = (ACell) args[ac-1]; + ADataStructure seq = (maybeSeq==null)?Vectors.empty():RT.ensureDataStructure(maybeSeq); + if (seq == null) return ctx.withCastError(ac-1,args, Types.SEQUENCE); + long n = seq.count(); + + ACell result; // Initial value, can be anything + long start=0; // first element for reduction + if (ac==3) { + result=args[1]; + } else { + // 2 arg form of reduce must apply function directly to 0 or 1 elements + int initial=(int)Math.min(2,n); // number of initial arguments to consume + if (initial==0) { + return reduceResult(ctx.invoke(fn, ACell.EMPTY_ARRAY)); + } else if (initial==1) { + return reduceResult(ctx.invoke(fn, new ACell[] {seq.get(0)})); + } + result=seq.get(0); + start = 1; + } + + // Need to reduce over remaining elements + ACell[] xs = new ACell[2]; // accumulator, next element + for (long i = start; i < n; i++) { + xs[0] = result; + xs[1] = seq.get(i); + ctx = ctx.invoke(fn, xs); + if (ctx.isExceptional()) { + return reduceResult(ctx); + } else { + result=ctx.getResult(); + } + } + + return ctx.withResult(Juice.REDUCE, result); + } + }); + + // Helper function for reduce + private static final Context reduceResult(Context ctx) { + Object ex=ctx.getValue(); // might be an ACell or Exception. We need to check for a Reduced result only + if (ex instanceof Reduced) { + ctx=ctx.withResult(((Reduced)ex).getValue()); + } + return ctx.consumeJuice(Juice.REDUCE); // bail out with exception + } + + public static final CoreFn REDUCED = reg(new CoreFn<>(Symbols.REDUCED) { + @SuppressWarnings("unchecked") + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + AExceptional result = Reduced.wrap((ACell) args[0]); + return context.withException(Juice.RETURN, result); + } + }); + + // ===================================================================================================== + // Predicates + + public static final CorePred NIL_Q = reg(new CorePred(Symbols.NIL_Q) { + @Override + public boolean test(ACell val) { + return val == null; + } + }); + + public static final CorePred VECTOR_Q = reg(new CorePred(Symbols.VECTOR_Q) { + @Override + public boolean test(ACell val) { + return val instanceof AVector; + } + }); + + public static final CorePred LIST_Q = reg(new CorePred(Symbols.LIST_Q) { + @Override + public boolean test(ACell val) { + return val instanceof AList; + } + }); + + public static final CorePred SET_Q = reg(new CorePred(Symbols.SET_Q) { + @Override + public boolean test(ACell val) { + return val instanceof ASet; + } + }); + + public static final CorePred MAP_Q = reg(new CorePred(Symbols.MAP_Q) { + @Override + public boolean test(ACell val) { + return val instanceof AMap; + } + }); + + public static final CorePred COLL_Q = reg(new CorePred(Symbols.COLL_Q) { + @Override + public boolean test(ACell val) { + return val instanceof ADataStructure; + } + }); + + public static final CorePred EMPTY_Q = reg(new CorePred(Symbols.EMPTY_Q) { + @Override + public boolean test(ACell val) { + // consider null as an empty object + // like with clojure + if (val == null) return true; + + return (val instanceof ADataStructure) && ((ADataStructure) val).isEmpty(); + } + }); + + public static final CorePred SYMBOL_Q = reg(new CorePred(Symbols.SYMBOL_Q) { + @Override + public boolean test(ACell val) { + return val instanceof Symbol; + } + }); + + public static final CorePred KEYWORD_Q = reg(new CorePred(Symbols.KEYWORD_Q) { + @Override + public boolean test(ACell val) { + return val instanceof Keyword; + } + }); + + public static final CorePred BLOB_Q = reg(new CorePred(Symbols.BLOB_Q) { + @Override + public boolean test(ACell val) { + if (!(val instanceof ABlob)) return false; + return ((ABlob)val).isRegularBlob(); + } + }); + + public static final CorePred ADDRESS_Q = reg(new CorePred(Symbols.ADDRESS_Q) { + @Override + public boolean test(ACell val) { + return val instanceof Address; + } + }); + + public static final CorePred LONG_Q = reg(new CorePred(Symbols.LONG_Q) { + @Override + public boolean test(ACell val) { + return val instanceof CVMLong; + } + }); + + public static final CorePred STR_Q = reg(new CorePred(Symbols.STR_Q) { + @Override + public boolean test(ACell val) { + return val instanceof AString; + } + }); + + public static final CorePred NUMBER_Q = reg(new CorePred(Symbols.NUMBER_Q) { + @Override + public boolean test(ACell val) { + return RT.isNumber(val); + } + }); + + public static final CorePred NAN_Q = reg(new CorePred(Symbols.NAN_Q) { + @Override + public boolean test(ACell val) { + return RT.isNaN(val); + } + }); + + public static final CorePred FN_Q = reg(new CorePred(Symbols.FN_Q) { + @Override + public boolean test(ACell val) { + return val instanceof AFn; + } + }); + + public static final CorePred ZERO_Q = reg(new CorePred(Symbols.ZERO_Q) { + @Override + public boolean test(ACell val) { + if (!RT.isNumber(val)) return false; + INumeric n = RT.ensureNumber(val); + + // According to the IEEE 754 standard, negative zero and positive zero should + // compare as equal with the usual (numerical) comparison operators + // This is the behaviour in Java + return n.doubleValue() == 0.0; + } + }); + + + + // ===================================================================================================== + // Core environment generation + + static Symbol symbolFor(ACell o) { + if (o instanceof CoreFn) return ((CoreFn) o).getSymbol(); + throw new Error("Cant get symbol for object of type " + o.getClass()); + } + + private static AHashMap register(AHashMap env, ACell o) { + Symbol sym = symbolFor(o); + assert (!env.containsKey(sym)) : "Duplicate core declaration: " + sym; + return env.assoc(sym, o); + } + + /** + * Bootstrap procedure to load the core.cvx library + * + * @param env Initial environment map + * @return Loaded environment map + * @throws IOException + */ + private static Context registerCoreCode(AHashMap env) throws IOException { + + //Awe use a fake state to build the initial environment with core address. + Address ADDR = Address.ZERO; + State state = State.EMPTY.putAccount(ADDR, AccountStatus.createActor()); + Context ctx = Context.createFake(state, ADDR); + + // Map in forms from env. + for (Map.Entry me : env.entrySet()) { + ctx=ctx.define(me.getKey(), me.getValue()); + } + + ACell form = null; + + // Compile and execute forms in turn. Later definitions can use earlier macros! + AList forms = Reader.readAll(Utils.readResourceAsString("convex/core.cvx")); + for (ACell f : forms) { + form = f; + ctx = ctx.expandCompile(form); + if (ctx.isExceptional()) { + throw new Error("Error compiling form: " + form + "\nException : " + ctx.getExceptional()); + } + AOp op = (AOp)ctx.getResult(); + ctx = ctx.execute(op); + // System.out.println("Core compilation juice: "+ctx.getJuice()); + assert (!ctx.isExceptional()) : "Error executing op: "+ op+ "\nException : "+ ctx.getExceptional().toString(); + } + + return ctx; + } + + @SuppressWarnings("unchecked") + private static Context applyDocumentation(Context ctx) throws IOException { + + AMap> metas = Reader.read(Utils.readResourceAsString("convex/core/metadata.cvx")); + + for (Map.Entry> entry : metas.entrySet()) { + try { + Symbol sym = entry.getKey(); + AHashMap meta = entry.getValue(); + MapEntry definedEntry = ctx.getEnvironment().getEntry(sym); + + if (definedEntry == null) { + // No existing value, might be a special. + AHashMap doc = (AHashMap) meta.get(Keywords.DOC); + if (doc == null) { + // No docs. + System.err.println("CORE WARNING: Missing :doc tag in metadata for: " + sym); + continue; + } else { + if (meta.get(Keywords.SPECIAL_Q) == CVMBool.TRUE) { + // Create a fake entry for special symbols. + ctx=ctx.define(sym, sym); + definedEntry = MapEntry.create(sym, sym); + } else { + System.err.println("CORE WARNING: Documentation for non-existent core symbol: " + sym); + continue; + } + } + } + + ctx = ctx.defineWithSyntax(Syntax.create(sym, meta), definedEntry.getValue()); + } catch (Throwable ex) { + throw new Error("Error applying documentation: " + entry, ex); + } + } + + return ctx; + } + + + + + static { + // Set up `convex.core` environment + AHashMap coreEnv = Maps.empty(); + AHashMap> coreMeta = Maps.empty(); + + try { + + // Register all objects from registered runtime + for (ACell o : tempReg) { + coreEnv = register(coreEnv, o); + } + + Context ctx = registerCoreCode(coreEnv); + ctx=applyDocumentation(ctx); + + coreEnv = ctx.getEnvironment(); + coreMeta = ctx.getMetadata(); + + METADATA = coreMeta; + ENVIRONMENT = coreEnv; + } catch (Throwable e) { + e.printStackTrace(); + throw new Error("Error initialising core!",e); + } + } +} diff --git a/convex-core/src/main/java/convex/core/lang/IFn.java b/convex-core/src/main/java/convex/core/lang/IFn.java new file mode 100644 index 000000000..cd26d5e62 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/IFn.java @@ -0,0 +1,25 @@ +package convex.core.lang; + +import convex.core.data.ACell; + +/** + * Interface for invokable objects with function interface. + * + * "Any sufficiently advanced technology is indistinguishable from magic." - + * Arthur C. Clarke + * + * @param Return type of function + */ +public interface IFn { + + /** + * Invoke this function in the given context. + * + * @param context Context in which the function is to be executed + * @param args Arguments to the function + * @return Context containing result of function invocation, or an exceptional + * value + */ + public abstract Context invoke(Context context, ACell[] args); + +} diff --git a/convex-core/src/main/java/convex/core/lang/Juice.java b/convex-core/src/main/java/convex/core/lang/Juice.java new file mode 100644 index 000000000..2e95cc492 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/Juice.java @@ -0,0 +1,332 @@ +package convex.core.lang; + +/** + * Static class defining juice costs for executable operations. + * + * "LISP programmers know the value of everything and the cost of nothing." - + * Alan Perlis + * + */ +public class Juice { + /** + * Juice required to resolve a constant value + * + * Very cheap, no allocs / lookup. + */ + public static final long CONSTANT = 10; + + /** + * Juice required to define a value in the current environment. + * + * We make this somewhat expensive - we want to discourage over-use as a general rule + * since it writes to global chain state. However memory accounting helps discourage + * superfluous defs, so it only needs to reflect execution cost. + */ + public static final long DEF = 100; + + /** + * Juice required to look up a value in the local environment. + */ + public static final long LOOKUP = 15; + + /** + * Juice required to look up a value in the dynamic environment. + * + * Potentially a bit pricey since read only, but might hit storage so..... + */ + public static final long LOOKUP_DYNAMIC = 40; + + /** + * Juice required to look up a symbol with a regular Address + */ + public static final long LOOKUP_SYM = LOOKUP_DYNAMIC+CONSTANT; + + /** + * Juice required to execute a Do block + * + * Very cheap, no allocs. + */ + public static final long DO = 10; + + + /** + * Juice required to execute a Let block + * + * Fairly cheap but some parameter munging required. Might revisit binding + * costs? + */ + public static final long LET = 30; + + + + /** + * Juice required to execute a Cond expression + * + * Pretty cheap, nothing nasty here (though conditions / results themselves + * might get pricey). + */ + public static final long COND_OP = 20; + + /** + * Juice required to create a lambda + * + * Sort of expensive - might allocate a bunch of stuff for the closure? + */ + public static final long LAMBDA = 100; + + /** + * Juice required to call an Actor + * + * Slightly expensive for context switching? + */ + public static final long CALL_OP = 100; + + /** + * Juice required to build a data structure. Make a bit expensive? + */ + protected static final long BUILD_DATA = 50; + + /** + * Juice required per element changed when building a data structure. Map entries + * count as two elements. + * + * We need to be a bit harsh on this! Risk of consuming too much heap space, + * might also result in multiple allocs for tree structures. + */ + protected static final long BUILD_PER_ELEMENT = 50; + + protected static final long MAP = 100; + protected static final long REDUCE = 100; + + /** + * Juice for general object equality comparison + * + * Pretty cheap. + */ + public static final long EQUALS = 20; + + /** + * Juice for numeric comparison + * + * Pretty cheap. Bit of casting perhaps. + */ + public static final long NUMERIC_COMPARE = 20; + + /** + * Juice for an apply operation + * + * Bit of cost to allow for parameter construction. Might need to revisit for + * bigger sequences? + */ + public static final long APPLY = 50; + + /** + * Juice for a cryptographic hash + * + * Expensive. + */ + public static final long HASH = 10000; + + /** + * Juice for a very cheap operation. O(1), no new cell allocations or non-trivial lookups. + */ + public static final long CHEAP_OP = 10; + + /** + * Juice for a simple built-in core function. Simple operations are assumed to + * require no expensive resource access, and operate with O(1) allocations + */ + public static final long SIMPLE_FN = 20; + + /** + * Juice for constructing a String + * + * Fairly cheap, since mostly in fast code, but charge extra for additional + * chars. + */ + protected static final long STR = SIMPLE_FN; + protected static final long STR_PER_CHAR = 5; + + /** + * Juice for storing a new constant value permanently in on-chain state Charged + * per node stored + */ + protected static final long STORE = 1000; + + protected static final long FETCH = 100; + + protected static final long ARITHMETIC = SIMPLE_FN; + + protected static final long ADDRESS = 100; + + protected static final long BALANCE = 200; + + /** + * Juice for creation of a blob + */ + protected static final long BLOB = 100; + protected static final long BLOB_PER_BYTE = 1; + + protected static final long GET = 30; + + protected static final long KEYWORD = 50; + + protected static final long SYMBOL = 50; + + public static final long TRANSFER = 100; + + public static final long SIMPLE_MACRO = 200; + + /** + * Juice for a recur form + * + * Fairly cheap, might have to construct some temp structures for recur + * arguments. + */ + public static final long RECUR = 30; + + /** + * Juice for a contract deployment + * + * Make this quite expensive, mainly to deter lots of willy-nilly deploying + */ + public static final long DEPLOY_CONTRACT = 1000; + + /** + * Probably should be expensive? + */ + protected static final long EVAL = 500; + + // Juice amounts for compiler. TODO: figure out if compile / eval should be + // allowed on-chain + + /** + * Juice cost to compile a Constant value + */ + public static final long COMPILE_CONSTANT = 30; + + /** + * Juice cost to compile a Constant value + */ + public static final long COMPILE_LOOKUP = 50; + + /** + * Juice cost to compile a general AST node + */ + public static final long COMPILE_NODE = 200; + + /** + * Juice cost to expand a constant + */ + public static final long EXPAND_CONSTANT = 40; + + /** + * Juice cost to expand a sequence + */ + public static final long EXPAND_SEQUENCE = 100; + + /** + * Juice cost to schedule + */ + public static final long SCHEDULE = 800; + + /** + * Default future schedule juice (10 per hour) + * + * This makes scheduling a few hours / days ahead cheap but year is quite + * expensive (~87,600). Also places an upper bound on advance schedules. + * + * TODO: review this + */ + public static final long SCHEDULE_MILLIS_PER_JUICE_UNIT = 360000; + + + /** + * Juice required to execute an exceptional return (return, halt, rollback etc.) + * + * Pretty cheap, one alloc and a bit of exceptional value handling. + */ + public static final long RETURN = 50; + + /** + * Juice cost for accepting an offer of Convex coins. + * + * We make this a little expensive because it involves updating two separate accounts. + */ + public static final long ACCEPT = 200; + + /** + * Juice cost for constructing a Syntax Object. Fairly lightweight. + */ + public static final long SYNTAX = Juice.SIMPLE_FN; + + /** + * Juice cost for extracting metadata from a Syntax object. + */ + public static final long META = Juice.CHEAP_OP; + + /** + * Juice cost for an 'assoc' + */ + public static final long ASSOC = Juice.BUILD_DATA+Juice.BUILD_PER_ELEMENT*2; + + /** + * Variable Juice cost for set comparison + */ + public static final long SET_COMPARE_PER_ELEMENT = 10; + + public static final long CREATE_ACCOUNT = 100; + + public static final long QUERY = Juice.CHEAP_OP; + + public static final long LOG = 100; + + public static final long SPECIAL = Juice.CHEAP_OP; + + public static final long SET_BANG = 20; + + /** + * Make this quite expensive. Discourage spamming Peer updates + */ + public static final long PEER_UPDATE = 1000; + + /** + * Saturating multiply and add: result = a + (b * c) + * + * Returns Long.MAX_VALUE on overflow. + * + * @param a First number (to be added) + * @param b Second number (to be multiplied) + * @param c Thirst number (to be multiplied) + * @return long result, capped at Long.MAX_VALUE + */ + public static final long addMul(long a, long b, long c) { + return add(a,mul(b,c)); + } + + /** + * Saturating multiply. Returns Long.MAX_VALUE on overflow. + * @param a First number + * @param b Second number + * @return long result, capped at Long.MAX_VALUE + */ + public static final long mul(long a, long b) { + if ((a<0)||(b<0)) return Long.MAX_VALUE; + if (Math.multiplyHigh(a, b)>0) return Long.MAX_VALUE; + return a*b; + } + + /** + * Saturating addition. Returns Long.MAX_VALUE on overflow. + * @param a First number + * @param b Second number + * @return long result, capped at Long.MAX_VALUE + */ + public static final long add(long a, long b) { + if ((a<0)||(b<0)) return Long.MAX_VALUE; + if ((a+b)<0) return Long.MAX_VALUE; + return a+b; + } + + +} diff --git a/convex-core/src/main/java/convex/core/lang/Ops.java b/convex-core/src/main/java/convex/core/lang/Ops.java new file mode 100644 index 000000000..007c7105b --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/Ops.java @@ -0,0 +1,94 @@ +package convex.core.lang; + +import java.nio.ByteBuffer; + +import convex.core.data.ACell; +import convex.core.exceptions.BadFormatException; +import convex.core.lang.ops.Cond; +import convex.core.lang.ops.Constant; +import convex.core.lang.ops.Def; +import convex.core.lang.ops.Do; +import convex.core.lang.ops.Invoke; +import convex.core.lang.ops.Lambda; +import convex.core.lang.ops.Let; +import convex.core.lang.ops.Local; +import convex.core.lang.ops.Lookup; +import convex.core.lang.ops.Query; +import convex.core.lang.ops.Special; +import convex.core.util.Utils; + +/** + * Static utility class for coded operations. + * + * Ops are the fundamental units of code (e.g. as used to implement Actors), and may be + * effectively considered as "bytecode" for the decentralised state machine. + */ +public class Ops { + public static final byte CONSTANT = 1; + public static final byte INVOKE = 2; + public static final byte DO = 3; + public static final byte COND = 4; + public static final byte LOOKUP = 5; + public static final byte DEF = 6; + public static final byte LAMBDA = 7; + public static final byte LET = 8; + public static final byte QUERY = 9; + public static final byte LOOP = 10; + public static final byte LOCAL=11; + public static final byte SET = 12; + // public static final byte CALL = 9; + // public static final byte RETURN = 10; + + public static final byte SPECIAL_BASE = 64; + + + + /** + * Reads an Op from the given ByteBuffer. Assumes Message tag already read. + * + * @param The return type of the Op + * @param bb ByteBuffer + * @return Op read from ByteBuffer + * @throws BadFormatException If encoding is invalid + */ + @SuppressWarnings("unchecked") + public static AOp read(ByteBuffer bb) throws BadFormatException { + byte opCode = bb.get(); + switch (opCode) { + case Ops.INVOKE: + return Invoke.read(bb); + case Ops.COND: + return Cond.read(bb); + case Ops.CONSTANT: + return Constant.read(bb); + case Ops.DEF: + return Def.read(bb); + case Ops.DO: + return Do.read(bb); + case Ops.LOOKUP: + return Lookup.read(bb); + // case Ops.CALL: return Call.read(bb); + case Ops.LAMBDA: + return (AOp) Lambda.read(bb); + case Ops.LET: + return Let.read(bb,false); + case Ops.QUERY: + return Query.read(bb); + case Ops.LOOP: + return Let.read(bb,true); + case Ops.LOCAL: + return Local.read(bb); + + // case Ops.RETURN: return (AOp) Return.read(bb); + default: + // range 64-127 is special ops + if ((opCode&0xC0) == 0x40) { + Special special=(Special) Special.create(opCode); + if (special==null) throw new BadFormatException("Bad OpCode for Special value: "+Utils.toHexString((byte)opCode)); + return special; + } + + throw new BadFormatException("Invalide OpCode: " + opCode); + } + } +} diff --git a/convex-core/src/main/java/convex/core/lang/RT.java b/convex-core/src/main/java/convex/core/lang/RT.java new file mode 100644 index 000000000..64fd87824 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/RT.java @@ -0,0 +1,1381 @@ +package convex.core.lang; + +import java.lang.reflect.Array; +import java.util.function.BiFunction; + +import convex.core.Constants; +import convex.core.data.ABlob; +import convex.core.data.ACell; +import convex.core.data.ACollection; +import convex.core.data.ACountable; +import convex.core.data.ADataStructure; +import convex.core.data.AHashMap; +import convex.core.data.AList; +import convex.core.data.AMap; +import convex.core.data.ASequence; +import convex.core.data.ASet; +import convex.core.data.AString; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.Address; +import convex.core.data.Blob; +import convex.core.data.Blobs; +import convex.core.data.Hash; +import convex.core.data.INumeric; +import convex.core.data.Keyword; +import convex.core.data.Lists; +import convex.core.data.MapEntry; +import convex.core.data.Maps; +import convex.core.data.Ref; +import convex.core.data.Sets; +import convex.core.data.Strings; +import convex.core.data.Symbol; +import convex.core.data.Vectors; +import convex.core.data.prim.APrimitive; +import convex.core.data.prim.CVMBool; +import convex.core.data.prim.CVMByte; +import convex.core.data.prim.CVMChar; +import convex.core.data.prim.CVMDouble; +import convex.core.data.prim.CVMLong; +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.impl.KeywordFn; +import convex.core.lang.impl.MapFn; +import convex.core.lang.impl.SeqFn; +import convex.core.lang.impl.SetFn; +import convex.core.util.Utils; + +/** + * Static utility class for Runtime functions. Mostly low-level support for Core + * language capabilities, which will be wrapped as functions in the initial + * execution environment. + * + * "Low-level programming is good for the programmer's soul." — John Carmack + */ +public class RT { + + /** + * Returns true if all elements in an array are equal. Nulls are equal to null + * only. + * + * @param Type of values + * @param values Array of values + * @return True if all values are equal + */ + public static Boolean allEqual(T[] values) { + for (int i = 0; i < values.length - 1; i++) { + if (!Utils.equals(values[i], values[i + 1])) return false; + } + return true; + } + + // Numerical comparison functions + + /** + * Check if the values passed are a short (length 0 or 1) array of numbers which + * is a special case for comparison operations. + * + * @return Boolean result, or null if the values are not comparable + */ + private static Boolean checkShortCompare(ACell[] values) { + int len = values.length; + if (len == 0) return true; + if (len == 1) { + if (null == RT.ensureNumber(values[0])) return null; // cast failure + return true; + } + return false; + } + + public static Boolean eq(ACell[] values) { + Boolean check = checkShortCompare(values); + if (check == null) return null; + if (check) return true; + for (int i = 0; i < values.length - 1; i++) { + Long comp = RT.compare(values[i], values[i + 1],Long.MAX_VALUE); + if (comp == null) return null; // cast error + if (comp != 0) return false; + } + return true; + } + + public static Boolean ge(ACell[] values) { + Boolean check = checkShortCompare(values); + if (check == null) return null; + if (check) return true; + for (int i = 0; i < values.length - 1; i++) { + Long comp = RT.compare(values[i], values[i + 1],Long.MIN_VALUE); + if (comp == null) return null; // cast error + if (comp < 0) return false; + } + return true; + } + + public static Boolean gt(ACell[] values) { + Boolean check = checkShortCompare(values); + if (check == null) return null; + if (check) return true; + for (int i = 0; i < values.length - 1; i++) { + Long comp = RT.compare(values[i], values[i + 1],Long.MIN_VALUE); + if (comp == null) return null; // cast error + if (comp <= 0) return false; + } + return true; + } + + public static Boolean le(ACell[] values) { + Boolean check = checkShortCompare(values); + if (check == null) return null; + if (check) return true; + for (int i = 0; i < values.length - 1; i++) { + Long comp = RT.compare(values[i], values[i + 1],Long.MAX_VALUE); + if (comp == null) return null; // cast error + if (comp > 0) return false; + } + return true; + } + + public static Boolean lt(ACell[] values) { + Boolean check = checkShortCompare(values); + if (check == null) return null; + if (check) return true; + for (int i = 0; i < values.length - 1; i++) { + Long comp = RT.compare(values[i], values[i + 1],Long.MAX_VALUE); + if (comp == null) return null; // cast error + if (comp >= 0) return false; + } + return true; + } + + /** + * Get the target common numeric type for a given set of arguments. - Integers + * upcast to Long - Anything else upcasts to Double + * + * @param args Argument array + * + * @return The target numeric type, or null if there is a non-numeric argument + */ + public static Class commonNumericType(ACell[] args) { + Class highestFound=Long.class; + for (int i = 0; i < args.length; i++) { + ACell a = args[i]; + Class klass = numericType(a); + if (klass == null) return null; // break if non-numeric + if (klass == Double.class) highestFound=Double.class; + } + return highestFound; + } + + /** + * Finds the first non-numeric value in an array. Used for error reporting. + * + * @param args Argument array + * @return First non-numeric value, or null if not found. + */ + public static int findNonNumeric(ACell[] args) { + for (int i = 0; i < args.length; i++) { + ACell a = args[i]; + Class klass = numericType(a); + if (klass == null) return i; + } + return -1; + } + + /** + * Gets the numeric class of an object + * + * @param a Numerical value + * @return Long.class or Double.class if cast possible, or null if not numeric. + */ + public static Class numericType(ACell a) { + if (a instanceof INumeric) { + return ((INumeric)a).numericType(); + } + return null; + } + + public static APrimitive plus(ACell[] args) { + Class type = commonNumericType(args); + if (type == null) return null; + if (type == Double.class) return plusDouble(args); + long result = 0; + for (int i = 0; i < args.length; i++) { + result += RT.longValue(args[i]); + } + return CVMLong.create(result); + } + + public static CVMDouble plusDouble(ACell[] args) { + double result = 0; + for (int i = 0; i < args.length; i++) { + result += RT.doubleValue(args[i]); + } + return CVMDouble.create(result); + } + + public static APrimitive minus(ACell[] args) { + Class type = commonNumericType(args); + if (type == null) return null; + if (type == Double.class) return minusDouble(args); + int n = args.length; + long result = longValue(args[0]); + if (n == 1) result= -result; + for (int i = 1; i < n; i++) { + result -= RT.longValue(args[i]); + } + return CVMLong.create(result); + } + + public static APrimitive minusDouble(ACell[] args) { + int n = args.length; + double result = doubleValue(args[0]); + if (n == 1) result= -result; + for (int i = 1; i < args.length; i++) { + result -= RT.doubleValue(args[i]); + } + return CVMDouble.create(result); + } + + public static APrimitive times(ACell[] args) { + Class type = commonNumericType(args); + if (type == null) return null; + if (type == Double.class) return timesDouble(args); + long result = 1; + for (int i = 0; i < args.length; i++) { + result *= RT.longValue(args[i]); + } + return CVMLong.create(result); + } + + public static APrimitive timesDouble(ACell[] args) { + double result = 1; + for (int i = 0; i < args.length; i++) { + result *= RT.doubleValue(args[i]); + } + return CVMDouble.create(result); + } + + public static CVMDouble divide(ACell[] args) { + int n = args.length; + CVMDouble arg0 = ensureDouble(args[0]); + if (arg0 == null) return null; + double result=arg0.doubleValue(); + + if (n == 1) return CVMDouble.create(1.0 / result); + for (int i = 1; i < args.length; i++) { + CVMDouble v = ensureDouble(args[i]); + if (v == null) return null; + result = result / v.doubleValue(); + } + return CVMDouble.create(result); + } + + /** + * Computes the result of a pow operation. Returns null if a cast fails. + * @param args Argument array, should be length 2 + * @return Result of exponentiation + */ + public static CVMDouble pow(ACell[] args) { + CVMDouble a = ensureDouble(args[0]); + CVMDouble b = ensureDouble(args[1]); + if ((a==null)||(b==null)) return null; + return CVMDouble.create(StrictMath.pow(a.doubleValue(), b.doubleValue())); + } + + /** + * Computes the result of a exp operation. Returns null if a cast fails. + * @param arg Numeric value + * @return Numeric result, or null + */ + public static CVMDouble exp(ACell arg) { + CVMDouble a = ensureDouble(arg); + if (a==null) return null; + return CVMDouble.create(StrictMath.exp(a.doubleValue())); + } + + /** + * Gets the floor a number after casting to a double. Equivalent to java.lang.StrictMath.floor(...) + * + * @param a Numerical Value + * @return The floor of the number, or null if cast fails + */ + public static CVMDouble floor(ACell a) { + CVMDouble d = RT.ensureDouble(a); + if (d == null) return null; + return CVMDouble.create(StrictMath.floor(d.doubleValue())); + } + + /** + * Gets the ceiling a number after casting to a double. Equivalent to java.lang.StrictMath.ceil(...) + * + * @param a Numerical Value + * @return The ceiling of the number, or null if cast fails + */ + public static CVMDouble ceil(ACell a) { + CVMDouble d = RT.ensureDouble(a); + if (d == null) return null; + return CVMDouble.create(StrictMath.ceil(d.doubleValue())); + } + + /** + * Gets the exact positive square root of a number after casting to a double. + * Returns NaN for negative numbers. + * + * @param a Numerical Value + * @return The square root of the number, or null if cast fails + */ + public static CVMDouble sqrt(ACell a) { + CVMDouble d = RT.ensureDouble(a); + if (d == null) return null; + return CVMDouble.create(StrictMath.sqrt(d.doubleValue())); + } + + /** + * Gets the absolute value of a numeric value. Supports double and long. + * + * @param a Numeric CVM value + * @return Absolute value, or null if not a numeric value + */ + public static APrimitive abs(ACell a) { + INumeric x=RT.ensureNumber(a); + if (x==null) return null; + if (x instanceof CVMLong) return CVMLong.create( Math.abs(((CVMLong) x).longValue())); + return CVMDouble.create(Math.abs(x.toDouble().doubleValue())); + } + + /** + * Gets the signum of a numeric value + * + * @param a Numeric value + * @return value of -1, 0 or 1, NaN is argument is NaN, or null if the argument is not numeric + */ + public static ACell signum(ACell a) { + INumeric x=RT.ensureNumber(a); + if (x==null) return null; + return x.signum(); + } + + /** + * Compares two objects representing numbers numerically. + * + * @param a First numeric value + * @param b Second numeric value + * @param nanValue Value to return in case of a NaN result + * @return Less than 0 if a is smaller, greater than 0 if a is larger, 0 if a + * equals b + */ + public static Long compare(ACell a, ACell b,Long nanValue) { + Class ca = numericType(a); + if (ca == null) return null; + Class cb = numericType(b); + if (cb == null) return null; + + if ((ca == Long.class) && (cb == Long.class)) return RT.compare(longValue(a), longValue(b)); + + double da=doubleValue(a); + double db=doubleValue(b); + if (da==db) return 0L; + if (dadb) return 1L; + + return nanValue; + } + + /** + * Compares two long values numerically, according to Java primitive + * comparisons. + * + * @param a First number + * @param b Second number + * @return -1 if a is less than b, 1 if greater, 0 is they are equal + */ + public static long compare(long a, long b) { + if (a < b) return -1; + if (a > b) return 1; + return 0; + } + + /** + * Converts a CVM value to the standard numeric representation. Result will be one of: + *
    + *
  • Long for Byte, Long
  • + *
  • Double for Double
  • + *
  • null for any non-numeric value
  • + *
+ * + * @param a Value to convert to numeric representation + * @return The number value, or null if cannot be converted + */ + public static INumeric ensureNumber(ACell a) { + if (a == null) return null; + + if (a instanceof INumeric) { + return ((INumeric)a).toStandardNumber(); + } + + return null; + } + + /** + * Tests if a Value is a valid numerical value + * @param val Value to test + * @return True if a number, false otherwise + */ + public static boolean isNumber(ACell val) { + return (val instanceof INumeric); + } + + /** + * Increments a Long value + * @param x Value to increment + * @return Long Value, or null if conversion fails + */ + public static CVMLong inc(ACell x) { + CVMLong n = ensureLong(x); + if (n == null) return null; + return CVMLong.create(n.longValue() + 1L); + } + + /** + * Decrements a Long value + * @param x Value to decrement + * @return Long Value, or null if conversion fails + */ + public static CVMLong dec(ACell x) { + CVMLong n = ensureLong(x); + if (n == null) return null; + return CVMLong.create(n.longValue() - 1L); + } + + /** + * Converts a numerical value to a CVM Double. + * @param a Value to cast + * @return Double value, or null if not convertible + */ + public static CVMDouble castDouble(ACell a) { + if (a instanceof CVMDouble) return (CVMDouble) a; + + CVMLong l=castLong(a); + if (l==null) return null; + return l.toDouble(); + } + + /** + * Ensures the argument is a CVM Long value. + * @param a Value to cast + * @return CVMDouble value, or null if not convertible + */ + public static CVMDouble ensureDouble(ACell a) { + if (a instanceof INumeric) { + INumeric ap=(INumeric)a; + return ap.toDouble(); + } + return null; + } + + /** + * Converts a numerical value to a CVM Long. Doubles and floats will be converted if possible. + * @param a Value to cast + * @return Long value, or null if not convertible + */ + public static CVMLong castLong(ACell a) { + if (a instanceof CVMLong) return (CVMLong) a; + INumeric n = ensureNumber(a); + if (n != null) { + return n.toLong(); + }; + + if (a instanceof APrimitive) { + return CVMLong.create(((APrimitive)a).longValue()); + } + + if (a instanceof ABlob) { + long lv=((ABlob)a).toLong(); + return CVMLong.create(lv); + } + + return null; + } + + /** + * Ensures the argument is a CVM Long value. + * @param a Value to cast + * @return CVMLong value, or null if not convertible + */ + public static CVMLong ensureLong(ACell a) { + if (a instanceof CVMLong) return (CVMLong) a; + if (a instanceof INumeric) { + INumeric ap=(INumeric)a; + if (ap.numericType()==Long.class) return ap.toLong(); + } + return null; + } + + /** + * Explicitly converts a numerical value to a CVM Byte. + * + * Doubles and floats will be converted if possible. + * + * @param a Value to cast + * @return Long value, or null if not convertible + */ + public static CVMByte castByte(ACell a) { + if (a instanceof CVMByte) return (CVMByte) a; + CVMLong l=castLong(a); + if (l == null) return null; + return CVMByte.create((byte)l.longValue()); + } + + /** + * Casts a value to a Character + * @param a Value to cast + * @return CVMChar value, or null if cast fails + */ + public static CVMChar toCharacter(ACell a) { + if (a instanceof CVMChar) return (CVMChar) a; + CVMLong l=castLong(a); + if (l == null) return null; + return CVMChar.create(l.longValue()); + } + + private static long longValue(ACell a) { + if (a instanceof APrimitive) return ((APrimitive) a).longValue(); + throw new IllegalArgumentException("Can't convert to long: " + Utils.getClassName(a)); + } + + private static double doubleValue(ACell a) { + if (a instanceof APrimitive) return ((APrimitive) a).doubleValue(); + throw new IllegalArgumentException("Can't convert to double: " + Utils.getClassName(a)); + } + + /** + * Converts any data structure to a vector + * + * @param o Object to attemptto convert to a Vector + * @return AVector instance, or null if not convertible + */ + @SuppressWarnings("unchecked") + public static AVector vec(Object o) { + if (o==null) return Vectors.empty(); + if (o instanceof ACell) return castVector((ACell) o); + + if (o.getClass().isArray()) { + ACell[] arr = Utils.toCellArray(o); + return Vectors.create(arr); + } + + if (o instanceof java.util.List) return Vectors.create((java.util.List) o); + + return null; + } + + /** + * Converts any countable data structure to a vector. Might be O(n) + * + * @param o Value to convert + * @return AVector instance, or null if conversion fails + */ + @SuppressWarnings("unchecked") + public static AVector castVector(ACell o) { + if (o == null) return Vectors.empty(); + if (o instanceof ACollection) return vec((ACollection) o); + if (o instanceof ACountable) { + ACountable ds=(ACountable) o; + long n=ds.count(); + AVector r=Vectors.empty(); + for (int i=0; i ASet castSet(ACell o) { + if (o == null) return Sets.empty(); + if (o instanceof ASet) return (ASet) o; + if (o instanceof ADataStructure) return Sets.create((ADataStructure) o); + return null; + } + + /** + * Converts any collection to a vector. Always succeeds, but may have O(n) cost + * + * Null values are converted to empty vector (considered as empty sequence) + * @param coll Collection to convert to a Vector + * @return Vector instance + */ + public static AVector vec(ACollection coll) { + if (coll == null) return Vectors.empty(); + return coll.toVector(); + } + + + /** + * Converts any collection of cells into a Sequence data structure. + * + * Potentially O(n) in size of collection. + * + * Nulls are converted to an empty vector. + * + * Returns null if conversion is not possible. + * + * @param Type of cell in collection + * @param o An object that contains a collection of cells + * @return An ASequence instance, or null if the argument cannot be converted to + * a sequence + */ + @SuppressWarnings("unchecked") + public static ASequence sequence(ACell o) { + if (o == null) return Vectors.empty(); + if (o instanceof ASequence) return (ASequence) o; + if (o instanceof ACollection) return ((ACollection) o).toVector(); + if (o instanceof AMap) { + // TODO: probably needs fixing! SECURITY + return sequence(((AMap) o).entryVector()); + } + + return null; + } + + /** + * Ensures argument is a sequence data structure. + * + * Nulls are converted to an empty vector. + * + * Returns null if conversion is not possible. + * + * @param Type of sequence elements + * @param o Value to cast to sequence + * @return An ASequence instance, or null if the argument cannot be converted to + * a sequence + */ + @SuppressWarnings("unchecked") + public static ASequence ensureSequence(ACell o) { + if (o == null) return Vectors.empty(); + if (o instanceof ASequence) return (ASequence) o; + return null; + } + + /** + * Gets the nth element from a sequential collection. + * + * Throws an exception if access is out of bounds - caller responsibility to check bounds first + * + * @param Type of element in collection + * @param o Countable Value + * @param i Index of element to get + * @return Element from collection at the specified position + */ + @SuppressWarnings("unchecked") + public static T nth(ACell o, long i) { + // special case, we treat nil as empty sequence + if (o == null) throw new IndexOutOfBoundsException("Can't get nth element from null"); + + if (o instanceof ACountable) return ((ACountable) o).get(i); // blobs, maps and collections + + throw new ClassCastException("Don't know how to get nth item of type "+RT.getType(o)); + } + + /** + * Variant of nth that also handles Java Arrays. Used for destructuring. + * + * @param Return type + * @param o Object to check for indexed element + * @param i Index to check + * @return Element at specified index + */ + @SuppressWarnings("unchecked") + public static T nth(Object o, long i) { + if (o instanceof ACell) return nth((ACell)o,i); + + try { + if (o.getClass().isArray()) { + return (T) Array.get(o, Utils.checkedInt(i)); + } + } catch (IllegalArgumentException e) { + // can come from checkedInt calls + throw new IndexOutOfBoundsException(e.getMessage()); + } + + throw new ClassCastException("Can't get nth element from object of class: " + Utils.getClassName(o)); + } + + /** + * Gets the count of elements in a collection or Java array. Null is considered an empty collection. + * + * @param o An Object representing a collection of items to be counted + * @return The count of elements in the collection, or null if not countable + */ + public static Long count(Object o) { + if (o == null) return 0L; + if (o instanceof ACell) return count((ACell)o); + if (o.getClass().isArray()) { + return (long) Array.getLength(o); + } + return null; + } + + /** + * Gets the count of elements in a countable data structure. Null is considered an empty collection. + * + * @param a Any Cell potentially representing a collection of items to be counted + * @return The count of elements in the collection, or null if not countable + */ + public static Long count(ACell a) { + if (a == null) return 0L; + if (a instanceof ACountable) return ((ACountable) a).count(); + return null; + } + + + + /** + * Converts arguments to an AString representation. Handles: + *
    + *
  • CVM Strings (unchanged)
  • + *
  • Blobs (converted to hex)
  • + *
  • Numbers (converted to canonical numeric representation)
  • + *
  • Other Objects (printed in canonical format) + *
+ * + * @param args Values to convert to String + * @return AString value + */ + public static AString str(ACell[] args) { + // TODO: execution cost limits?? + StringBuilder sb = new StringBuilder(); + for (ACell o : args) { + String s=RT.str(o); + sb.append(s); + } + return Strings.create(sb.toString()); + } + + /** + * Converts a value to a CVM String representation. Required to work for all valid + * types. + * + * @param a Value to convert to a String + * @return String representation of object + */ + public static String str(ACell a) { + if (a == null) return "nil"; + if (a instanceof Blob) return ((Blob)a).toHexString(); + String s = a.toString(); + return s; + } + + /** + * Gets the name from a CVM value. Supports Strings, Keywords and Symbols. + * + * @param a Value to cast to a name + * @return Name of the argument, or null if not Named + */ + public static AString name(ACell a) { + if (a instanceof AString) return (AString) a; + if (a instanceof Keyword) return Strings.create(((Keyword) a).getName()); + if (a instanceof Symbol) return Strings.create(((Symbol) a).getName()); + return null; + } + + /** + * Prepends an element to a sequential data structure. The new element will + * always be in position 0 + * + * @param Type of elements + * @param x Element to prepend + * @param xs Any sequential object, or null (will be treated as empty sequence) + * @return A new list with the cons'ed element at the start + */ + @SuppressWarnings("unchecked") + public static AList cons(T x, ASequence xs) { + if (xs == null) return Lists.of(x); + return ((ASequence) xs).cons(x); + } + + /** + * Prepends two elements to a sequential data structure. The new elements will + * always be in position 0 and 1 + * + * @param Type of elements + * @param x Element to prepend at position 0 + * @param y Element to prepend at position 1 + * @param xs Any sequential object, or null (will be treated as empty sequence) + * @return A new list with the cons'ed elements at the start + */ + public static AList cons(T x, T y, ACell xs) { + ASequence nxs = RT.sequence(xs); + if (xs == null) return Lists.of(x, y); + return nxs.cons(y).cons(x); + } + + /** + * Prepends three elements to a sequential data structure. The new elements will + * always be in position 0, 1 and 2 + * + * @param Type of elements + * @param x Element to prepend at position 0 + * @param y Element to prepend at position 1 + * @param z Element to prepend at position 2 + * @param xs Any sequential object + * @return A new list with the cons'ed elements at the start + */ + public static AList cons(T x, T y, T z, ACell xs) { + ASequence nxs = RT.sequence(xs); + return nxs.cons(y).cons(x).cons(z); + } + + /** + * Coerces any object to a collection type, or returns null if not possible. + * Null is converted to an empty vector. + * + * @param a value to coerce to collection type. + * @return Collection object, or null if coercion failed. + */ + @SuppressWarnings("unchecked") + static ACollection collection(ACell a) { + if (a == null) return Vectors.empty(); + if (a instanceof ACollection) return (ACollection) a; + return null; + } + + /** + * Coerces any object to a data structure type, or returns null if not possible. + * Null is converted to an empty vector. + * + * @param a value to coerce to collection type. + * @return Collection object, or null if coercion failed. + */ + @SuppressWarnings("unchecked") + static ADataStructure castDataStructure(ACell a) { + if (a == null) return Vectors.empty(); + if (a instanceof ADataStructure) return (ADataStructure) a; + return null; + } + + /** + * Coerces an argument to a function interface. Certain values e.g. Keywords can + * be used / applied in function position. + * + * @param Function return type + * @param a Value to cast to a function + * @return AFn instance, or null if the argument cannot be coerced to a + * function. + * + */ + @SuppressWarnings("unchecked") + public static AFn castFunction(ACell a) { + if (a instanceof AFn) return (AFn) a; + if (a instanceof AMap) return MapFn.wrap((AMap) a); + if (a instanceof ASequence) return SeqFn.wrap((ASequence) a); + if (a instanceof ASet) return (AFn) SetFn.wrap((ASet) a); + if (a instanceof Keyword) return KeywordFn.wrap((Keyword) a); + return null; + } + + /** + * Ensure the argument is a valid CVM function. Returns null otherwise. + * + * @param Function return type + * @param a Value to cast to a function + * @return IFn instance, or null if the argument cannot be coerced to a + * function. + * + */ + @SuppressWarnings("unchecked") + public static AFn ensureFunction(ACell a) { + if (a instanceof AFn) return (AFn) a; + return null; + } + + /** + * Casts the argument to a valid Address. + * + * Handles: + *
    + *
  • Strings, which are interpreted as 16-character hex strings
  • + *
  • Addresses, which are returned unchanged
  • + *
  • Blobs, which are converted to addresses if and only if they are of the correct length (8 bytes)
  • + *
  • Numeric Longs, which are converted to the equivalent Address
  • + *
+ * + * @param a Value to cast to an Address + * @return Address value or null if not castable to a valid address + */ + public static Address castAddress(ACell a) { + if (a instanceof Address) return (Address) a; + if (a instanceof ABlob) return Address.create((ABlob)a); + if (a instanceof AString) return Address.fromHex(a.toString()); + CVMLong value=RT.ensureLong(a); + if (value==null) return null; + return Address.create(value.longValue()); + } + + /** + * Casts an arbitrary value to an Address + * @param a Value to cast. Strings or CVM values accepted + * @return Address instance, or null if not convertible + */ + public static Address castAddress(Object a) { + if (a instanceof ACell) return castAddress((ACell)a); + if (a instanceof String) return Address.parse((String)a); + return null; + } + + /** + * Ensures the argument is a valid Address. + * + * @param a Value to cast + * @return Address value or null if not a valid address + */ + public static Address ensureAddress(ACell a) { + if (a instanceof Address) return (Address) a; + return null; + } + + /** + * Implicit cast to an AccountKey. Accepts blobs of correct length + * @param a Value to cast + * @return AccountKey instance, or null if coercion fails + */ + public static AccountKey ensureAccountKey(ACell a) { + if (a==null) return null; + if (a instanceof AccountKey) return (AccountKey) a; + if (a instanceof ABlob) { + ABlob b = (ABlob) a; + return AccountKey.create(b); + } + + return null; + } + + /** + * Coerce to an AccountKey. Accepts strings and blobs of correct length + * @param a Value to cast + * @return AccountKey instance, or null if coercion fails + */ + public static AccountKey castAccountKey(ACell a) { + if (a==null) return null; + if (a instanceof AString) return AccountKey.fromHexOrNull((AString)a); + return ensureAccountKey(a); + } + + /** + * Converts an object to a canonical blob representation. Handles blobs, + * addresses, hashes and hex strings + * + * @param a Object to convert to a Blob + * @return Blob value, or null if not convertable to a blob + */ + public static ABlob castBlob(ACell a) { + // handle address, hash, blob instances + if (a instanceof ABlob) return Blobs.toCanonical((ABlob) a); + if (a instanceof AString) return Blobs.fromHex(a.toString()); + return null; + } + + /** + * Converts the argument to a non-null Map. Nulls are implicitly converted to the empty + * map. + * + * @param Type of map keys + * @param Type of map values + * @param a Value to cast + * @return Map instance, or null if argument cannot be converted to a map + * + */ + @SuppressWarnings("unchecked") + public static AMap ensureMap(ACell a) { + if (a == null) return Maps.empty(); + if (a instanceof AMap) return (AMap) a; + return null; + } + + /** + * Gets an element from a data structure using the given key. + * + * @param coll Collection to query + * @param key Key to look up in collection + * @return Value from collection with the specified key, or null if not found. + */ + public static ACell get(ADataStructure coll, ACell key) { + if (coll == null) return null; + return coll.get(key); + } + + /** + * Gets an element from a data structure using the given key. Returns the + * notFound parameter if the data structure does not have the specified key + * + * @param coll Collection to query + * @param key Key to look up in collection + * @param notFound Value to return if the lookup failed + * @return Value from collection with the specified key, or notFound argument + * if not found. + */ + public static ACell get(ADataStructure coll, ACell key, ACell notFound) { + if (coll == null) return notFound; + return coll.get(key,notFound); + } + + /** + * Converts any CVM value to a boolean value. An value is considered falsey if null + * or equal to CVMBool.FALSE, truthy otherwise + * + * @param a Object to convert to boolean value + * @return true if object is truthy, false otherwise + */ + public static boolean bool(ACell a) { + return !((a == null) || (a == CVMBool.FALSE)); + } + + /** + * Converts an object to a map entry. Handles MapEntries and length 2 vectors. + * + * @param Type of map key + * @param Type of map value + * @param x Value to cast + * @return MapEntry instance, or null if conversion fails + */ + @SuppressWarnings("unchecked") + public static MapEntry ensureMapEntry(ACell x) { + MapEntry me; + if (x instanceof MapEntry) { + me = (MapEntry) x; + } else if (x instanceof AVector) { + AVector v = (AVector) x; + if (v.count() != 2) return null; + me = MapEntry.createRef(v.getRef(0), v.getRef(1)); + } else { + return null; + } + return me; + } + + /** + * Coerces to Hash type. Converts blobs of correct length. + * + * @param o Value to cast + * @return Hash instance, or null if conversion not possible + */ + public static Hash ensureHash(ACell o) { + if (o instanceof Hash) return ((Hash) o); + if (o instanceof ABlob) { + ABlob blob=(ABlob)o; + if (blob.count()!=Hash.LENGTH) return null; + return Hash.wrap(blob.getBytes()); + } + + return null; + } + + /** + * Coerces an named argument to a keyword. + * + * @param a Value to cast + * @return Keyword if correctly constructed, or null if a failure occurs + */ + public static Keyword castKeyword(ACell a) { + if (a instanceof Keyword) return (Keyword) a; + AString name = name(a); + if (name == null) return null; + Keyword k = Keyword.create(name); + return k; + } + + /** + * Coerces an named argument to a Symbol. + * + * @param a Value to cast + * @return Symbol if correctly constructed, or null if a failure occurs + */ + public static Symbol ensureSymbol(ACell a) { + if (a instanceof Symbol) return (Symbol) a; + return null; + } + + + + /** + * Casts to an ADataStructure instance + * + * @param Type of data structure element + * @param a Value to cast + * @return ADataStructure instance, or null if not a data structure + */ + @SuppressWarnings("unchecked") + public static ADataStructure ensureDataStructure(ACell a) { + if (a instanceof ADataStructure) return (ADataStructure) a; + return null; + } + + /** + * Casts to an ACountable instance + * + * @param Type of countable element + * @param a Value to cast + * @return ADataStructure instance, or null if not a data structure + */ + @SuppressWarnings("unchecked") + public static ACountable ensureCountable(ACell a) { + if (a instanceof ACountable) return (ACountable) a; + return null; + } + + /** + * Tests if a value is one of the canonical boolean values 'true' or 'false' + * + * @param value Value to test + * @return True if the value is a canonical boolean value. + */ + public static boolean isBoolean(ACell value) { + return (value == CVMBool.TRUE) || (value == CVMBool.FALSE); + } + + /** + * Concatenates two sequences. Ignores nulls. + * + * @param a First sequence. Will be used to determine the type of the result if + * not null. + * @param b Second sequence. Will be the result if the first parameter is null. + * @return Concatenated Sequence + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static ASequence concat(ASequence a, ASequence b) { + if (a == null) return b; + if (b == null) return a; + return a.concat((ASequence) b); + } + + /** + * Validates an object. Might be a Cell or Ref + * + * @param o Object to validate + * @throws InvalidDataException For any validation failure + */ + public static void validate(Object o) throws InvalidDataException { + if (o==null) return; + if (o instanceof ACell) { + ((ACell) o).validate(); + } else if (o instanceof Ref) { + ((Ref) o).validate(); + } else { + throw new InvalidDataException("Data of class" + Utils.getClass(o) + + " neither IValidated, canonical nor embedded: ", o); + } + + } + + /** + * Validate a Cell. + * + * @param o Object to validate + * @throws InvalidDataException For any validation failure + */ + public static void validateCell(ACell o) throws InvalidDataException { + if (o==null) return; + if (o instanceof ACell) { + ((ACell) o).validateCell(); + } + } + + /** + * Associates a key with a given value in an associative data structure + * + * @param coll Any associative data structure + * @param key Key to update or add + * @param value Value to associate with key + * @return Updated data structure, or null if cast fails + */ + @SuppressWarnings("unchecked") + public static ADataStructure assoc(ADataStructure coll, ACell key, ACell value) { + if (coll == null) return (ADataStructure) Maps.create(key, value); + return coll.assoc(key, value); + } + + /** + * Returns the vector of keys of a map, or null if the object is not a map + * + * @param a Value to extract keys from (i.e. a Map) + * @return Vector of keys in the map + */ + @SuppressWarnings("unchecked") + public static AVector keys(ACell a) { + if (!(a instanceof AMap)) return null; + AMap m = (AMap) a; + return m.reduceEntries(new BiFunction<>() { + @Override + public AVector apply(AVector t, MapEntry u) { + return t.conj(u.getKey()); + } + }, Vectors.empty()); + } + + /** + * Returns the vector of values of a map, or null if the object is not a map + * + * @param a Value to extract values from (i.e. a Map) + * @return Vector of values from a map, or null if the object is not a map + */ + @SuppressWarnings("unchecked") + public static AVector values(ACell a) { + if (!(a instanceof AMap)) return null; + AMap m = (AMap) a; + return m.reduceValues(new BiFunction, R, AVector>() { + @Override + public AVector apply(AVector t, R u) { + return t.conj(u); + } + }, Vectors.empty()); + } + + /** + * Ensures the argument is an IAssociative instance. A null argument is considered an empty map. + * + * @param o Value to cast + * @return IAssociative instance, or null if conversion is not possible + */ + public static ADataStructure ensureAssociative(ACell o) { + if (o==null) return Maps.empty(); + if (o instanceof ADataStructure) return (ADataStructure) o; + return null; + } + + /** + * Ensures the value is a set. null is converted to the empty set. + * + * Returns null if the argument is not a set. + * + * @param Type of set element + * @param a Value to cast + * @return A set instance, or null if the argument cannot be converted to a set + */ + @SuppressWarnings("unchecked") + public static ASet ensureSet(ACell a) { + if (a==null) return Sets.empty(); + if (!(a instanceof ASet)) return null; + return (ASet) a; + } + + /** + * Casts the argument to a hashmap. null is converted to the empty HashMap. + * @param Type of keys + * @param Type of values + * @param a Any object + * @return AHashMap instance, or null if not a hash map + */ + @SuppressWarnings("unchecked") + public static AHashMap ensureHashMap(ACell a) { + if (a==null) return Maps.empty(); + if (a instanceof AHashMap) return (AHashMap) a; + return null; + } + + /** + * Implicitly casts the argument to a Blob + * @param object Value to cast to Blob + * @return Blob instance, or null if cast fails + */ + public static ABlob ensureBlob(ACell object) { + if (object instanceof ABlob) return ((ABlob)object); + return null; + } + + /** + * Implicitly casts the argument to a CVM String + * + * @param a Value to cast to a String + * @return AString instance, or null if cast fails + */ + public static AString ensureString(ACell a) { + if (a instanceof AString) return ((AString)a); + return null; + } + + public static boolean isValidAmount(long amount) { + return ((amount>=0)&&(amount T cvm(Object o) { + if (o==null) return null; + if (o instanceof ACell) return ((T)o); + if (o instanceof String) return (T) Strings.create((String)o); + if (o instanceof Double) return (T)CVMDouble.create(((Double)o)); + if (o instanceof Number) return (T)CVMLong.create(((Number)o).longValue()); + if (o instanceof Character) return (T)CVMChar.create((Character)o); + if (o instanceof Boolean) return (T)CVMBool.create((Boolean)o); + throw new IllegalArgumentException("Can't convert to CVM type with class: "+Utils.getClassName(o)); + } + + /** + * Converts a CVM value to equivalent JVM value + * @param o Value to convert to JVM type + * @return Java value, or unchanged input + */ + @SuppressWarnings("unchecked") + public static T jvm(ACell o) { + if (o instanceof AString) return (T) o.toString(); + if (o instanceof CVMLong) return (T)(Long)((CVMLong)o).longValue(); + if (o instanceof CVMDouble) return (T)(Double)((CVMDouble)o).doubleValue(); + if (o instanceof CVMByte) return (T)(Byte)(byte)((CVMByte)o).longValue(); + if (o instanceof CVMBool) return (T)(Boolean)((CVMBool)o).booleanValue(); + if (o instanceof CVMChar) return (T)(Character)((CVMChar)o).charValue(); + return (T)o; + } + + /** + * Compute mode. + * @param a First numeric argument (numerator) + * @param b First numeric argument (divisor) + * @return Numeric value or null if cast fails + */ + public static CVMLong mod(ACell a , ACell b) { + CVMLong la=RT.castLong(a); + if (la==null) return null; + + CVMLong lb=RT.castLong(b); + if (lb==null) return null; + + long num = la.longValue(); + long denom = lb.longValue(); + long result = num % denom; + if (result<0) result+=denom; + + return CVMLong.create(result); + } + + /** + * Get the runtime Type of any CVM value + * @param a Any CVM value + * @return Type of CVM value + */ + public static AType getType(ACell a) { + if (a==null) return Types.NIL; + return a.getType(); + } + + public static boolean isNaN(ACell val) { + return CVMDouble.NaN.equals(val); + } + + + + + +} diff --git a/convex-core/src/main/java/convex/core/lang/Reader.java b/convex-core/src/main/java/convex/core/lang/Reader.java new file mode 100644 index 000000000..f68216dc0 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/Reader.java @@ -0,0 +1,78 @@ +package convex.core.lang; + +import java.io.IOException; + +import convex.core.data.ACell; +import convex.core.data.AList; +import convex.core.data.Syntax; +import convex.core.lang.reader.AntlrReader; +import convex.core.util.Utils; + +/** + * Parboiled Parser implementation which reads source code and produces a tree + * of parsed objects. + * + * Supports reading in either raw form (ACell) mode or wrapping with Syntax Objects. The + * latter is required for source references etc. + * + * "Talk is cheap. Show me the code." - Linus Torvalds + */ +@SuppressWarnings("javadoc") +public class Reader { + /** + * Parses an expression and returns a Syntax object + * + * @param source + * @return Parsed form + */ + public static Syntax readSyntax(String source) { + return Syntax.create(read(source)); + } + + public static ACell readResource(String path) { + String source; + try { + source = Utils.readResourceAsString(path); + } catch (IOException e) { + throw Utils.sneakyThrow(e); + } + return read(source); + } + + public static ACell readResourceAsData(String path) throws IOException { + String source = Utils.readResourceAsString(path); + return read(source); + } + + /** + * Parses an expression list and returns a list of raw forms + * + * @param source + * @return List of Syntax Objects + */ + public static AList readAll(String source) { + return AntlrReader.readAll(source); + } + + /** + * Parses an expression and returns a form as an Object + * + * @param source + * @return Parsed form + */ + public static ACell read(java.io.Reader source) throws IOException { + return AntlrReader.read(source); + } + + /** + * Parses an expression and returns a form + * + * @param source + * @return Parsed form + */ + @SuppressWarnings("unchecked") + public static R read(String source) { + return (R) AntlrReader.read(source); + } + +} diff --git a/convex-core/src/main/java/convex/core/lang/Symbols.java b/convex-core/src/main/java/convex/core/lang/Symbols.java new file mode 100644 index 000000000..6efe8d9fc --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/Symbols.java @@ -0,0 +1,297 @@ +package convex.core.lang; + +import convex.core.data.Symbol; + +/** + * Static class for Symbol constants. + * + * "If you have more things than names, your design is broken" - Stuart Halloway + */ +public class Symbols { + public static final Symbol COUNT = Symbol.create("count"); + public static final Symbol CONJ = Symbol.create("conj"); + public static final Symbol CONS = Symbol.create("cons"); + public static final Symbol GET = Symbol.create("get"); + public static final Symbol GET_IN = Symbol.create("get-in"); + public static final Symbol ASSOC = Symbol.create("assoc"); + public static final Symbol ASSOC_IN = Symbol.create("assoc-in"); + public static final Symbol DISSOC = Symbol.create("dissoc"); + public static final Symbol DISJ = Symbol.create("disj"); + public static final Symbol NTH = Symbol.create("nth"); + + public static final Symbol VECTOR = Symbol.create("vector"); + public static final Symbol VEC = Symbol.create("vec"); + public static final Symbol SET = Symbol.create("set"); + public static final Symbol HASH_MAP = Symbol.create("hash-map"); + public static final Symbol BLOB_MAP = Symbol.create("blob-map"); + public static final Symbol HASH_SET = Symbol.create("hash-set"); + public static final Symbol LIST = Symbol.create("list"); + public static final Symbol EMPTY = Symbol.create("empty"); + public static final Symbol INTO = Symbol.create("into"); + + public static final Symbol CONCAT = Symbol.create("concat"); + public static final Symbol MAP = Symbol.create("map"); + public static final Symbol REDUCE = Symbol.create("reduce"); + + public static final Symbol KEYS = Symbol.create("keys"); + public static final Symbol VALUES = Symbol.create("values"); + + public static final Symbol STR = Symbol.create("str"); + public static final Symbol KEYWORD = Symbol.create("keyword"); + public static final Symbol SYMBOL = Symbol.create("symbol"); + public static final Symbol NAME = Symbol.create("name"); + + public static final Symbol EQUALS = Symbol.create("="); + + public static final Symbol EQ = Symbol.create("=="); + public static final Symbol LT = Symbol.create("<"); + public static final Symbol GT = Symbol.create(">"); + public static final Symbol LE = Symbol.create("<="); + public static final Symbol GE = Symbol.create(">="); + + public static final Symbol NOT = Symbol.create("not"); + public static final Symbol OR = Symbol.create("or"); + public static final Symbol AND = Symbol.create("and"); + + public static final Symbol ASSERT = Symbol.create("assert"); + public static final Symbol FAIL = Symbol.create("fail"); + public static final Symbol TRY = Symbol.create("try"); + public static final Symbol CATCH = Symbol.create("catch"); + + public static final Symbol APPLY = Symbol.create("apply"); + public static final Symbol HASH = Symbol.create("hash"); + + public static final Symbol QUOTE = Symbol.create("quote"); + public static final Symbol QUASIQUOTE = Symbol.create("quasiquote"); + + public static final Symbol SYNTAX_QUOTE = Symbol.create("syntax-quote"); + public static final Symbol UNQUOTE = Symbol.create("unquote"); + public static final Symbol UNQUOTE_SPLICING = Symbol.create("unquote-splicing"); + + public static final Symbol DO = Symbol.create("do"); + public static final Symbol COND = Symbol.create("cond"); + public static final Symbol DEF = Symbol.create("def"); + public static final Symbol UNDEF = Symbol.create("undef"); + public static final Symbol UNDEF_STAR = Symbol.create("undef*"); + + public static final Symbol FN = Symbol.create("fn"); + public static final Symbol MACRO = Symbol.create("macro"); + public static final Symbol EXPANDER = Symbol.create("expander"); + + public static final Symbol IF = Symbol.create("if"); + public static final Symbol WHEN = Symbol.create("when"); + public static final Symbol LET = Symbol.create("let"); + + public static final Symbol STORE = Symbol.create("store"); + public static final Symbol FETCH = Symbol.create("fetch"); + + public static final Symbol ADDRESS = Symbol.create("address"); + public static final Symbol BALANCE = Symbol.create("balance"); + public static final Symbol TRANSFER = Symbol.create("transfer"); + public static final Symbol ACCEPT = Symbol.create("accept"); + public static final Symbol ACCOUNT = Symbol.create("account"); + + public static final Symbol ACCOUNT_Q = Symbol.create("account?"); + + public static final Symbol STAKE = Symbol.create("stake"); + public static final Symbol CREATE_PEER = Symbol.create("create-peer"); + public static final Symbol SET_PEER_DATA = Symbol.create("set-peer-data"); + public static final Symbol SET_PEER_STAKE = Symbol.create("set-peer-stake"); + + public static final Symbol CALL = Symbol.create("call"); + public static final Symbol CALL_STAR = Symbol.create("call*"); + + public static final Symbol HALT = Symbol.create("halt"); + public static final Symbol ROLLBACK = Symbol.create("rollback"); + + public static final Symbol CALLABLE_Q = Symbol.create("callable?"); + public static final Symbol DEPLOY = Symbol.create("deploy"); + public static final Symbol DEPLOY_ONCE = Symbol.create("deploy-once"); + + public static final Symbol BLOB = Symbol.create("blob"); + + public static final Symbol INC = Symbol.create("inc"); + public static final Symbol DEC = Symbol.create("dec"); + + public static final Symbol LONG = Symbol.create("long"); + public static final Symbol BYTE = Symbol.create("byte"); + public static final Symbol CHAR = Symbol.create("char"); + + public static final Symbol DOUBLE = Symbol.create("double"); + + public static final Symbol BOOLEAN = Symbol.create("boolean"); + public static final Symbol BOOLEAN_Q = Symbol.create("boolean?"); + + public static final Symbol PLUS = Symbol.create("+"); + public static final Symbol MINUS = Symbol.create("-"); + public static final Symbol TIMES = Symbol.create("*"); + public static final Symbol DIVIDE = Symbol.create("/"); + + public static final Symbol ABS = Symbol.create("abs"); + public static final Symbol SIGNUM = Symbol.create("signum"); + public static final Symbol SQRT = Symbol.create("sqrt"); + public static final Symbol EXP = Symbol.create("exp"); + public static final Symbol POW = Symbol.create("pow"); + public static final Symbol MOD = Symbol.create("mod"); + public static final Symbol QUOT = Symbol.create("quot"); + public static final Symbol REM = Symbol.create("rem"); + + + public static final Symbol FLOOR = Symbol.create("floor"); + public static final Symbol CEIL = Symbol.create("ceil"); + + public static final Symbol NAN = Symbol.create("NaN"); + + public static final Symbol LOOP = Symbol.create("loop"); + public static final Symbol RECUR = Symbol.create("recur"); + public static final Symbol TAILCALL_STAR = Symbol.create("tailcall*"); + + public static final Symbol RETURN = Symbol.create("return"); + public static final Symbol BREAK = Symbol.create("break"); + public static final Symbol REDUCED = Symbol.create("reduced"); + + + public static final char SPECIAL_STAR = '*'; + public static final Symbol STAR_ADDRESS = Symbol.create("*address*"); + public static final Symbol STAR_MEMORY = Symbol.create("*memory*"); + public static final Symbol STAR_CALLER = Symbol.create("*caller*"); + public static final Symbol STAR_ORIGIN = Symbol.create("*origin*"); + public static final Symbol STAR_JUICE = Symbol.create("*juice*"); + public static final Symbol STAR_BALANCE = Symbol.create("*balance*"); + public static final Symbol STAR_DEPTH = Symbol.create("*depth*"); + public static final Symbol STAR_RESULT = Symbol.create("*result*"); + public static final Symbol STAR_TIMESTAMP = Symbol.create("*timestamp*"); + public static final Symbol STAR_OFFER = Symbol.create("*offer*"); + public static final Symbol STAR_STATE = Symbol.create("*state*"); + public static final Symbol STAR_HOLDINGS = Symbol.create("*holdings*"); + public static final Symbol STAR_SEQUENCE = Symbol.create("*sequence*"); + public static final Symbol STAR_KEY = Symbol.create("*key*"); + + public static final Symbol STAR_LANG = Symbol.create("*lang*"); + + public static final Symbol STAR_INITIAL_EXPANDER = Symbol.create("*initial-expander*"); + + public static final Symbol HERO = Symbol.create("hero"); + + public static final Symbol COMPILE = Symbol.create("compile"); + public static final Symbol READ = Symbol.create("read"); + public static final Symbol EVAL = Symbol.create("eval"); + public static final Symbol EVAL_AS = Symbol.create("eval-as"); + + public static final Symbol QUERY = Symbol.create("query"); + public static final Symbol QUERY_AS = Symbol.create("query-as"); + + + public static final Symbol EXPAND = Symbol.create("expand"); + + public static final Symbol SCHEDULE = Symbol.create("schedule"); + public static final Symbol SCHEDULE_STAR = Symbol.create("schedule*"); + + public static final Symbol FIRST = Symbol.create("first"); + public static final Symbol SECOND = Symbol.create("second"); + public static final Symbol LAST = Symbol.create("last"); + public static final Symbol NEXT = Symbol.create("next"); + public static final Symbol REVERSE = Symbol.create("reverse"); + + public static final Symbol AMPERSAND = Symbol.create("&"); + public static final Symbol UNDERSCORE = Symbol.create("_"); + + public static final Symbol X = Symbol.create("x"); + public static final Symbol E = Symbol.create("e"); + + public static final Symbol NIL = Symbol.create("nil"); + + public static final Symbol NIL_Q = Symbol.create("nil?"); + public static final Symbol LIST_Q = Symbol.create("list?"); + public static final Symbol VECTOR_Q = Symbol.create("vector?"); + public static final Symbol SET_Q = Symbol.create("set?"); + public static final Symbol MAP_Q = Symbol.create("map?"); + + public static final Symbol COLL_Q = Symbol.create("coll?"); + public static final Symbol EMPTY_Q = Symbol.create("empty?"); + + public static final Symbol SYMBOL_Q = Symbol.create("symbol?"); + public static final Symbol KEYWORD_Q = Symbol.create("keyword?"); + public static final Symbol BLOB_Q = Symbol.create("blob?"); + public static final Symbol ADDRESS_Q = Symbol.create("address?"); + public static final Symbol LONG_Q = Symbol.create("long?"); + public static final Symbol STR_Q = Symbol.create("str?"); + public static final Symbol NUMBER_Q = Symbol.create("number?"); + public static final Symbol HASH_Q = Symbol.create("hash?"); + + public static final Symbol FN_Q = Symbol.create("fn?"); + public static final Symbol ACTOR_Q = Symbol.create("actor?"); + + public static final Symbol ZERO_Q = Symbol.create("zero?"); + + public static final Symbol CONTAINS_KEY_Q = Symbol.create("contains-key?"); + + public static final Symbol FOO = Symbol.create("foo"); + public static final Symbol BAR = Symbol.create("bar"); + public static final Symbol BAZ = Symbol.create("baz");; + + + public static final Symbol LOOKUP = Symbol.create("lookup"); + + // State global fields + public static final Symbol TIMESTAMP = Symbol.create("timestamp"); + public static final Symbol JUICE_PRICE = Symbol.create("juice-price"); + public static final Symbol FEES = Symbol.create("fees"); + + // source info + public static final Symbol START = Symbol.create("start"); + public static final Symbol END = Symbol.create("end"); + public static final Symbol SOURCE = Symbol.create("source"); + + public static final Symbol SYNTAX_Q = Symbol.create("syntax?"); + public static final Symbol SYNTAX = Symbol.create("syntax"); + public static final Symbol GET_META = Symbol.create("get-meta"); + public static final Symbol UNSYNTAX = Symbol.create("unsyntax"); + + public static final Symbol DOC = Symbol.create("doc"); + public static final Symbol META = Symbol.create("meta"); + public static final Symbol META_STAR = Symbol.create("*meta*"); + + public static final Symbol LOOKUP_META = Symbol.create("lookup-meta"); + + public static final Symbol GET_HOLDING = Symbol.create("get-holding"); + public static final Symbol SET_HOLDING = Symbol.create("set-holding"); + + public static final Symbol GET_CONTROLLER = Symbol.create("get-controller"); + public static final Symbol SET_CONTROLLER = Symbol.create("set-controller"); + public static final Symbol CHECK_TRUSTED_Q = Symbol.create("check-trusted?"); + + public static final Symbol SET_MEMORY = Symbol.create("set-memory"); + public static final Symbol TRANSFER_MEMORY = Symbol.create("transfer-memory"); + + public static final Symbol RECEIVE_ALLOWANCE = Symbol.create("receive-allowance"); + public static final Symbol RECEIVE_COIN = Symbol.create("receive-coin"); + public static final Symbol RECEIVE_ASSET = Symbol.create("receive-asset"); + + + public static final Symbol SET_BANG = Symbol.create("set!"); + public static final Symbol SET_STAR = Symbol.create("set*"); + + public static final Symbol REGISTER = Symbol.create("register"); + + public static final Symbol SUBSET_Q = Symbol.create("subset?"); + public static final Symbol UNION = Symbol.create("union"); + public static final Symbol INTERSECTION = Symbol.create("intersection"); + public static final Symbol DIFFERENCE = Symbol.create("difference"); + + public static final Symbol MERGE = Symbol.create("merge"); + + public static final Symbol ENCODING = Symbol.create("encoding"); + + public static final Symbol CREATE_ACCOUNT = Symbol.create("create-account"); + public static final Symbol SET_KEY = Symbol.create("set-key"); + + public static final Symbol LOG = Symbol.create("log"); + + public static final Symbol CNS_RESOLVE = Symbol.create("cns-resolve"); + + public static final Symbol NAN_Q = Symbol.create("nan?"); + + + +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/AClosure.java b/convex-core/src/main/java/convex/core/lang/impl/AClosure.java new file mode 100644 index 000000000..099dfdbca --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/AClosure.java @@ -0,0 +1,35 @@ +package convex.core.lang.impl; + +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.lang.AFn; + +/** + * Abstract base class for functions that can close over a lexical environment. + * + * @param Return type of function + */ +public abstract class AClosure extends AFn { + /** + * Lexical environment saved for this closure + */ + protected final AVector lexicalEnv; + + protected AClosure(AVector lexicalEnv) { + this.lexicalEnv=lexicalEnv; + } + + /** + * Produces an copy of this closure with the specified environment + * + * @param env New lexical environment to use for this closure + * @return Closure updated with new lexical environment + */ + public abstract > F withEnvironment(AVector env); + + /** + * Print the "internal" representation of a closure e.g. "[x] 1", excluding the 'fn' symbol. + * @param sb StringBuilder to print to + */ + public abstract void printInternal(StringBuilder sb); +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/ADataFn.java b/convex-core/src/main/java/convex/core/lang/impl/ADataFn.java new file mode 100644 index 000000000..f5439ed2e --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/ADataFn.java @@ -0,0 +1,77 @@ +package convex.core.lang.impl; + +import convex.core.data.ACell; +import convex.core.data.IRefFunction; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.AFn; +import convex.core.lang.Context; + +/** + * Abstract base class for data structure lookup functions. + * + * Not a canonical object, can't exist as CVM value. + * + * @param Type of function return value + */ +public abstract class ADataFn extends AFn { + + @Override + public int estimatedEncodingSize() { + throw new UnsupportedOperationException(); + } + + @Override + public Context invoke(Context context, ACell[] args) { + throw new UnsupportedOperationException(); + } + + @Override + public AFn updateRefs(IRefFunction func) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasArity(int n) { + throw new UnsupportedOperationException(); + } + + @Override + public void validateCell() throws InvalidDataException { + throw new UnsupportedOperationException(); + } + + @Override + public int encode(byte[] bs, int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCanonical() { + return false; + } + + @Override + public ACell toCanonical() { + throw new UnsupportedOperationException("Can't make canonical!"); + } + + @Override + public boolean isCVMValue() { + return false; + } + + @Override + public byte getTag() { + throw new UnsupportedOperationException(); + } + + @Override + public int getRefCount() { + throw new UnsupportedOperationException(); + } +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/AExceptional.java b/convex-core/src/main/java/convex/core/lang/impl/AExceptional.java new file mode 100644 index 000000000..207555044 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/AExceptional.java @@ -0,0 +1,32 @@ +package convex.core.lang.impl; + +import convex.core.data.ACell; + +/** + * Abstract base class for exceptional return values. + * + * Java exceptions are expensive and don't make it easy to provide exactly the + * semantics we want so we return exceptional values in response to errors + * during on-chain execution. + * + * Notable uses: - Early return values from functions - Tail calls - Loop / + * recur + * + * "Do not fear to be eccentric in opinion, for every opinion now accepted was + * once eccentric." ― Bertrand Russell + */ +public abstract class AExceptional { + + /** + * Returns the Exception code for this exceptional value + * @return Exception Code + */ + public abstract ACell getCode(); + + /** + * Gets the message for an exceptional value. May or may not be meaningful. + * @return Exception Message + */ + public abstract ACell getMessage(); + +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/AReturn.java b/convex-core/src/main/java/convex/core/lang/impl/AReturn.java new file mode 100644 index 000000000..6916be607 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/AReturn.java @@ -0,0 +1,8 @@ +package convex.core.lang.impl; + +/** + * Abstract base class for exceptional returns + */ +public abstract class AReturn extends AExceptional { + +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/ATrampoline.java b/convex-core/src/main/java/convex/core/lang/impl/ATrampoline.java new file mode 100644 index 000000000..34746f90b --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/ATrampoline.java @@ -0,0 +1,28 @@ +package convex.core.lang.impl; + +import convex.core.data.ACell; + +/** + * Abstract base class for trampolining function return values + */ +public abstract class ATrampoline extends AReturn { + + protected final ACell[] args; + + public ATrampoline(ACell[] values) { + this.args=values; + } + + public ACell getValue(int i) { + return args[i]; + } + + public ACell[] getValues() { + return args; + } + + public int arity() { + return args.length; + } + +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/CoreFn.java b/convex-core/src/main/java/convex/core/lang/impl/CoreFn.java new file mode 100644 index 000000000..b59d86f0e --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/CoreFn.java @@ -0,0 +1,128 @@ +package convex.core.lang.impl; + +import convex.core.data.ACell; +import convex.core.data.IRefFunction; +import convex.core.data.Ref; +import convex.core.data.Symbol; +import convex.core.data.Tag; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.AFn; +import convex.core.lang.Context; + +/** + * Abstract base class for core language functions implemented in the Runtime + * + * Core functions are tagged using their symbols in on-chain representation + * + * @param Type of function result + */ +public abstract class CoreFn extends AFn implements ICoreDef { + + private Symbol symbol; + private int arity; + private boolean variadic; + + protected CoreFn(Symbol symbol) { + this.symbol = symbol; + this.arity=0; + this.variadic=true; + } + + @Override + public abstract Context invoke(Context context, ACell[] args); + + public Symbol getSymbol() { + return symbol; + } + + public byte getTag() { + return Tag.CORE_DEF; + } + + protected String name() { + return symbol.getName().toString(); + } + + @Override + public boolean isCanonical() { + return true; + } + + @Override + public CoreFn toCanonical() { + return this; + } + + protected String minArityMessage(int minArity, int actual) { + return name() + " requires minimum arity " + minArity + " but called with: " + actual; + } + + protected String maxArityMessage(int maxArity, int actual) { + return name() + " requires maximum arity " + maxArity + " but called with: " + actual; + } + + protected String rangeArityMessage(int minArity, int maxArity, int actual) { + return name() + " requires arity between "+minArity+ " and " + maxArity + " but called with: " + actual; + } + + protected String exactArityMessage(int arity, int actual) { + return name() + " requires arity " + arity + " but called with: " + actual; + } + + @Override + public boolean hasArity(int n) { + if (n==arity) return true; + if (n Ref getRef(int i) { + throw new IndexOutOfBoundsException("Bad ref index: "+i); + } + + @Override + public CoreFn updateRefs(IRefFunction func) { + return this; + } + + @Override + public int estimatedEncodingSize() { + return 20; + } + + @Override + public void validateCell() throws InvalidDataException { + symbol.validateCell(); + } + + @Override + public boolean isEmbedded() { + // embed core functions, since they are the same size as small symbols + return true; + } + +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/CorePred.java b/convex-core/src/main/java/convex/core/lang/impl/CorePred.java new file mode 100644 index 000000000..c7737d351 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/CorePred.java @@ -0,0 +1,29 @@ +package convex.core.lang.impl; + +import convex.core.data.ACell; +import convex.core.data.Symbol; +import convex.core.data.prim.CVMBool; +import convex.core.lang.Context; +import convex.core.lang.Juice; + +/** + * Abstract base class for core predicate functions + */ +public abstract class CorePred extends CoreFn { + + protected CorePred(Symbol symbol) { + super(symbol); + } + + @SuppressWarnings("unchecked") + @Override + public Context invoke(@SuppressWarnings("rawtypes") Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(name() + " requires exactly one argument"); + ACell val = args[0]; + // ensure we return one of the two canonical boolean values + CVMBool result = test(val) ? CVMBool.TRUE : CVMBool.FALSE; + return context.withResult(Juice.SIMPLE_FN, result); + } + + public abstract boolean test(ACell val); +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/ErrorValue.java b/convex-core/src/main/java/convex/core/lang/impl/ErrorValue.java new file mode 100644 index 000000000..cca4f38a9 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/ErrorValue.java @@ -0,0 +1,137 @@ +package convex.core.lang.impl; + +import java.util.ArrayList; +import java.util.List; + +import convex.core.data.ACell; +import convex.core.data.AString; +import convex.core.data.Strings; + +/** + * Class representing an Error value produced by the CVM. + * + * See "Error Handling" CAD. + * + * Contains: + *
    + *
  • An immutable Error Code
  • + *
  • An immutable Error Message
  • + *
  • A mutable error trace (for information purposes outside the CVM)
  • + *
+ * + * "Computers are useless. They can only give you answers." + * - Pablo Picasso + * + */ +public class ErrorValue extends AExceptional { + + private final ACell code; + private final ACell message; + private final ArrayList trace=new ArrayList<>(); + private ACell log; + + private ErrorValue(ACell code, ACell message) { + if (code==null) throw new IllegalArgumentException("Error code must not be null"); + this.code=code; + this.message=message; + } + + public static ErrorValue create(ACell code) { + return new ErrorValue(code,null); + } + + /** + * Creates an ErrorValue with the specified type and message. Message may be null. + * @param code Type of error + * @param message Off-chain message as CVM String + * @return New ErrorValue instance + */ + public static ErrorValue create(ACell code, AString message) { + return new ErrorValue(code,message); + } + + /** + * Creates an ErrorValue with the specified type and message. Message may be null. + * @param code Type of error + * @param message Off-chain message, must be valid CVM Value + * @return New ErrorValue instance + */ + public static ErrorValue createRaw(ACell code, ACell message) { + return new ErrorValue(code,message); + } + + /** + * Creates an ErrorValue with the specified type and message. Message may be null. + * @param code Code of error + * @param message Off-chain message as Java String + * @return New ErrorValue instance + */ + public static ErrorValue create(ACell code, String message) { + return new ErrorValue(code,Strings.create(message)); + } + + /** + * Gets the Error Code for this ErrorVAlue instance. The Error Code may be any value, but + * by convention (and exclusively in Convex runtime code) it is a upper-case keyword e.g. :ASSERT + * + * @return Error code value + */ + public ACell getCode() { + return code; + } + + public void addTrace(String traceMessage) { + trace.add(Strings.create(traceMessage)); + } + + public void addLog(ACell log) { + this.log=log; + } + + /** + * Gets the optional message associated with this error value, or null if not supplied. + * @return The message carried with this error + */ + public ACell getMessage() { + return message; + } + + @Override + public String toString() { + StringBuilder sb=new StringBuilder(); + sb.append("ErrorValue["+code+"]"+((message==null)?"":" : "+message)); + if (trace!=null) { + for (Object o:trace) { + sb.append("\n"); + sb.append(o.toString()); + } + } + + return sb.toString(); + } + + /** + * Gets the trace for this Error. + * + * The trace List is mutable, and may be used to implement accumulation of additional trace entries. + * + * @return List of trace entries. + */ + public List getTrace() { + return trace; + } + + /** + * Gets the CVM local log at the time of the Error. + * + * The trace List is mutable, and may be used to implement accumulation of additional trace entries. + * + * @return List of trace entries. + */ + public ACell getLog() { + return log; + } + + + +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/Fn.java b/convex-core/src/main/java/convex/core/lang/impl/Fn.java new file mode 100644 index 000000000..19049c3ca --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/Fn.java @@ -0,0 +1,216 @@ +package convex.core.lang.impl; + +import java.nio.ByteBuffer; + +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.Format; +import convex.core.data.IRefFunction; +import convex.core.data.Ref; +import convex.core.data.Tag; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.Symbols; +import convex.core.util.Utils; + +/** + * Value class representing a instantiated closure / lambda function. + * + * Includes the following information: + *
    + *
  1. Parameters of the function, as a vector of Syntax objects
  2. + *
  3. Body of the function, as a compiled operation
  4. + *
  5. captured lexical bindings at time of creation.
  6. + *
+ * + * @param Return type of function + */ +public class Fn extends AClosure { + + // note: embedding these fields directly for efficiency rather than going by + // Refs. + + private final AVector params; + private final AOp body; + + private Long variadic=null; + + private Fn(AVector params, AOp body, AVector lexicalEnv) { + super(lexicalEnv); + this.params = params; + this.body = body; + } + + public static Fn create(AVector params, AOp body) { + AVector binds = Context.EMPTY_BINDINGS; + return new Fn(params, body, binds); + } + + @SuppressWarnings("unchecked") + @Override + public > F withEnvironment(AVector env) { + if (this.lexicalEnv==env) return (F) this; + return (F) new Fn(params, body, env); + } + + @Override + public boolean hasArity(int n) { + long var=checkVariadic(); + long pc=params.count(); + if (var>=0) return n>=(pc-2); // n must be at least number of params excluding [& more] + return (n==pc); + } + + /** + * Checks if the function is variadic. + * + * @return negative if non-variadic, index of variadic parameter if variadic + */ + private Long checkVariadic() { + if (variadic!=null) return variadic; + long pc=params.count(); + for (int i=0; i invoke(Context context, ACell[] args) { + // update local bindings for the duration of this function call + final AVector savedBindings = context.getLocalBindings(); + + // update to correct lexical environment, then bind function parameters + context = context.withLocalBindings(lexicalEnv); + + Context boundContext = context.updateBindings(params, args); + if (boundContext.isExceptional()) return boundContext.withLocalBindings(savedBindings); + + Context ctx = boundContext.execute(body); + + // return with restored bindings + return ctx.withLocalBindings(savedBindings); + } + + @Override + public boolean isCanonical() { + return true; + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.FN; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = params.encode(bs,pos); + pos = body.encode(bs,pos); + pos = lexicalEnv.encode(bs,pos); + return pos; + } + + @Override + public int estimatedEncodingSize() { + return 1+params.estimatedEncodingSize()+body.estimatedEncodingSize()+lexicalEnv.estimatedEncodingSize(); + } + + public static Fn read(ByteBuffer bb) throws BadFormatException { + try { + AVector params = Format.read(bb); + if (params==null) throw new BadFormatException("Null parameters to Fn"); + AOp body = Format.read(bb); + if (body==null) throw new BadFormatException("Null body in Fn"); + AVector lexicalEnv = Format.read(bb); + return new Fn<>(params, body, lexicalEnv); + } catch (ClassCastException e) { + throw new BadFormatException("Bad Fn format", e); + } + } + + @Override + public void print(StringBuilder sb) { + sb.append("(fn "); + printInternal(sb); + sb.append(')'); + } + + @Override + public void printInternal(StringBuilder sb) { + // Custom param printing, avoid printing Syntax metadata for now + sb.append('['); + long size = params.count(); + for (long i = 0; i < size; i++) { + if (i > 0) sb.append(' '); + Utils.print(sb,params.get(i)); + } + sb.append(']'); + + sb.append(' '); + body.print(sb); + } + + /** + * Returns the declared param names for a function. + * + * @return A binding vector describing the parameters for this function + */ + public AVector getParams() { + return params; + } + + public AOp getBody() { + return body; + } + + @Override + public int getRefCount() { + return params.getRefCount() + body.getRefCount() + lexicalEnv.getRefCount(); + } + + @Override + public Ref getRef(int i) { + int pc = params.getRefCount(); + if (i < pc) return params.getRef(i); + i -= pc; + int bc = body.getRefCount(); + if (i < bc) return body.getRef(i); + i -= bc; + return lexicalEnv.getRef(i); + } + + @Override + public Fn updateRefs(IRefFunction func) { + AVector newParams = params.updateRefs(func); + AOp newBody = body.updateRefs(func); + AVector newLexicalEnv = lexicalEnv.updateRefs(func); + if ((params == newParams) && (body == newBody) && (lexicalEnv == newLexicalEnv)) return this; + return new Fn<>(newParams, newBody, lexicalEnv); + } + + @Override + public void validateCell() throws InvalidDataException { + params.validateCell(); + body.validateCell(); + lexicalEnv.validateCell(); + } + + @Override + public ACell toCanonical() { + return this; + } + + + + + +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/HaltValue.java b/convex-core/src/main/java/convex/core/lang/impl/HaltValue.java new file mode 100644 index 000000000..e428b60b5 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/HaltValue.java @@ -0,0 +1,43 @@ +package convex.core.lang.impl; + +import convex.core.ErrorCodes; +import convex.core.data.ACell; + +/** + * Class representing a halt return value + * + * "Computers are useless. They can only give you answers." - Pablo Picasso + * + * @param Type of return value + */ +public class HaltValue extends AReturn { + + private final T value; + + public HaltValue(T value) { + this.value = value; + } + + public static HaltValue wrap(T value) { + return new HaltValue(value); + } + + public T getValue() { + return value; + } + + @Override + public String toString() { + return "HaltValue: " + value; + } + + @Override + public ACell getCode() { + return ErrorCodes.HALT; + } + + @Override + public ACell getMessage() { + return null; + } +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/ICoreDef.java b/convex-core/src/main/java/convex/core/lang/impl/ICoreDef.java new file mode 100644 index 000000000..9e3d38633 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/ICoreDef.java @@ -0,0 +1,20 @@ +package convex.core.lang.impl; + +import convex.core.data.Symbol; + +/** + * Interface for objects that act as definitions in the core environment. + * + * These are serialised as symbolic references, and will be deserialised to + * point to the same core object. + */ +public interface ICoreDef { + + /** + * Defines the symbol for this core definition. + * + * @return The symbol for this core definition. + */ + public Symbol getSymbol(); + +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/KeywordFn.java b/convex-core/src/main/java/convex/core/lang/impl/KeywordFn.java new file mode 100644 index 000000000..2be864cfc --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/KeywordFn.java @@ -0,0 +1,52 @@ +package convex.core.lang.impl; + +import convex.core.data.ACell; +import convex.core.data.ADataStructure; +import convex.core.data.Keyword; +import convex.core.data.type.Types; +import convex.core.lang.Context; +import convex.core.lang.RT; + +public class KeywordFn extends ADataFn { + private Keyword key; + + public KeywordFn(Keyword k) { + this.key = k; + } + + public static KeywordFn wrap(Keyword k) { + return new KeywordFn(k); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public Context invoke(Context context, ACell[] args) { + int n = args.length; + T result; + if (n == 1) { + ADataStructure gettable = RT.ensureAssociative(args[0]); + if (gettable == null) return context.withCastError(0, Types.DATA_STRUCTURE); + result = (T) gettable.get(key); + } else if (n == 2) { + ACell ds = args[0]; + ACell notFound = args[1]; + if (ds == null) { + result = (T) notFound; + } else { + ADataStructure gettable = RT.ensureAssociative(ds); + if (gettable == null) return context.withCastError(0, Types.DATA_STRUCTURE); + result = (T) RT.get(gettable, key, notFound); + } + } else { + return context.withArityError("Expected arity 1 or 2 for keyword lookup but got: " + n); + } + return context.withResult(result); + } + + @Override + public void print(StringBuilder sb) { + key.print(sb); + } + + +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/MapFn.java b/convex-core/src/main/java/convex/core/lang/impl/MapFn.java new file mode 100644 index 000000000..751ac88be --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/MapFn.java @@ -0,0 +1,41 @@ +package convex.core.lang.impl; + +import convex.core.data.ACell; +import convex.core.data.AMap; +import convex.core.lang.Context; + +public class MapFn extends ADataFn { + + private AMap map; + + public MapFn(AMap m) { + this.map = m; + } + + public static MapFn wrap(AMap m) { + return new MapFn(m); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public Context invoke(Context context, ACell[] args) { + int n = args.length; + T result; + if (n == 1) { + ACell key = args[0]; + result = (T) map.get(key); + } else if (n == 2) { + K key = (K) args[0]; + result = (T) map.get(key, args[1]); + } else { + return context.withArityError("Expected arity 1 or 2 for map lookup but got: " + n); + } + return context.withResult(result); + } + + @Override + public void print(StringBuilder sb) { + map.print(sb); + } + +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/MultiFn.java b/convex-core/src/main/java/convex/core/lang/impl/MultiFn.java new file mode 100644 index 000000000..cb845d3fc --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/MultiFn.java @@ -0,0 +1,149 @@ +package convex.core.lang.impl; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.Format; +import convex.core.data.IRefFunction; +import convex.core.data.Ref; +import convex.core.data.Tag; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.AFn; +import convex.core.lang.Context; + +public class MultiFn extends AClosure { + + private final AVector> fns; + private final int num; + + private MultiFn(AVector> fns, AVector env) { + super(env); + this.fns=fns; + this.num=fns.size(); + } + + private MultiFn(AVector> fns) { + this(fns,Context.EMPTY_BINDINGS); + } + + + public static MultiFn create(AVector> fns) { + return new MultiFn<>(fns); + } + + @Override + public boolean isCanonical() { + return true; + } + + @Override + public MultiFn toCanonical() { + return this; + } + + @Override + public void print(StringBuilder sb) { + sb.append("(fn "); + printInternal(sb); + sb.append(')'); + } + + @Override + public void printInternal(StringBuilder sb) { + for (long i=0; i0) sb.append(' '); + sb.append('('); + fns.get(i).printInternal(sb); + sb.append(')'); + } + } + + @Override + public Context invoke(Context context, ACell[] args) { + for (int i=0; i fn=fns.get(i); + if (fn.supportsArgs(args)) { + return fn.invoke((Context) context, args); + } + } + // TODO: type specific message? + return context.withArityError("No matching function arity found for arity "+args.length); + } + + @Override + public boolean hasArity(int n) { + for (int i=0; i fn=fns.get(i); + if (fn.hasArity(n)) { + return true; + } + } + return false; + } + + @Override + public AFn updateRefs(IRefFunction func) { + AVector> newFns=fns.updateRefs(func); + if (fns==newFns) return this; + return new MultiFn(newFns); + } + + @Override + public void validateCell() throws InvalidDataException { + if (num<=0) throw new InvalidDataException("MultiFn must contain at least one function",this); + fns.validateCell(); + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.FN_MULTI; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = fns.encode(bs,pos); + return pos; + } + + public static MultiFn read(ByteBuffer bb) throws BadFormatException, BufferUnderflowException { + AVector> fns=Format.read(bb); + if (fns==null) throw new BadFormatException("Null fns!"); + return new MultiFn(fns); + } + + @Override + public int estimatedEncodingSize() { + return fns.estimatedEncodingSize()+1; + } + + @Override + public int getRefCount() { + return fns.getRefCount(); + } + + @Override + public Ref getRef(int i) { + return fns.getRef(i); + } + + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public > F withEnvironment(AVector env) { + // TODO: Can make environment update more efficient? + if (env==this.lexicalEnv) return (F) this; + return (F) new MultiFn(fns.map(fn->fn.withEnvironment(env)),env); + } + + + + + + + + +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/RecordFormat.java b/convex-core/src/main/java/convex/core/lang/impl/RecordFormat.java new file mode 100644 index 000000000..e58b5d4ad --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/RecordFormat.java @@ -0,0 +1,59 @@ +package convex.core.lang.impl; + +import java.util.HashMap; +import java.util.Set; + +import convex.core.data.AVector; +import convex.core.data.Keyword; +import convex.core.data.Sets; +import convex.core.data.Vectors; + +public class RecordFormat { + + protected final long count; + protected final AVector keys; + protected final HashMap indexes = new HashMap<>(); + protected final Set keySet; + + private RecordFormat(AVector keys) { + this.keys = keys; + count = keys.count(); + for (int i = 0; i < count; i++) { + indexes.put(keys.get(i), Long.valueOf(i)); + } + keySet = Sets.create(keys); + } + + public long count() { + return count; + } + + public AVector getKeys() { + return keys; + } + + public boolean containsKey(Object key) { + return indexes.containsKey(key); + } + + public static RecordFormat of(Keyword... keys) { + return new RecordFormat(Vectors.create(keys)); + } + + public Set keySet() { + return keySet; + } + + public Long indexFor(Object key) { + return indexes.get(key); + } + + /** + * Gets the key at the specified index + * @param i Index of record key + * @return Keyword at the specified index + */ + public Keyword getKey(int i) { + return keys.get(i); + } +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/RecurValue.java b/convex-core/src/main/java/convex/core/lang/impl/RecurValue.java new file mode 100644 index 000000000..9d97ca577 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/RecurValue.java @@ -0,0 +1,45 @@ +package convex.core.lang.impl; + +import convex.core.ErrorCodes; +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.Vectors; + +/** + * Class representing a function return value. + * + * Contains argument values for each parameter to be substituted in the + * surrounding function / loop + */ +public class RecurValue extends ATrampoline { + + private RecurValue(ACell[] values) { + super(values); + } + + /** + * Wraps an object array as a RecurValue + * + * @param values Values to recur with + * @return new RecurValue + */ + public static RecurValue wrap(ACell... values) { + return new RecurValue(values); + } + + @Override + public String toString() { + AVector seq = Vectors.create(args); // should always convert OK + return "RecurValue: " + seq; + } + + @Override + public ACell getCode() { + return ErrorCodes.RECUR; + } + + @Override + public ACell getMessage() { + return null; + } +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/Reduced.java b/convex-core/src/main/java/convex/core/lang/impl/Reduced.java new file mode 100644 index 000000000..a88d343cb --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/Reduced.java @@ -0,0 +1,32 @@ +package convex.core.lang.impl; + +import convex.core.ErrorCodes; +import convex.core.data.ACell; + +public class Reduced extends AReturn { + + private final ACell value; + + public Reduced(ACell value) { + this.value=value; + } + + public static Reduced wrap(ACell value) { + return new Reduced(value); + } + + @Override + public ACell getCode() { + return ErrorCodes.REDUCED; + } + + @Override + public ACell getMessage() { + return null; + } + + public ACell getValue() { + return value; + } + +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/ReturnValue.java b/convex-core/src/main/java/convex/core/lang/impl/ReturnValue.java new file mode 100644 index 000000000..d773ab3a3 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/ReturnValue.java @@ -0,0 +1,43 @@ +package convex.core.lang.impl; + +import convex.core.ErrorCodes; +import convex.core.data.ACell; + +/** + * Class representing a function return value + * + * "Computers are useless. They can only give you answers." - Pablo Picasso + * + * @param Type of return value + */ +public class ReturnValue extends AReturn { + + private final T value; + + public ReturnValue(T value) { + this.value = value; + } + + public static ReturnValue wrap(T value) { + return new ReturnValue(value); + } + + public T getValue() { + return value; + } + + @Override + public String toString() { + return "ReturnValue: " + value; + } + + @Override + public ACell getCode() { + return ErrorCodes.RETURN; + } + + @Override + public ACell getMessage() { + return null; + } +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/RollbackValue.java b/convex-core/src/main/java/convex/core/lang/impl/RollbackValue.java new file mode 100644 index 000000000..f61a144cf --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/RollbackValue.java @@ -0,0 +1,43 @@ +package convex.core.lang.impl; + +import convex.core.ErrorCodes; +import convex.core.data.ACell; + +/** + * Class representing a function return value + * + * "Computers are useless. They can only give you answers." - Pablo Picasso + * + * @param Type of return value + */ +public class RollbackValue extends AReturn { + + private final T value; + + public RollbackValue(T value) { + this.value = value; + } + + public static RollbackValue wrap(T value) { + return new RollbackValue(value); + } + + public T getValue() { + return value; + } + + @Override + public String toString() { + return "RollbackValue: " + value; + } + + @Override + public ACell getCode() { + return ErrorCodes.ROLLBACK; + } + + @Override + public ACell getMessage() { + return value; + } +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/SeqFn.java b/convex-core/src/main/java/convex/core/lang/impl/SeqFn.java new file mode 100644 index 000000000..12b3b1eff --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/SeqFn.java @@ -0,0 +1,56 @@ +package convex.core.lang.impl; + +import convex.core.data.ACell; +import convex.core.data.ASequence; +import convex.core.data.prim.CVMLong; +import convex.core.data.type.Types; +import convex.core.lang.Context; +import convex.core.lang.RT; + +/** + * Wrapper for interpreting a sequence object as an invokable function + * + * + * @param Type of values to return + */ +public class SeqFn extends ADataFn { + + private ASequence seq; + + public SeqFn(ASequence m) { + this.seq = m; + } + + public static SeqFn wrap(ASequence m) { + return new SeqFn(m); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public Context invoke(Context context, ACell[] args) { + int n = args.length; + if (n == 1) { + CVMLong key = RT.ensureLong(args[0]); + if (key==null) return context.withCastError(0,args, Types.LONG); + long ix=key.longValue(); + if ((ix < 0) || (ix >= seq.count())) return (Context) context.withBoundsError(ix); + T result = (T) seq.get(key); + return context.withResult(result); + } else if (n == 2) { + CVMLong key = RT.ensureLong(args[0]); + if (key==null) return context.withCastError(0,args, Types.LONG); + long ix=key.longValue(); + if ((ix < 0) || (ix >= seq.count())) return (Context) context.withResult((T)args[1]); + T result = (T) seq.get(key); + return context.withResult(result); + } else { + return context.withArityError("Expected arity 1 or 2 for sequence lookup"); + } + } + + @Override + public void print(StringBuilder sb) { + seq.print(sb); + } + +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/SetFn.java b/convex-core/src/main/java/convex/core/lang/impl/SetFn.java new file mode 100644 index 000000000..de86c164e --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/SetFn.java @@ -0,0 +1,38 @@ +package convex.core.lang.impl; + +import convex.core.data.ACell; +import convex.core.data.ASet; +import convex.core.data.prim.CVMBool; +import convex.core.lang.Context; + +public class SetFn extends ADataFn { + + private ASet set; + + public SetFn(ASet m) { + this.set = m; + } + + public static SetFn wrap(ASet m) { + return new SetFn(m); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Context invoke(Context context, ACell[] args) { + int n = args.length; + if (n == 1) { + ACell key = args[0]; + CVMBool result = CVMBool.create(set.contains(key)); + return context.withResult(result); + } else { + return context.withArityError("Expected arity 1 for set lookup but got: " + n + " in set: " + set); + } + } + + @Override + public void print(StringBuilder sb) { + set.print(sb); + } + +} diff --git a/convex-core/src/main/java/convex/core/lang/impl/TailcallValue.java b/convex-core/src/main/java/convex/core/lang/impl/TailcallValue.java new file mode 100644 index 000000000..b30677eed --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/impl/TailcallValue.java @@ -0,0 +1,49 @@ +package convex.core.lang.impl; + +import convex.core.ErrorCodes; +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.Vectors; +import convex.core.lang.AFn; + +/** + * Class representing a function return value. + * + * Contains argument values for each parameter to be substituted in the + * surrounding function / loop + */ +public class TailcallValue extends ATrampoline { + + private AFn function; + + private TailcallValue(AFn f, ACell[] values) { + super(values); + this.function=f; + } + + public static TailcallValue wrap(AFn f, ACell[] args) { + return new TailcallValue(f,args); + } + + @Override + public String toString() { + AVector seq = Vectors.create(args); // should always convert OK + return "Tailcall: " + seq; + } + + @Override + public ACell getCode() { + return ErrorCodes.TAILCALL; + } + + @Override + public ACell getMessage() { + return null; + } + + public AFn getFunction() { + return function; + } + + +} diff --git a/convex-core/src/main/java/convex/core/lang/ops/AMultiOp.java b/convex-core/src/main/java/convex/core/lang/ops/AMultiOp.java new file mode 100644 index 000000000..0cb2c533d --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/ops/AMultiOp.java @@ -0,0 +1,67 @@ +package convex.core.lang.ops; + +import convex.core.data.ACell; +import convex.core.data.ASequence; +import convex.core.data.AVector; +import convex.core.data.Format; +import convex.core.data.IRefFunction; +import convex.core.data.Ref; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.AOp; + +/** + * Abstract base class for Ops with multiple nested operations + * + * MultiOps may selectively evaluate sub-expressions. + * + * @param Type of function return + */ +public abstract class AMultiOp extends AOp { + protected final AVector> ops; + + protected AMultiOp(AVector> ops) { + // TODO: need to think about bounds on number of child ops? + this.ops = ops; + } + + /** + * Recreates this object with an updated list of child Ops. + * + * @param newOps + * @return + */ + protected abstract AMultiOp recreate(ASequence> newOps); + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = Format.write(bs,pos, ops); + return pos; + } + + @Override + public int estimatedEncodingSize() { + return 10+ops.estimatedEncodingSize(); + } + + @Override + public AMultiOp updateRefs(IRefFunction func) { + ASequence> newOps = ops.updateRefs(func); + return recreate(newOps); + } + + @Override + public int getRefCount() { + return ops.getRefCount(); + } + + @SuppressWarnings("unchecked") + @Override + public Ref getRef(int i) { + return (Ref) ops.getRef(i); + } + + @Override + public void validateCell() throws InvalidDataException { + ops.validateCell(); + } +} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Cond.java b/convex-core/src/main/java/convex/core/lang/ops/Cond.java new file mode 100644 index 000000000..9cd3d0697 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/ops/Cond.java @@ -0,0 +1,112 @@ +package convex.core.lang.ops; + +import java.nio.ByteBuffer; + +import convex.core.data.ACell; +import convex.core.data.ASequence; +import convex.core.data.AVector; +import convex.core.data.Format; +import convex.core.data.IRefFunction; +import convex.core.data.Vectors; +import convex.core.exceptions.BadFormatException; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.Juice; +import convex.core.lang.Ops; +import convex.core.lang.RT; + +/** + * Op representing a conditional expression. + * + * Child ops: + * 1. Should be condition / result pairs (with an optional single default result). + * 2. Are executed in sequence until the first condition succeeds + * 3. Are only executed if required, i.e. cond operates as a "short-circuiting" conditional. + * + * @param Result type of Op + */ +public class Cond extends AMultiOp { + + protected Cond(AVector> ops) { + super(ops); + } + + /** + * Create a Cond operation with the given nested operations + * + * @param Return type of Cond + * @param ops Ops to execute conditionally + * @return Cond instance + */ + public static Cond create(AOp... ops) { + ASequence> refOps=Vectors.create(ops); + return create(refOps); + } + + @Override + protected Cond recreate(ASequence> newOps) { + if (ops==newOps) return this; + return new Cond(newOps.toVector()); + } + + public static Cond create(ASequence> ops) { + return new Cond(ops.toVector()); + } + + @SuppressWarnings("unchecked") + @Override + public Context execute(Context context) { + int n=ops.size(); + Context ctx=context.consumeJuice(Juice.COND_OP); + if (ctx.isExceptional()) return (Context) ctx; + + for (int i=0; i<(n-1); i+=2) { + AOp testOp=ops.get(i); + ctx=ctx.execute(testOp); + + // bail out from exceptional result in test + if (ctx.isExceptional()) return (Context) ctx; + + ACell test=ctx.getResult(); + if (RT.bool(test)) { + return (Context) ctx.execute(ops.get(i+1)); + } + } + if ((n&1)==0) { + // no default value, return null + return ctx.withResult((T)null); + } else { + // default value + return (Context) ctx.execute(ops.get(n-1)); + } + } + + @Override + public void print(StringBuilder sb) { + sb.append("(cond"); + int len=ops.size(); + for (int i=0; i Cond read(ByteBuffer b) throws BadFormatException { + AVector> ops=Format.read(b); + return create(ops); + } + + @Override + public Cond updateRefs(IRefFunction func) { + ASequence> newOps= ops.updateRefs(func); + return recreate(newOps); + } + + +} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Constant.java b/convex-core/src/main/java/convex/core/lang/ops/Constant.java new file mode 100644 index 000000000..465adb512 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/ops/Constant.java @@ -0,0 +1,131 @@ +package convex.core.lang.ops; + +import java.nio.ByteBuffer; + +import convex.core.data.AList; +import convex.core.data.AString; +import convex.core.data.AVector; +import convex.core.data.Format; +import convex.core.data.IRefFunction; +import convex.core.data.List; +import convex.core.data.Ref; +import convex.core.data.Strings; +import convex.core.data.VectorLeaf; +import convex.core.data.prim.CVMBool; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.Juice; +import convex.core.lang.Ops; +import convex.core.lang.RT; +import convex.core.data.ACell; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * Operation representing a constant value + * + * "One man's constant is another man's variable." - Alan Perlis + * + * @param Type of constant value + */ +public class Constant extends AOp { + + public static final Constant NULL = new Constant<>(Ref.NULL_VALUE); + public static final Constant TRUE = new Constant<>(Ref.TRUE_VALUE); + public static final Constant FALSE = new Constant<>(Ref.FALSE_VALUE); + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static final Constant> EMPTY_VECTOR = new Constant(VectorLeaf.EMPTY_REF); + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static final Constant> EMPTY_LIST = new Constant(List.EMPTY_REF); + + + private final Ref valueRef; + + private Constant(Ref valueRef) { + this.valueRef = valueRef; + } + + public static Constant create(T value) { + return new Constant(Ref.get(value)); + } + + public static Constant of(Object value) { + return create(RT.cvm(value)); + } + + public static Constant createString(String stringValue) { + return new Constant(Strings.create(stringValue).getRef()); + } + + public static Constant createFromRef(Ref valueRef) { + if (valueRef == null) throw new IllegalArgumentException("Can't create with null ref"); + return new Constant(valueRef); + } + + @Override + public Context execute(Context context) { + return context.withResult(Juice.CONSTANT, valueRef.getValue()); + } + + @Override + public void print(StringBuilder sb) { + Utils.print(sb,valueRef.getValue()); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = valueRef.encode(bs,pos); + return pos; + } + + @Override + public int estimatedEncodingSize() { + return 1+Format.MAX_EMBEDDED_LENGTH; + } + + public static AOp read(ByteBuffer bb) throws BadFormatException { + Ref ref = Format.readRef(bb); + return createFromRef(ref); + } + + @Override + public byte opCode() { + return Ops.CONSTANT; + } + + @Override + public int getRefCount() { + return 1; + } + + @SuppressWarnings("unchecked") + @Override + public Ref getRef(int i) { + if (i != 0) throw new IndexOutOfBoundsException(Errors.badIndex(i)); + return (Ref) valueRef; + } + + @Override + public Constant updateRefs(IRefFunction func) { + @SuppressWarnings("unchecked") + Ref newRef = (Ref) func.apply(valueRef); + if (valueRef == newRef) return this; + return createFromRef(newRef); + } + + @SuppressWarnings("unchecked") + public static AOp nil() { + return (AOp) Constant.NULL; + } + + @Override + public void validateCell() throws InvalidDataException { + if (valueRef == null) throw new InvalidDataException("Missing contant value ref!", this); + } + + +} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Def.java b/convex-core/src/main/java/convex/core/lang/ops/Def.java new file mode 100644 index 000000000..1053bf5a4 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/ops/Def.java @@ -0,0 +1,155 @@ +package convex.core.lang.ops; + +import java.nio.ByteBuffer; + +import convex.core.data.Format; +import convex.core.data.IRefFunction; +import convex.core.data.Ref; +import convex.core.data.Symbol; +import convex.core.data.Syntax; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.Juice; +import convex.core.lang.Ops; +import convex.core.lang.RT; +import convex.core.data.ACell; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * Op that creates a definition in the current environment. + * + * Def may optionally have symbolic metadata attached to the symbol. + * + * @param Type of defined value + */ +public class Def extends AOp { + + // symbol Syntax Object including metadata to add to the defined environment + private final ACell symbol; + + // expression to execute to determine the defined value + private final Ref> op; + + private Def(ACell key, Ref> op) { + this.op = op; + this.symbol = key; + if (symbol==null) throw new IllegalArgumentException("Null key in Def!!!"); + } + + public static Def create(ACell key, Ref> op) { + if (!validKey(key)) throw new IllegalArgumentException("Invalid Def key: "+key); + return new Def(key, op); + } + + public static Def create(Syntax key, Ref> op) { + if (!validKey(key)) throw new IllegalArgumentException("Invalid Def key: "+key); + return new Def(key, op); + } + + public static Def create(Syntax key, AOp op) { + return create(key, op.getRef()); + } + + public static Def create(Symbol key, AOp op) { + return new Def(key, op.getRef()); + } + + public static Def create(ACell key, AOp op) { + return new Def(key, op.getRef()); + } + + public static Def create(String key, AOp op) { + return create(Symbol.create(key), op); + } + + @SuppressWarnings("unchecked") + @Override + public Context execute(Context context) { + Context ctx = context.execute(op.getValue()); + if (ctx.isExceptional()) return ctx; + + ACell opResult = ctx.getResult(); + + // TODO: defined syntax metadata + if (symbol instanceof Syntax) { + Syntax syn=(Syntax)symbol; + ctx = ctx.defineWithSyntax(syn, opResult); + } else { + ctx=ctx.define((Symbol)symbol, opResult); + } + return (Context) ctx.withResult(Juice.DEF, opResult); + } + + @Override + public int getRefCount() { + return 1; + } + + @SuppressWarnings("unchecked") + @Override + public Ref> getRef(int i) { + if (i != 0) throw new IndexOutOfBoundsException(Errors.badIndex(i)); + return op; + } + + @SuppressWarnings("unchecked") + @Override + public Def updateRefs(IRefFunction func) { + ACell newSymbol=symbol.updateRefs(func); + Ref> newRef = (Ref>) func.apply(op); + if (op == newRef) return this; + return new Def(newSymbol, newRef); + } + + @Override + public void print(StringBuilder sb) { + sb.append("(def "); + symbol.print(sb); + sb.append(' '); + Utils.print(sb, op.getValue()); + sb.append(')'); + } + + @Override + public byte opCode() { + return Ops.DEF; + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = Format.write(bs,pos, symbol); + pos = op.encode(bs,pos); + return pos; + } + + @Override + public int estimatedEncodingSize() { + return symbol.estimatedEncodingSize()+Format.MAX_EMBEDDED_LENGTH; + } + + public static Def read(ByteBuffer b) throws BadFormatException { + ACell symbol = Format.read(b); + Ref> ref = Format.readRef(b); + if (!validKey(symbol)) throw new BadFormatException("Symbol not valid for Def op"); + return new Def<>(symbol, ref); + } + + @Override + public void validateCell() throws InvalidDataException { + if (!validKey(symbol)) { + throw new InvalidDataException("Def requires a Symbol or Syntax Object for definition but was: "+RT.getType(symbol),this); + } + symbol.validateCell(); + } + + private static boolean validKey(ACell key) { + if (key instanceof Symbol) return true; + if (!(key instanceof Syntax)) return false; + return ((Syntax)key).getValue() instanceof Symbol; + } + + +} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Do.java b/convex-core/src/main/java/convex/core/lang/ops/Do.java new file mode 100644 index 000000000..628ad38c5 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/ops/Do.java @@ -0,0 +1,88 @@ +package convex.core.lang.ops; + +import java.nio.ByteBuffer; + +import convex.core.data.ACell; +import convex.core.data.ASequence; +import convex.core.data.AVector; +import convex.core.data.Format; +import convex.core.data.Vectors; +import convex.core.exceptions.BadFormatException; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.Juice; +import convex.core.lang.Ops; + +/** + * Op for executing a sequence of child operations in order + * + * "Design is to take things apart in such a way that they can be put back + * together" + * - Rich Hickey + * + * @param Result type of Do Op + */ +public class Do extends AMultiOp { + + public static final Do EMPTY = Do.create(); + + protected Do(AVector> ops) { + super(ops); + } + + public static Do create(AOp... ops) { + return new Do(Vectors.create(ops)); + } + + @Override + protected Do recreate(ASequence> newOps) { + if (ops == newOps) return this; + return new Do(newOps.toVector()); + } + + public static Do create(ASequence> ops) { + return new Do(ops.toVector()); + } + + @SuppressWarnings("unchecked") + @Override + public Context execute(Context context) { + int n = ops.size(); + if (n == 0) return (Context) context.withResult(Juice.DO, null); // need cast to avoid bindings overload + + Context ctx = (Context) context.consumeJuice(Juice.DO); + if (ctx.isExceptional()) return ctx; + + // execute each operation in turn + // TODO: early return + for (int i = 0; i < n; i++) { + AOp op = ops.get(i); + ctx = (Context) ctx.execute(op); + + if (ctx.isExceptional()) break; + + } + return ctx; + } + + @Override + public void print(StringBuilder sb) { + sb.append("(do"); + int len = ops.size(); + for (int i = 0; i < len; i++) { + sb.append(' '); + ops.get(i).print(sb); + } + sb.append(')'); + } + + @Override + public byte opCode() { + return Ops.DO; + } + + public static Do read(ByteBuffer b) throws BadFormatException { + AVector> ops = Format.read(b); + return create(ops); + } +} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Invoke.java b/convex-core/src/main/java/convex/core/lang/ops/Invoke.java new file mode 100644 index 000000000..9bb1abeee --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/ops/Invoke.java @@ -0,0 +1,108 @@ +package convex.core.lang.ops; + +import java.nio.ByteBuffer; + +import convex.core.data.ACell; +import convex.core.data.ASequence; +import convex.core.data.AVector; +import convex.core.data.Format; +import convex.core.data.Vectors; +import convex.core.data.type.Types; +import convex.core.exceptions.BadFormatException; +import convex.core.lang.AFn; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.Ops; +import convex.core.lang.RT; + +/** + * Op representing the invocation of a function. + * + * The first child Op identifies the function to be called, the remaining ops + * are arguments. + * + * @param Result type of Op + */ +public class Invoke extends AMultiOp { + + protected Invoke(AVector> ops) { + super(ops); + } + + public static Invoke create(ASequence> ops) { + AVector> vops = ops.toVector(); + return new Invoke(vops); + } + + public static Invoke create(AOp... ops) { + return create(Vectors.create(ops)); + } + + @SuppressWarnings("unchecked") + public static , F extends AOp> Invoke create(F f, ASequence args) { + ASequence> nargs = (ASequence>) args; + ASequence> ops = nargs.cons(f); + + return create(ops); + } + + @Override + protected Invoke recreate(ASequence> newOps) { + if (ops == newOps) return this; + return create(newOps); + } + + public static Invoke create(String string, AOp... args) { + return create(Lookup.create(string), Vectors.create(args)); + } + + @SuppressWarnings("unchecked") + @Override + public Context execute(Context context) { + // execute first op to obtain function value + AOp fnOp=ops.get(0); + Context ctx = (Context) context.execute(fnOp); + if (ctx.isExceptional()) return ctx; + + ACell rf = ctx.getResult(); + AFn fn = RT.castFunction(rf); + if (fn == null) return context.withCastError(0, Types.FUNCTION); + + int arity = ops.size() - 1; + ACell[] args = new ACell[arity]; + for (int i = 0; i < arity; i++) { + // Compute the op for each argument in order + AOp argOp=ops.get(i + 1); + ctx = (Context) ctx.execute(argOp); + if (ctx.isExceptional()) return ctx; + + args[i] = ctx.getResult(); + } + + ctx = ctx.invoke(fn, args); + return (Context) ctx; + } + + @Override + public void print(StringBuilder sb) { + sb.append('('); + int len = ops.size(); + for (int i = 0; i < len; i++) { + if (i > 0) sb.append(' '); + ops.get(i).print(sb); + } + sb.append(')'); + } + + @Override + public byte opCode() { + return Ops.INVOKE; + } + + public static Invoke read(ByteBuffer bb) throws BadFormatException { + AVector> ops = Format.read(bb); + if (ops == null) throw new BadFormatException("Can't read an Invoke with no ops"); + + return create(ops); + } +} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Lambda.java b/convex-core/src/main/java/convex/core/lang/ops/Lambda.java new file mode 100644 index 000000000..8d3af901b --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/ops/Lambda.java @@ -0,0 +1,111 @@ +package convex.core.lang.ops; + +import java.nio.ByteBuffer; + +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.Format; +import convex.core.data.IRefFunction; +import convex.core.data.Ref; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.Juice; +import convex.core.lang.Ops; +import convex.core.lang.impl.AClosure; +import convex.core.lang.impl.Fn; +import convex.core.util.Errors; +import convex.core.util.Utils; + +/** + * Op responsible for creating a new function (closure). + * + * Captures value of local variable bindings during execution. + * + * Equivalent to (fn [...] ...) + * + * @param Result type of Closure + */ +public class Lambda extends AOp> { + + private Ref> function; + + protected Lambda(Ref> newFunction) { + this.function=newFunction; + } + + public static Lambda create(AVector params, AOp body) { + return new Lambda(Fn.create(params,body).getRef()); + } + + public static Lambda create(AClosure fn) { + return new Lambda(fn.getRef()); + } + + @Override + public Context> execute(Context context) { + AClosure fn= function.getValue().withEnvironment(context.getLocalBindings()); + return context.withResult(Juice.LAMBDA,fn); + } + + @Override + public int getRefCount() { + return 1; + } + + @SuppressWarnings("unchecked") + @Override + public Ref getRef(int i) { + if (i==0) return (Ref) function; + throw new IndexOutOfBoundsException(Errors.badIndex(i)); + } + + @Override + public Lambda updateRefs(IRefFunction func) { + @SuppressWarnings("unchecked") + Ref> newFunction=(Ref>) func.apply(function); + if (function==newFunction) return this; + return new Lambda(newFunction); + } + + @Override + public void print(StringBuilder sb) { + function.getValue().print(sb); + } + + @Override + public byte opCode() { + return Ops.LAMBDA; + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos=function.encode(bs, pos); + return pos; + } + + public static Lambda read(ByteBuffer bb) throws BadFormatException { + Ref> function=Format.readRef(bb); + return new Lambda(function); + } + + @Override + public void validate() throws InvalidDataException { + super.validate(); + + ACell fn=function.getValue(); + if (!(fn instanceof AClosure)) { + throw new InvalidDataException("Lambda child must be a closure but got: "+Utils.getClassName(fn),this); + } + } + + @Override + public void validateCell() throws InvalidDataException { + // nothing to do? + } + + public AClosure getFunction() { + return function.getValue(); + } +} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Let.java b/convex-core/src/main/java/convex/core/lang/ops/Let.java new file mode 100644 index 000000000..38c9e1bbd --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/ops/Let.java @@ -0,0 +1,179 @@ +package convex.core.lang.ops; + +import java.nio.ByteBuffer; + +import convex.core.data.ACell; +import convex.core.data.ASequence; +import convex.core.data.AVector; +import convex.core.data.Format; +import convex.core.data.IRefFunction; +import convex.core.data.Ref; +import convex.core.exceptions.BadFormatException; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.Juice; +import convex.core.lang.Ops; +import convex.core.lang.impl.RecurValue; +import convex.core.util.Utils; + +/** + * Op for executing a body after lexically binding one or more symbols. + * + * Can represent (let [..] ..) and (loop [..] ..). + * + * Loop version can act as a target for (recur ...) expressions. + * + * @param Result type of Op + */ +public class Let extends AMultiOp { + + /** + * Vector of binding forms. Can be destructuring forms + */ + protected final AVector symbols; + + protected final int bindingCount; + protected final boolean isLoop; + + protected Let(AVector syms, AVector> ops, boolean isLoop) { + super(ops); + symbols = syms; + bindingCount = syms.size(); + this.isLoop = isLoop; + } + + public static Let create(AVector syms, AVector> ops, boolean isLoop) { + return new Let(syms, ops, isLoop); + } + + @Override + public Let updateRefs(IRefFunction func) { + ASequence> newOps = ops.updateRefs(func); + AVector newSymbols = symbols.updateRefs(func); + + return recreate(newOps, newSymbols); + } + + @Override + public int getRefCount() { + return super.getRefCount() + symbols.getRefCount(); + } + + @Override + public final Ref getRef(int i) { + int n = super.getRefCount(); + if (i < n) return super.getRef(i); + return symbols.getRef(i - n); + } + + @Override + protected Let recreate(ASequence> newOps) { + return recreate(newOps, symbols); + } + + protected Let recreate(ASequence> newOps, AVector newSymbols) { + if ((ops == newOps) && (symbols == newSymbols)) return this; + return new Let(newSymbols, newOps.toVector(), isLoop); + } + + @SuppressWarnings("unchecked") + @Override + public Context execute(final Context context) { + Context ctx = context.consumeJuice(Juice.LET); + if (ctx.isExceptional()) return (Context) ctx; + + AVector savedEnv = ctx.getLocalBindings(); + + // execute each operation for bound values in turn + for (int i = 0; i < bindingCount; i++) { + AOp op = ops.get(i); + ctx = ctx.executeLocalBinding(symbols.get(i), op); + if (ctx.isExceptional()) { + // return if exception during initial binding. + // No chance to recur since we didn't enter loop body + return ctx.withLocalBindings(savedEnv); + } + } + + ctx = executeBody(ctx); + if (isLoop&&ctx.isExceptional()) { + // check for recur if this Let form is a loop + // other exceptionals we can just let slip + Object o = ctx.getExceptional(); + while (o instanceof RecurValue) { + RecurValue rv = (RecurValue) o; + ACell[] newArgs = rv.getValues(); + if (newArgs.length != bindingCount) { + // recur arity is wrong, need to break loop with exceptional result + String message="Expected " + bindingCount + " value(s) for recur but got: " + newArgs.length; + ctx = ctx.withArityError(message); + break; + } + + // restore old lexical environment, then add back new ones + ctx=ctx.withLocalBindings(savedEnv); + ctx = ctx.updateBindings(symbols, newArgs); + if (ctx.isExceptional()) break; + + ctx = executeBody(ctx); + o = ctx.getValue(); + } + } + // restore old lexical environment before returning + return ctx.withLocalBindings(savedEnv); + } + + public Context executeBody(Context ctx) { + int end = ops.size(); + if (bindingCount == end) return ctx.withResult(null); + for (int i = bindingCount; i < end; i++) { + ctx = ctx.execute(ops.get(i)); + if (ctx.isExceptional()) { + return ctx; + } + } + return ctx; + } + + @Override + public void print(StringBuilder sb) { + sb.append(isLoop ? "(loop [" : "(let ["); + int len = ops.size(); + for (int i = 0; i < bindingCount; i++) { + if (i > 0) sb.append(' '); + Utils.print(sb, symbols.get(i)); + sb.append(' '); + ops.get(i).print(sb); + sb.append(' '); + } + sb.append("] "); + + for (int i = bindingCount; i < len; i++) { + sb.append(' '); + ops.get(i).print(sb); + } + sb.append(')'); + } + + @Override + public byte opCode() { + return (isLoop)?Ops.LOOP:Ops.LET; + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = Format.write(bs,pos, symbols); + return super.encodeRaw(bs,pos); // AMultiOp superclass writeRaw + } + + @Override + public int estimatedEncodingSize() { + return super.estimatedEncodingSize()+symbols.estimatedEncodingSize(); + } + + public static Let read(ByteBuffer b, boolean isLoop) throws BadFormatException { + AVector syms = Format.read(b); + AVector> ops = Format.read(b); + return create(syms, ops.toVector(),isLoop); + } +} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Local.java b/convex-core/src/main/java/convex/core/lang/ops/Local.java new file mode 100644 index 000000000..ac7bca5c2 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/ops/Local.java @@ -0,0 +1,104 @@ +package convex.core.lang.ops; + +import java.nio.ByteBuffer; + +import convex.core.ErrorCodes; +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.Format; +import convex.core.data.IRefFunction; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.Juice; +import convex.core.lang.Ops; + +/** + * Op to look up a local value from the lexical environment + * + * @param Result type of Op + */ +public class Local extends AOp { + + /** + * Stack position in lexical stack + */ + private final long position; + + + private Local(long position) { + this.position=position; + } + + + /** + * Creates Local to look up a lexical value in the given position + * + * @param position Position in lexical value vector + * @return Special instance, or null if not found + */ + public static final Local create(long position) { + if (position<0) return null; + return new Local(position); + } + + @SuppressWarnings("unchecked") + @Override + public Context execute(Context context) { + Context ctx=(Context) context; + AVector env=ctx.getLocalBindings(); + long ec=env.count(); + if ((position<0)||(position>=ec)) { + return ctx.withError(ErrorCodes.BOUNDS,"Bad position for Local: "+position); + } + T result = (T)env.get(position); + return (Context) ctx.withResult(Juice.LOOKUP,result); + } + + @Override + public byte opCode() { + return Ops.LOCAL; + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos=Format.writeVLCLong(bs, pos, position); + return pos; + } + + public static Local read(ByteBuffer bb) throws BadFormatException { + long position=Format.readVLCLong(bb); + return create(position); + } + + @Override + public Local updateRefs(IRefFunction func) { + return this; + } + + @Override + public void validateCell() throws InvalidDataException { + if (position<0) { + throw new InvalidDataException("Invalid Local position "+position, this); + } + } + + @Override + public int getRefCount() { + return 0; + } + + @Override + public void print(StringBuilder sb) { + sb.append(toString()); + } + + @Override + public String toString() { + return "%" + position; + } + + + +} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Lookup.java b/convex-core/src/main/java/convex/core/lang/ops/Lookup.java new file mode 100644 index 000000000..9a2020dde --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/ops/Lookup.java @@ -0,0 +1,136 @@ +package convex.core.lang.ops; + +import java.nio.ByteBuffer; + +import convex.core.ErrorCodes; +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.data.Format; +import convex.core.data.IRefFunction; +import convex.core.data.Ref; +import convex.core.data.Symbol; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.Juice; +import convex.core.lang.Ops; +import convex.core.lang.RT; + +/** + * Op to look up a Symbol in the current execution context. + * + * Holds an optional Address to specify lookup in another Account environment. If null, the Lookup will be performed in + * the current environment. + * + * Consumes juice for lookup when executed. + * + * @param Result type of Op + */ +public class Lookup extends AOp { + private final AOp
address; + private final Symbol symbol; + + private Lookup(AOp
address,Symbol symbol) { + this.address=address; + this.symbol = symbol; + } + + public static Lookup create(AOp
address, Symbol form) { + return new Lookup(address,form); + } + + public static Lookup create(AOp
address, String name) { + return create(address,Symbol.create(name)); + } + + public static Lookup create(Address addr, Symbol sym) { + return create(Constant.of(addr),sym); + } + + public static Lookup create(Symbol symbol) { + return create((AOp
)null,symbol); + } + + public static Lookup create(String name) { + return create(Symbol.create(name)); + } + + @SuppressWarnings("unchecked") + @Override + public Context execute(Context context) { + Context rctx=(Context) context; + Address namespaceAddress=null; + if (address!=null) { + rctx=(Context) rctx.execute(address); + if (rctx.isExceptional()) return rctx; + ACell maybeAddress=rctx.getResult(); + namespaceAddress=RT.ensureAddress(maybeAddress); + if (namespaceAddress==null) return rctx.withError(ErrorCodes.CAST,"Lookup requires Address but got: "+RT.getType(maybeAddress)); + } + + // Do a dynamic lookup, with address if specified or address from current context otherwise + namespaceAddress=(address==null)?context.getAddress():namespaceAddress; + return rctx.lookupDynamic(namespaceAddress,symbol).consumeJuice(Juice.LOOKUP_DYNAMIC); + } + + @Override + public void print(StringBuilder sb) { + if (address!=null) { + address.print(sb); + sb.append('/'); + } + symbol.print(sb); + } + + @Override + public byte opCode() { + return Ops.LOOKUP; + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos= symbol.encode(bs, pos); + pos= Format.write(bs,pos, address); // might be null + return pos; + } + + public static Lookup read(ByteBuffer bb) throws BadFormatException { + Symbol sym = Format.read(bb); + if (sym==null) throw new BadFormatException("Lookup symbol cannot be null"); + AOp
address = Format.read(bb); + return create(address,sym); + } + + @Override + public int getRefCount() { + if (address==null) return 0; + return address.getRefCount(); + } + + @Override + public Ref getRef(int i) { + if (address==null) throw new IndexOutOfBoundsException(); + return address.getRef(i); + } + + @Override + public Lookup updateRefs(IRefFunction func) { + if (address==null) return this; + AOp
newAddress=address.updateRefs(func); + if (address==newAddress) return this; + return create(newAddress,symbol); + } + + @Override + public void validateCell() throws InvalidDataException { + if (address!=null) address.validateCell(); + symbol.validateCell(); + } + + public AOp
getAddress() { + return address; + } + + +} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Query.java b/convex-core/src/main/java/convex/core/lang/ops/Query.java new file mode 100644 index 000000000..6cf0fea30 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/ops/Query.java @@ -0,0 +1,94 @@ +package convex.core.lang.ops; + +import java.nio.ByteBuffer; + +import convex.core.State; +import convex.core.data.ACell; +import convex.core.data.ASequence; +import convex.core.data.AVector; +import convex.core.data.Format; +import convex.core.data.Vectors; +import convex.core.exceptions.BadFormatException; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.Juice; +import convex.core.lang.Ops; + +/** + * Op for executing a sequence of child operations in order + * + * "Design is to take things apart in such a way that they can be put back + * together" + * - Rich Hickey + * + * @param Result type of Op + */ +public class Query extends AMultiOp { + + protected Query(AVector> ops) { + super(ops); + } + + public static Query create(AOp... ops) { + return new Query(Vectors.create(ops)); + } + + @Override + protected Query recreate(ASequence> newOps) { + if (ops == newOps) return this; + return new Query(newOps.toVector()); + } + + public static Query create(ASequence> ops) { + return new Query(ops.toVector()); + } + + @SuppressWarnings("unchecked") + @Override + public Context execute(Context context) { + State savedState=context.getState(); + + int n = ops.size(); + if (n == 0) return (Context) context.withResult(Juice.QUERY, null); // need cast to avoid bindings overload + + Context ctx = (Context) context.consumeJuice(Juice.QUERY); + if (ctx.isExceptional()) return ctx; + + AVector savedBindings=context.getLocalBindings(); + + // execute each operation in turn + // TODO: early return + for (int i = 0; i < n; i++) { + AOp op = ops.get(i); + ctx = (Context) ctx.execute(op); + + if (ctx.isExceptional()) break; + + } + // restore state unconditionally. + ctx=ctx.withState(savedState); + ctx=ctx.withLocalBindings(savedBindings); + return ctx; + } + + @Override + public void print(StringBuilder sb) { + sb.append("(query"); + int len = ops.size(); + for (int i = 0; i < len; i++) { + sb.append(' '); + ops.get(i).print(sb); + } + sb.append(')'); + } + + @Override + public byte opCode() { + return Ops.QUERY; + } + + public static Query read(ByteBuffer b) throws BadFormatException { + AVector> ops = Format.read(b); + return create(ops); + } +} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Set.java b/convex-core/src/main/java/convex/core/lang/ops/Set.java new file mode 100644 index 000000000..5f29ecee0 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/ops/Set.java @@ -0,0 +1,128 @@ +package convex.core.lang.ops; + +import java.nio.ByteBuffer; + +import convex.core.ErrorCodes; +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.Format; +import convex.core.data.IRefFunction; +import convex.core.data.Ref; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.Juice; +import convex.core.lang.Ops; +import convex.core.util.Errors; + +/** + * Op to set a lexical value in the local execution context. + * + * @param Result type of Op + */ +public class Set extends AOp { + + /** + * Stack position in lexical stack + */ + private final long position; + + /** + * Op to compute new value + */ + private final Ref> op; + + private Set(long position, Ref> op) { + this.position = position; + this.op = op; + } + + /** + * Creates special Op for the given opCode + * + * @param position Position in lexical value vector + * @param op Op to calculate new value + * @return Special instance, or null if not found + */ + public static final Set create(long position, AOp op) { + if (position < 0) return null; + return new Set(position, op.getRef()); + } + + @SuppressWarnings("unchecked") + @Override + public Context execute(Context context) { + Context ctx = (Context) context; + AVector env = ctx.getLocalBindings(); + long ec = env.count(); + if ((position < 0) || (position >= ec)) + return context.withError(ErrorCodes.BOUNDS, "Bad position for set!: " + position); + + ctx = ctx.execute(op.getValue()); + if (ctx.isExceptional()) return ctx; + ACell value = ctx.getResult(); + + AVector newEnv = env.assoc(position, value); + ctx = ctx.withLocalBindings(newEnv); + return ctx.consumeJuice(Juice.SET_BANG); + } + + @Override + public byte opCode() { + return Ops.SET; + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = Format.writeVLCLong(bs, pos, position); + return pos; + } + + public static Set read(ByteBuffer bb) throws BadFormatException { + long position = Format.readVLCLong(bb); + AOp op = Format.read(bb); + return create(position, op); + } + + @Override + public Set updateRefs(IRefFunction func) { + @SuppressWarnings("unchecked") + Ref> newOp = (Ref>) func.apply(op); + if (op == newOp) return this; + return new Set(position, newOp); + } + + @Override + public void validateCell() throws InvalidDataException { + if (op == null) { + throw new InvalidDataException("Null Set op ", this); + } + if (position < 0) { + throw new InvalidDataException("Invalid Local position " + position, this); + } + } + + @Override + public int getRefCount() { + return 1; + } + + @SuppressWarnings("unchecked") + @Override + public Ref> getRef(int i) { + if (i != 0) throw new IndexOutOfBoundsException(Errors.badIndex(i)); + return op; + } + + @Override + public void print(StringBuilder sb) { + sb.append(toString()); + } + + @Override + public String toString() { + return "(set! %" + position + " " + op.getValue() + ")"; + } + +} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Special.java b/convex-core/src/main/java/convex/core/lang/ops/Special.java new file mode 100644 index 000000000..53d7d877b --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/ops/Special.java @@ -0,0 +1,154 @@ +package convex.core.lang.ops; + +import java.util.HashMap; + +import convex.core.data.ACell; +import convex.core.data.IRefFunction; +import convex.core.data.Symbol; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.Juice; +import convex.core.lang.Ops; +import convex.core.lang.Symbols; + +public class Special extends AOp { + + private final byte opCode; + + private static int NUM_SPECIALS=14; + private static final int BASE=Ops.SPECIAL_BASE; + private static final int LIMIT=BASE+NUM_SPECIALS; + private static final Symbol[] symbols=new Symbol[NUM_SPECIALS]; + private static final Special[] specials=new Special[NUM_SPECIALS]; + private static final HashMap opcodes=new HashMap<>(); + + private static final byte S_JUICE=BASE+0; + private static final byte S_CALLER=BASE+1; + private static final byte S_ADDRESS=BASE+2; + private static final byte S_MEMORY=BASE+3; + private static final byte S_BALANCE=BASE+4; + private static final byte S_ORIGIN=BASE+5; + private static final byte S_RESULT=BASE+6; + private static final byte S_TIMESTAMP=BASE+7; + private static final byte S_DEPTH=BASE+8; + private static final byte S_OFFER=BASE+9; + private static final byte S_STATE=BASE+10; + private static final byte S_HOLDINGS=BASE+11; + private static final byte S_SEQUENCE=BASE+12; + private static final byte S_KEY=BASE+13; + + static { + reg(S_JUICE,Symbols.STAR_JUICE); + reg(S_CALLER,Symbols.STAR_CALLER); + reg(S_ADDRESS,Symbols.STAR_ADDRESS); + reg(S_MEMORY,Symbols.STAR_MEMORY); + reg(S_BALANCE,Symbols.STAR_BALANCE); + reg(S_ORIGIN,Symbols.STAR_ORIGIN); + reg(S_RESULT,Symbols.STAR_RESULT); + reg(S_TIMESTAMP,Symbols.STAR_TIMESTAMP); + reg(S_DEPTH,Symbols.STAR_DEPTH); + reg(S_OFFER,Symbols.STAR_OFFER); + reg(S_STATE,Symbols.STAR_STATE); + reg(S_HOLDINGS,Symbols.STAR_HOLDINGS); + reg(S_SEQUENCE,Symbols.STAR_SEQUENCE); + reg(S_KEY,Symbols.STAR_KEY); + } + + private static byte reg(byte opCode, Symbol sym) { + int i=opCode-BASE; + symbols[i]=sym; + Special special=new Special<>(opCode); + specials[i]=special; + opcodes.put(sym,Integer.valueOf(opCode)); + return opCode; + } + + + private Special(byte opCode) { + this.opCode=opCode; + } + + + /** + * Creates special Op for the given opCode + * @param opCode Special opcode + * @return Special instance, or null if not found + */ + public static final Special create(int opCode) { + if ((opCodeLIMIT)) return null; + return specials[opCode-BASE]; + } + + @SuppressWarnings("unchecked") + @Override + public Context execute(Context context) { + Context ctx=(Context) context; + switch (opCode) { + case S_JUICE: ctx= ctx.withResult(CVMLong.create(ctx.getJuice())); break; + case S_CALLER: ctx= ctx.withResult(ctx.getCaller()); break; + case S_ADDRESS: ctx= ctx.withResult(ctx.getAddress()); break; + case S_MEMORY: ctx= ctx.withResult(CVMLong.create(ctx.getAccountStatus().getMemory())); break; + case S_BALANCE: ctx= ctx.withResult(CVMLong.create(ctx.getBalance())); break; + case S_ORIGIN: ctx= ctx.withResult(ctx.getOrigin()); break; + case S_RESULT: break; // unchanged context with current result + case S_TIMESTAMP: ctx= ctx.withResult(ctx.getState().getTimeStamp()); break; + case S_DEPTH: ctx= ctx.withResult(CVMLong.create(ctx.getDepth()-1)); break; // Depth before executing this Op + case S_OFFER: ctx= ctx.withResult(CVMLong.create(ctx.getOffer())); break; + case S_STATE: ctx= ctx.withResult(ctx.getState()); break; + case S_HOLDINGS: ctx= ctx.withResult(ctx.getHoldings()); break; + case S_SEQUENCE: ctx= ctx.withResult(CVMLong.create(ctx.getAccountStatus().getSequence())); break; + case S_KEY: ctx= ctx.withResult(ctx.getAccountStatus().getAccountKey()); break; + default: + throw new Error("Bad Opcode"+opCode); + } + return ctx.consumeJuice(Juice.SPECIAL); + } + + @Override + public byte opCode() { + return opCode; + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + // No data + return pos; + } + + @Override + public Special updateRefs(IRefFunction func) { + return this; + } + + @Override + public void validateCell() throws InvalidDataException { + if ((opCode=LIMIT)) { + throw new InvalidDataException("Invalid Special opCode "+opCode, this); + } + } + + @Override + public int getRefCount() { + return 0; + } + + @Override + public void print(StringBuilder sb) { + symbols[opCode-BASE].print(sb); + } + + /** + * Gets the special Op for a given Symbol, or null if not found + * @param Result type + * @param sym Symbol to look up + * @return Special Op or null + */ + @SuppressWarnings("unchecked") + public static Special forSymbol(Symbol sym) { + Integer special=opcodes.get(sym); + if (special==null) return null; + return (Special) specials[special-BASE]; + } +} diff --git a/convex-core/src/main/java/convex/core/lang/reader/AntlrReader.java b/convex-core/src/main/java/convex/core/lang/reader/AntlrReader.java new file mode 100644 index 000000000..4679a1c36 --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/reader/AntlrReader.java @@ -0,0 +1,477 @@ +package convex.core.lang.reader; + +import java.io.IOException; +import java.util.ArrayList; + +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.ErrorNode; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.ParseTreeWalker; +import org.antlr.v4.runtime.tree.TerminalNode; + +import convex.core.data.ACell; +import convex.core.data.AHashMap; +import convex.core.data.AList; +import convex.core.data.Address; +import convex.core.data.Blob; +import convex.core.data.Keyword; +import convex.core.data.Lists; +import convex.core.data.Maps; +import convex.core.data.Sets; +import convex.core.data.Strings; +import convex.core.data.Symbol; +import convex.core.data.Syntax; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMBool; +import convex.core.data.prim.CVMChar; +import convex.core.data.prim.CVMDouble; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.ParseException; +import convex.core.lang.RT; +import convex.core.lang.Symbols; +import convex.core.lang.reader.antlr.ConvexLexer; +import convex.core.lang.reader.antlr.ConvexListener; +import convex.core.lang.reader.antlr.ConvexParser; +import convex.core.lang.reader.antlr.ConvexParser.AddressContext; +import convex.core.lang.reader.antlr.ConvexParser.BlobContext; +import convex.core.lang.reader.antlr.ConvexParser.BoolContext; +import convex.core.lang.reader.antlr.ConvexParser.CharacterContext; +import convex.core.lang.reader.antlr.ConvexParser.CommentedContext; +import convex.core.lang.reader.antlr.ConvexParser.DataStructureContext; +import convex.core.lang.reader.antlr.ConvexParser.DoubleValueContext; +import convex.core.lang.reader.antlr.ConvexParser.FormContext; +import convex.core.lang.reader.antlr.ConvexParser.FormsContext; +import convex.core.lang.reader.antlr.ConvexParser.KeywordContext; +import convex.core.lang.reader.antlr.ConvexParser.ListContext; +import convex.core.lang.reader.antlr.ConvexParser.LiteralContext; +import convex.core.lang.reader.antlr.ConvexParser.LongValueContext; +import convex.core.lang.reader.antlr.ConvexParser.MapContext; +import convex.core.lang.reader.antlr.ConvexParser.NilContext; +import convex.core.lang.reader.antlr.ConvexParser.PathSymbolContext; +import convex.core.lang.reader.antlr.ConvexParser.QuotedContext; +import convex.core.lang.reader.antlr.ConvexParser.SetContext; +import convex.core.lang.reader.antlr.ConvexParser.SingleFormContext; +import convex.core.lang.reader.antlr.ConvexParser.SpecialLiteralContext; +import convex.core.lang.reader.antlr.ConvexParser.StringContext; +import convex.core.lang.reader.antlr.ConvexParser.SymbolContext; +import convex.core.lang.reader.antlr.ConvexParser.SyntaxContext; +import convex.core.lang.reader.antlr.ConvexParser.VectorContext; +import convex.core.util.Utils; + +public class AntlrReader { + + public static class CRListener implements ConvexListener { + ArrayList> stack=new ArrayList<>(); + + public CRListener() { + stack.add(new ArrayList<>()); + } + + public void push(ACell a) { + int n=stack.size()-1; + ArrayList top=stack.get(n); + top.add(a); + } + + public ACell pop() { + int n=stack.size()-1; + ArrayList top=stack.get(n); + int c=top.size()-1; + ACell cell=top.get(c); + top.remove(c); + return cell; + } + + + private void pushList() { + stack.add(new ArrayList<>()); + } + + public ArrayList popList() { + int n=stack.size()-1; + ArrayList top=stack.get(n); + stack.remove(n); + return top; + } + + @Override + public void visitTerminal(TerminalNode node) { + // Nothing to do + } + + @Override + public void visitErrorNode(ErrorNode node) { + throw new ParseException(node.getSourceInterval()+" "+node.getText()); + } + + @Override + public void enterEveryRule(ParserRuleContext ctx) { + // Nothing to do + } + + @Override + public void exitEveryRule(ParserRuleContext ctx) { + // Nothing to do + } + + @Override + public void enterForm(FormContext ctx) { + // Nothing to do + } + + @Override + public void exitForm(FormContext ctx) { + // Nothing to do + } + + @Override + public void enterForms(FormsContext ctx) { + // We add a new ArrayList to the stack to capture values + pushList(); + } + + @Override + public void exitForms(FormsContext ctx) { + // Nothing to do + } + + @Override + public void enterDataStructure(DataStructureContext ctx) { + // Nothing to do + } + + @Override + public void exitDataStructure(DataStructureContext ctx) { + // Nothing to do + } + + @Override + public void enterList(ListContext ctx) { + // Nothing to do + } + + @Override + public void exitList(ListContext ctx) { + ArrayList elements=popList(); + push(Lists.create(elements)); + } + + @Override + public void enterVector(VectorContext ctx) { + // Nothing to do + } + + @Override + public void exitVector(VectorContext ctx) { + ArrayList elements=popList(); + push(Vectors.create(elements)); + } + + @Override + public void enterSet(SetContext ctx) { + // Nothing to do + } + + @Override + public void exitSet(SetContext ctx) { + ArrayList elements=popList(); + push(Sets.fromCollection(elements)); + } + + @Override + public void enterMap(MapContext ctx) { + // Nothing to do + } + + @Override + public void exitMap(MapContext ctx) { + ArrayList elements=popList(); + if (Utils.isOdd(elements.size())) { + throw new ParseException("Map requires an even number form forms."); + } + push(Maps.create(elements.toArray(new ACell[elements.size()]))); + } + + @Override + public void enterLiteral(LiteralContext ctx) { + // Nothing to do + } + + @Override + public void exitLiteral(LiteralContext ctx) { + // Nothing to do + } + + @Override + public void enterLongValue(LongValueContext ctx) { + // Nothing to do + } + + @Override + public void exitLongValue(LongValueContext ctx) { + String s=ctx.getText(); + // System.out.println(s); + push( CVMLong.parse(s)); + } + + @Override + public void enterDoubleValue(DoubleValueContext ctx) { + // Nothing to do + } + + @Override + public void exitDoubleValue(DoubleValueContext ctx) { + String s=ctx.getText(); + push( CVMDouble.parse(s)); + } + + @Override + public void enterNil(NilContext ctx) { + // Nothing to do + } + + @Override + public void exitNil(NilContext ctx) { + push(null); + } + + @Override + public void enterBool(BoolContext ctx) { + // Nothing to do + } + + @Override + public void exitBool(BoolContext ctx) { + push(CVMBool.parse(ctx.getText())); + } + + @Override + public void enterCharacter(CharacterContext ctx) { + // TODO Auto-generated method stub + + } + + @Override + public void exitCharacter(CharacterContext ctx) { + String s=ctx.getText(); + CVMChar c=CVMChar.parse(s); + if (c==null) throw new ParseException("Bad character literal format: "+s); + push(c); + } + + @Override + public void enterKeyword(KeywordContext ctx) { + // Nothing to do + } + + @Override + public void exitKeyword(KeywordContext ctx) { + String s=ctx.getText(); + Keyword k=Keyword.create(s.substring(1)); + if (k==null) throw new ParseException("Bad keyword format: "+s); + push( k); + } + + @Override + public void enterSymbol(SymbolContext ctx) { + // Nothing to do + } + + @Override + public void exitSymbol(SymbolContext ctx) { + String s=ctx.getText(); + Symbol sym=Symbol.create(s); + if (sym==null) throw new ParseException("Bad keyword format: "+s); + push( sym); + } + + @Override + public void enterAddress(AddressContext ctx) { + // Nothing to do + } + + @Override + public void exitAddress(AddressContext ctx) { + String s=ctx.getText(); + push (Address.parse(s)); + } + + @Override + public void enterSyntax(SyntaxContext ctx) { + // add new list to collect [syntax, value] + pushList(); + } + + + @Override + public void exitSyntax(SyntaxContext ctx) { + ArrayList elements=popList(); + if (elements.size()!=2) throw new ParseException("Metadata requires metadata and annotated form but got:"+ elements); + AHashMap meta=ReaderUtils.interpretMetadata(elements.get(0)); + ACell value=elements.get(1); + push(Syntax.create(value, meta)); + } + + @Override + public void enterBlob(BlobContext ctx) { + // Nothing to do + + } + + @Override + public void exitBlob(BlobContext ctx) { + String s=ctx.getText(); + Blob b=Blob.fromHex(s.substring(2)); + if (b==null) throw new ParseException("Invalid Blob syntax: "+s); + push(b); + } + + @Override + public void enterQuoted(QuotedContext ctx) { + // Nothing to do + } + + @Override + public void exitQuoted(QuotedContext ctx) { + ACell form=pop(); + String qs=ctx.getStart().getText(); + Symbol qsym=ReaderUtils.getQuotingSymbol(qs); + if (qsym==null) throw new ParseException("Invalid quoting reader macro: "+qs); + push(Lists.of(qsym,form)); + } + + @Override + public void enterString(StringContext ctx) { + // Nothing to do + } + + @Override + public void exitString(StringContext ctx) { + String s=ctx.getText(); + int n=s.length(); + s=s.substring(1, n-1); // skip surrounding double quotes + s=ReaderUtils.unescapeString(s); + push(Strings.create(s)); + } + + @Override + public void enterSpecialLiteral(SpecialLiteralContext ctx) { + // Nothing to do + } + + @Override + public void exitSpecialLiteral(SpecialLiteralContext ctx) { + pop(); // pop the symbol + String s=ctx.getText(); + ACell special=ReaderUtils.specialLiteral(s); + if (special==null) throw new ParseException("Invalid special literal: "+s); + push(special); + } + + @Override + public void enterCommented(CommentedContext ctx) { + // make a dummy list, doesn't matter what goes in here + pushList(); + } + + @Override + public void exitCommented(CommentedContext ctx) { + // remove commented form + popList(); + } + + @Override + public void enterPathSymbol(PathSymbolContext ctx) { + // Nothing + } + + @Override + public void exitPathSymbol(PathSymbolContext ctx) { + String matchString=ctx.getText(); + String[] ss=matchString.split("/",-1); // negative limit keeps empty values in cases like `#0//` + int n=ss.length; + if (n<2) { + throw new ParseException("Expected followed by symbol but got: ["+ matchString+"]"); + } + + ACell lookup=(ss[0].startsWith("#"))?Address.parse(ss[0]):Symbol.create(ss[0]);; + if (lookup==null) throw new ParseException("Path must start with Addres or Symbol"); + + for (int i=1; i top=visitor.popList(); + if (top.size()!=1) { + throw new ParseException("Bad parse output: "+top); + } + + return top.get(0); + } + + public static AList readAll(String source) { + return readAll(CharStreams.fromString(source)); + } + + public static AList readAll(CharStream cs) { + ConvexLexer lexer=new ConvexLexer(cs); + lexer.removeErrorListeners(); + CommonTokenStream tokens = new CommonTokenStream(lexer); + ConvexParser parser = new ConvexParser(tokens); + parser.removeErrorListeners(); + ParseTree tree = parser.forms(); + + CRListener visitor=new CRListener(); + ParseTreeWalker.DEFAULT.walk(visitor, tree); + + ArrayList top=visitor.popList(); + return Lists.create(top); + } + +} diff --git a/convex-core/src/main/java/convex/core/lang/reader/ReaderUtils.java b/convex-core/src/main/java/convex/core/lang/reader/ReaderUtils.java new file mode 100644 index 000000000..e5ede368b --- /dev/null +++ b/convex-core/src/main/java/convex/core/lang/reader/ReaderUtils.java @@ -0,0 +1,79 @@ +package convex.core.lang.reader; + +import java.util.HashMap; + +import org.apache.commons.text.StringEscapeUtils; + +import convex.core.data.ACell; +import convex.core.data.AHashMap; +import convex.core.data.AMap; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.Maps; +import convex.core.data.Symbol; +import convex.core.data.Syntax; +import convex.core.data.prim.CVMChar; +import convex.core.data.prim.CVMDouble; +import convex.core.lang.Symbols; + +public class ReaderUtils { + + /** + * Converts a metadata object according to the following rule: - Map -> + * unchanged - Keyword -> {:keyword true} - Any other expression -> {:tag + * expression} + * + * @param metaNode Syntax node containing metadata + * @return Metadata map + */ + @SuppressWarnings("unchecked") + public static AHashMap interpretMetadata(ACell metaNode) { + ACell val = Syntax.unwrapAll(metaNode); + if (val instanceof AMap) return (AHashMap) val; + if (val instanceof Keyword) return Maps.of(val, Boolean.TRUE); + return Maps.of(Keywords.TAG, val); + } + + private static final HashMap specialCharacters=Maps.hashMapOf( + "newline",CVMChar.create('\n'), + "space",CVMChar.create(' '), + "tab",CVMChar.create('\t'), + "formfeed",CVMChar.create('\f'), + "backspace",CVMChar.create('\b'), + "return",CVMChar.create('\r') + ); + + public static CVMChar specialCharacter(String s) { + return specialCharacters.get(s); + } + + private static final HashMap quotingSymbols=Maps.hashMapOf( + "'",Symbols.QUOTE, + "`",Symbols.QUASIQUOTE, + "~",Symbols.UNQUOTE, + "~@",Symbols.UNQUOTE_SPLICING + ); + + public static Symbol getQuotingSymbol(String s) { + return quotingSymbols.get(s); + } + + public static String unescapeString(String s) { + return StringEscapeUtils.unescapeJava(s); + } + + public static String escapeString(String s) { + return StringEscapeUtils.escapeJava(s); + } + + private static final HashMap specialLiterals=Maps.hashMapOf( + "##NaN",CVMDouble.NaN, + "##Inf",CVMDouble.POSITIVE_INFINITY, + "##-Inf",CVMDouble.NEGATIVE_INFINITY + ); + + public static ACell specialLiteral(String s) { + return specialLiterals.get(s); + } + +} diff --git a/convex-core/src/main/java/convex/core/store/AStore.java b/convex-core/src/main/java/convex/core/store/AStore.java new file mode 100644 index 000000000..d30d03364 --- /dev/null +++ b/convex-core/src/main/java/convex/core/store/AStore.java @@ -0,0 +1,115 @@ +package convex.core.store; + +import java.io.IOException; +import java.util.function.Consumer; + +import convex.core.data.ABlob; +import convex.core.data.ACell; +import convex.core.data.Format; +import convex.core.data.Hash; +import convex.core.data.Ref; +import convex.core.exceptions.BadFormatException; + +/** + * Abstract base class for object storage subsystems + * + * "The perfect kind of architecture decision is the one which never has to be + * made" ― Robert C. Martin + * + */ +public abstract class AStore { + + /** + * Stores a @Ref in long term storage as defined by this store implementation. + * + * Will store nested Refs if required. + * + * Does not store embedded values. If it is necessary to persist an embedded value + * deliberately in the store, use storeTopRef(...) instead. + * + * If the persisted Ref represents novelty (i.e. not previously stored) Will + * call the provided noveltyHandler. + * + * @param ref A Ref to the given object. Should be either Direct or STORED at + * minimum to present risk of MissingDataException. + * @param status Status to store at + * @param noveltyHandler Novelty Handler function for Novelty detected. May be null. + * @return The persisted Ref, of status STORED at minimum + */ + public abstract Ref storeRef(Ref ref, int status,Consumer> noveltyHandler); + + /** + * Stores a top level @Ref in long term storage as defined by this store implementation. + * + * Will store nested Refs if required. + * + * Will only store an embedded Ref if it is the top level item. + * + * If the persisted Ref represents novelty (i.e. not previously stored) Will + * call the provided noveltyHandler + * + * @param ref A Ref to the given object. Should be either Direct or STORED at + * minimum to present risk of MissingDataException. + * @param status Status to store at + * @param noveltyHandler Novelty Handler function for Novelty detected. May be null. + * @return The persisted Ref, of status STORED at minimum + */ + public abstract Ref storeTopRef(Ref ref, int status,Consumer> noveltyHandler); + + + /** + * Gets the stored Ref for a given hash value, or null if not found. + * + * If the result is non-null, the Ref will have a status equal to STORED at minimum. + * Calls to Ref.getValue() should therefore never throw MissingDataException. + * + * @param hash A hash value to look up in the persisted store + * @return The stored Ref, or null if the hash value is not persisted + */ + public abstract Ref refForHash(Hash hash); + + /** + * Gets the Root Hash from the Store. Root hash is typically used to store the Peer state + * in situations where the Peer needs to be restored from persistent storage. + * + * @return Root hash value from this store. + * @throws IOException In case of store IO error + */ + public abstract Hash getRootHash() throws IOException; + + /** + * Sets the root hash for this Store + * @param h Root Hash to set + * @throws IOException In case of store IO error + */ + public abstract void setRootHash(Hash h) throws IOException; + + /** + * Closes this store and frees associated resources + */ + public abstract void close(); + + protected final BlobCache blobCache=BlobCache.create(100000); + + /** + * Decodes a Cell from an Encoding. Looks up Cell in cache if available. Otherwise + * equivalent to Format.read(Blob). + * @param encoding Encoding of Cell + * @return Decoded Cell (may be a a null value) + * + * @throws BadFormatException If cell encoding is invalid + */ + public final ACell decode(ABlob encoding) throws BadFormatException { + ACell cached=blobCache.getCell(encoding); + if (cached!=null) return cached; + + ACell decoded=Format.read(encoding.toBlob()); + if (decoded==null) return decoded; // handle null value + + // TODO: can remove this check once happy with all tests + assert(decoded.cachedEncoding()!=null); + blobCache.putCell(decoded); + + return decoded; + } +} diff --git a/convex-core/src/main/java/convex/core/store/BlobCache.java b/convex-core/src/main/java/convex/core/store/BlobCache.java new file mode 100644 index 000000000..3ab5a1717 --- /dev/null +++ b/convex-core/src/main/java/convex/core/store/BlobCache.java @@ -0,0 +1,66 @@ +package convex.core.store; + +import java.lang.ref.SoftReference; + +import convex.core.data.ABlob; +import convex.core.data.ACell; + +/** + * In-memory cache for Blob decoding. Should be used in the context of a specific Store + */ +public final class BlobCache { + + private SoftReference[] cache; + private int size; + + @SuppressWarnings("unchecked") + private BlobCache(int size) { + this.size=size; + this.cache=new SoftReference[size]; + }; + + public static BlobCache create(int size) { + return new BlobCache(size); + } + + int getSize() { + return size; + } + + /** + * Gets the Cached Cell for a given Blob Encoding, or null if not cached. + * @param encoding Encoding of Cell to look up in cache + * @return Cached Cell, or null if not found + */ + public ACell getCell(ABlob encoding) { + int ix=calcIndex(encoding); + SoftReference ref=cache[ix]; + if (ref==null) return null; + ACell cell=ref.get(); + if (cell!=null) { + if (encoding.equals(cell.getEncoding())) { + return cell; + } + return null; // cached value not the same as this encoding + } + cache[ix]=null; + return null; + } + + /** + * Stores a cell in the cache + * @param cell Cell to store + */ + public void putCell(ACell cell) { + int ix=calcIndex(cell.getEncoding()); + cache[ix]=new SoftReference<>(cell); + } + + private int calcIndex(ABlob encoding) { + int hash=Long.hashCode(encoding.toLong()); + int ix=Math.floorMod(hash, size); + return ix; + } + + +} diff --git a/convex-core/src/main/java/convex/core/store/MemoryStore.java b/convex-core/src/main/java/convex/core/store/MemoryStore.java new file mode 100644 index 000000000..2912a2f87 --- /dev/null +++ b/convex-core/src/main/java/convex/core/store/MemoryStore.java @@ -0,0 +1,108 @@ +package convex.core.store; + +import java.io.IOException; +import java.util.HashMap; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import convex.core.data.ACell; +import convex.core.data.Hash; +import convex.core.data.Ref; +import convex.core.util.Utils; + +/** + * Class implementing caching and storage of hashed node data + * + * Persists refs as direct refs, i.e. retains fully in memory + */ +public class MemoryStore extends AStore { + public static final MemoryStore DEFAULT = new MemoryStore(); + + private static final Logger log = LoggerFactory.getLogger(MemoryStore.class.getName()); + + /** + * Storage of persisted Refs for each hash value + */ + private final HashMap> hashRefs = new HashMap>(); + + private Hash rootHash; + + @Override + @SuppressWarnings("unchecked") + public Ref refForHash(Hash hash) { + Ref ref = (Ref) hashRefs.get(hash); + return ref; + } + + @Override + public Ref storeRef(Ref r2, int status, Consumer> noveltyHandler) { + return persistRef(r2,noveltyHandler,status,false); + } + + @Override + public Ref storeTopRef(Ref ref, int status,Consumer> noveltyHandler) { + return persistRef(ref,noveltyHandler,status,true); + } + + @SuppressWarnings("unchecked") + public Ref persistRef(Ref ref, Consumer> noveltyHandler, int requiredStatus, boolean topLevel) { + // Convert to direct Ref. Don't want to store a soft ref! + ref = ref.toDirect(); + + final T o=ref.getValue(); + if (o==null) return (Ref) Ref.NULL_VALUE; + + ACell cell = (ACell) o; + boolean embedded=cell.isEmbedded(); + + Hash hash=null; + if (!embedded) { + // check store for existing ref first. Return this is we have it + hash = ref.getHash(); + Ref existing = refForHash(hash); + if ((existing != null)) { + if (existing.getStatus()>=requiredStatus) return existing; + ref=existing; + } + } + + // need to do recursive persistence + cell = cell.updateRefs(r -> { + return r.persist(noveltyHandler); + }); + + ref=ref.withValue((T)cell); + final ACell oTemp=cell; + + if (topLevel||!embedded) { + // Persist at top level + final Hash fHash = (hash!=null)?hash:ref.getHash(); + if (log.isTraceEnabled()) { + log.trace("Persisting ref 0x"+fHash.toHexString()+" of class "+Utils.getClassName(oTemp)+" with store "+this); + } + + hashRefs.put(fHash, (Ref) ref); + if (noveltyHandler != null) noveltyHandler.accept((Ref) ref); + } + return ref.withMinimumStatus(requiredStatus); + } + + @Override + public Hash getRootHash() throws IOException { + return rootHash; + } + + @Override + public void setRootHash(Hash h) { + rootHash=h; + } + + @Override + public void close() { + hashRefs.clear(); + rootHash=null; + } +} diff --git a/convex-core/src/main/java/convex/core/store/Stores.java b/convex-core/src/main/java/convex/core/store/Stores.java new file mode 100644 index 000000000..d0221268d --- /dev/null +++ b/convex-core/src/main/java/convex/core/store/Stores.java @@ -0,0 +1,70 @@ +package convex.core.store; + +import etch.EtchStore; + +public class Stores { + + // Default store + private static AStore defaultStore=null; + + // Configured global store + private static AStore globalStore=null; + + // Thread local current store, in case servers want different stores + private static final ThreadLocal currentStore = new ThreadLocal<>() { + @Override + protected AStore initialValue() { + return getGlobalStore(); + } + }; + + /** + * Gets the current (thread-local) Store instance. This is initialised to be the + * global store, but can be changed with Stores.setCurrent(...) + * + * @return Store for the current thread + */ + public static AStore current() { + return Stores.currentStore.get(); + } + + /** + * Sets the current thread-local store for this thread + * + * @param store Any AStore instance + */ + public static void setCurrent(AStore store) { + currentStore.set(store); + } + + private synchronized static AStore getDefaultStore() { + if (defaultStore==null) { + defaultStore=EtchStore.createTemp("convex-db");; + } + return defaultStore; + } + + /** + * Gets the global store instance. If not previously set, a default temporary + * store will be created and used as the global store. + * + * @return Current global store + */ + public static AStore getGlobalStore() { + if (globalStore==null) { + globalStore=getDefaultStore(); + } + return globalStore; + } + + /** + * Sets the global store for this JVM. Global store is the store used for + * any new thread. + * + * @param store Store instance to use as global store + */ + public static void setGlobalStore(EtchStore store) { + if (store==null) throw new IllegalArgumentException("Cannot set global store to null)"); + globalStore=store; + } +} diff --git a/convex-core/src/main/java/convex/core/transactions/ATransaction.java b/convex-core/src/main/java/convex/core/transactions/ATransaction.java new file mode 100644 index 000000000..47d1b949e --- /dev/null +++ b/convex-core/src/main/java/convex/core/transactions/ATransaction.java @@ -0,0 +1,118 @@ +package convex.core.transactions; + +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.data.Format; +import convex.core.data.type.AType; +import convex.core.data.type.Transaction; +import convex.core.lang.Context; + +/** + * Abstract base class for immutable transactions + * + * Transactions may modify the on-chain State according to the rules of the + * specific transaction type. When applied to a State, a transaction must + * produce either: a) A valid updated State b) A TransactionException + * + * Any other class of exception should be regarded as a serious failure, + * indicating a code error or system integrity issue. + * + */ +public abstract class ATransaction extends ACell { + protected final Address address; + protected final long sequence; + + protected ATransaction(Address address, long sequence) { + if (address==null) throw new ClassCastException("Null Address for transaction"); + this.address=address; + this.sequence = sequence; + } + + /** + * Writes this transaction to a byte array, including the message tag + */ + @Override + public abstract int encode(byte[] bs, int pos); + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = Format.writeVLCLong(bs,pos, address.longValue()); + pos = Format.writeVLCLong(bs,pos, sequence); + return pos; + } + + @Override + public abstract int estimatedEncodingSize(); + + /** + * Applies the functional effect of this transaction to the current state. + * + * Important points: + *
    + *
  • Assumes all relevant accounting preparation already complete, including juice reservation
  • + *
  • Performs complete state update (including any rollbacks from errors)
  • + *
  • Produces result, which may be exceptional
  • + *
  • Does not finalise memory/juice accounting (will be completed afterwards)
  • + *
+ * + * @param ctx Context for which to apply this Transaction + * @return The updated chain state + */ + public abstract Context apply(Context ctx); + + /** + * Gets the *origin* Address for this transaction + * @return Address for this Transaction + */ + public Address getAddress() { + return address; + } + + public final long getSequence() { + return sequence; + } + + + + /** + * Gets the max juice allowed for this transaction + * + * @return Juice limit + */ + public abstract Long getMaxJuice(); + + @Override public final boolean isCVMValue() { + // Transactions exist outside CVM only + return false; + } + + @Override + public AType getType() { + return Transaction.INSTANCE; + } + + /** + * Updates this transaction with the specified sequence number + * @param newSequence New sequence number + * @return Updated transaction, or this transaction if the sequence number is unchanged. + */ + public abstract ATransaction withSequence(long newSequence); + + /** + * Updates this transaction with the specified address + * @param newAddress New address + * @return Updated transaction, or this transaction if unchanged. + */ + public abstract ATransaction withAddress(Address newAddress); + + @Override + public boolean isCanonical() { + return true; + } + + @Override + public ACell toCanonical() { + return this; + } + +} diff --git a/convex-core/src/main/java/convex/core/transactions/Call.java b/convex-core/src/main/java/convex/core/transactions/Call.java new file mode 100644 index 000000000..a1453c847 --- /dev/null +++ b/convex-core/src/main/java/convex/core/transactions/Call.java @@ -0,0 +1,144 @@ +package convex.core.transactions; + +import java.nio.ByteBuffer; + +import convex.core.Constants; +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.Address; +import convex.core.data.Format; +import convex.core.data.IRefFunction; +import convex.core.data.Ref; +import convex.core.data.Symbol; +import convex.core.data.Tag; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.Context; +import convex.core.util.Utils; + +/** + * Transaction representing a Call to an Actor. + * + * The signer of the transaction will be both the *origin* and *caller* for the Actor code. + * + * This is the most efficient way to execute Actor code directly as a client, and is roughly equivalent to invoking + * (call actor offer (function-name arg1 arg2 .....)) + */ +public class Call extends ATransaction { + + protected final Address target; + protected final long offer; + protected final Symbol functionName; + protected final AVector args; + + + + protected Call(Address address, long sequence, Address target, long offer,Symbol functionName,AVector args) { + super(address,sequence); + this.target=target; + this.functionName=functionName; + this.offer=offer; + this.args=args; + } + + public static Call create(Address address, long sequence, Address target, long offer,Symbol functionName,AVector args) { + return new Call(address,sequence,target,0,functionName,args); + } + + + public static Call create(Address address, long sequence, Address target, Symbol functionName,AVector args) { + return create(address,sequence,target,0,functionName,args); + } + + @Override + public void print(StringBuilder sb) { + sb.append("{"); + sb.append(":target "); + Utils.print(sb, target); + if (offer>0) { + sb.append(" :offer "); + sb.append(offer); + } + sb.append('}'); + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++] = Tag.CALL; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = super.encodeRaw(bs,pos); // sequence + pos = Format.write(bs,pos, target); + pos=Format.writeVLCLong(bs,pos, offer); + pos=Format.write(bs,pos, functionName); + pos=Format.write(bs,pos, args); + return pos; + } + + public static ATransaction read(ByteBuffer bb) throws BadFormatException { + Address address=Address.create(Format.readVLCLong(bb)); + long sequence = Format.readVLCLong(bb); + Address target=Format.read(bb); + long offer = Format.readVLCLong(bb); + Symbol functionName=Format.read(bb); + AVector args = Format.read(bb); + return create(address,sequence, target, offer, functionName,args); + } + + @Override + public int estimatedEncodingSize() { + return 100; + } + + @Override + public Context apply(Context ctx) { + return ctx.actorCall(target, offer, functionName, args.toCellArray()); + } + + @Override + public Long getMaxJuice() { + return Constants.MAX_TRANSACTION_JUICE; + } + + @Override + public void validateCell() throws InvalidDataException { + target.validateCell(); + } + + @Override + public int getRefCount() { + return args.getRefCount(); + } + + @Override + public Ref getRef(int i) { + return args.getRef(i); + } + + @Override + public ACell updateRefs(IRefFunction func) { + AVector newArgs=args.updateRefs(func); + if (args==newArgs) return this; + return new Call(address,sequence,target,offer,functionName,newArgs); + } + + @Override + public Call withSequence(long newSequence) { + if (newSequence==this.sequence) return this; + return create(address,newSequence,target,offer,functionName,args); + } + + @Override + public Call withAddress(Address newAddress) { + if (newAddress==this.address) return this; + return create(newAddress,sequence,target,offer,functionName,args); + } + + @Override + public byte getTag() { + return Tag.CALL; + } +} diff --git a/convex-core/src/main/java/convex/core/transactions/Invoke.java b/convex-core/src/main/java/convex/core/transactions/Invoke.java new file mode 100644 index 000000000..3538af418 --- /dev/null +++ b/convex-core/src/main/java/convex/core/transactions/Invoke.java @@ -0,0 +1,173 @@ +package convex.core.transactions; + +import java.nio.ByteBuffer; + +import convex.core.Constants; +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.data.Format; +import convex.core.data.IRefFunction; +import convex.core.data.Ref; +import convex.core.data.Tag; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.Reader; +import convex.core.util.Utils; + +/** + * Transaction class representing the Invoke of an on-chain operation. + * + * The command provided may be specified as either: + *
    + *
  • A Form (will be compiled and executed)
  • + *
  • A pre-compiled Op (will be executed directly, cheaper)
  • + *
+ * + * Peers may separately implement functionality to parse and compile a command provided as a String: this must be + * performed outside the CVM which not provide a parser internally. + */ +public class Invoke extends ATransaction { + protected final ACell command; + + protected Invoke(Address address,long sequence, ACell args) { + super(address,sequence); + this.command = args; + } + + public static Invoke create(Address address,long sequence, ACell command) { + return new Invoke(address,sequence, command); + } + + /** + * Creates an Invoke transaction + * @param address Address of origin Account + * @param sequence Sequence number + * @param command Command as a string, which will be read as Convex Lisp code + * @return New Invoke transaction instance + */ + public static Invoke create(Address address,long sequence, String command) { + return create(address,sequence, Reader.read(command)); + } + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++] = Tag.INVOKE; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = super.encodeRaw(bs,pos); // nonce, address + pos = Format.write(bs,pos, command); + return pos; + } + + /** + * Get the command for this transaction, as code. + * @return Command object. + */ + public Object getCommand() { + return command; + } + + /** + * Read a Transfer transaction from a ByteBuffer + * + * @param bb ByteBuffer containing the transaction + * @throws BadFormatException if the data is invalid + * @return The Transfer object + */ + public static Invoke read(ByteBuffer bb) throws BadFormatException { + Address address=Address.create(Format.readVLCLong(bb)); + long sequence = Format.readVLCLong(bb); + + ACell args = Format.read(bb); + return create(address,sequence, args); + } + + @SuppressWarnings("unchecked") + @Override + public Context apply(final Context context) { + Context ctx=(Context) context; + + // Run command + if (command instanceof AOp) { + ctx = ctx.run((AOp) command); + } else { + ctx = ctx.run(command); + } + return (Context) ctx; + } + + @Override + public int estimatedEncodingSize() { + // tag (1), nonce(<12) and target (33) + // plus allowance for Amount + return 1 + 12 + Format.MAX_EMBEDDED_LENGTH + Format.MAX_VLC_LONG_LENGTH; + } + + @Override + public boolean isCanonical() { + return true; + } + + @Override + public void print(StringBuilder sb) { + sb.append("{"); + sb.append(":invoke "); + Utils.print(sb, command); + sb.append('}'); + } + + @Override + public void validateCell() throws InvalidDataException { + if (command instanceof AOp) { + // OK? + ((AOp) command).validateCell(); + } else { + if (!Format.isCanonical(command)) throw new InvalidDataException("Non-canonical object as command?", this); + } + } + + @Override + public Long getMaxJuice() { + // TODO make this a field + return Constants.MAX_TRANSACTION_JUICE; + } + + @Override + public int getRefCount() { + return Utils.refCount(command); + } + + @Override + public Ref getRef(int i) { + return Utils.getRef(command, i); + } + + @Override + public Invoke updateRefs(IRefFunction func) { + ACell newCommand = Utils.updateRefs(command, func); + if (newCommand == command) return this; + return Invoke.create(address,getSequence(), newCommand); + } + + @Override + public Invoke withSequence(long newSequence) { + if (newSequence==this.sequence) return this; + return create(address,newSequence,command); + } + + @Override + public Invoke withAddress(Address newAddress) { + if (newAddress==this.address) return this; + return create(newAddress,sequence,command); + } + + @Override + public byte getTag() { + return Tag.INVOKE; + } +} diff --git a/convex-core/src/main/java/convex/core/transactions/Transfer.java b/convex-core/src/main/java/convex/core/transactions/Transfer.java new file mode 100644 index 000000000..74c9e9c78 --- /dev/null +++ b/convex-core/src/main/java/convex/core/transactions/Transfer.java @@ -0,0 +1,147 @@ +package convex.core.transactions; + +import java.nio.ByteBuffer; + +import convex.core.Constants; +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.data.Format; +import convex.core.data.Tag; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.Context; +import convex.core.lang.Juice; +import convex.core.lang.RT; + +/** + * Transaction class representing a coin Transfer from one account to another + */ +public class Transfer extends ATransaction { + public static final long TRANSFER_JUICE = Juice.TRANSFER; + + protected final Address target; + protected final long amount; + + protected Transfer(Address address,long nonce, Address target, long amount) { + super(address,nonce); + this.target = target; + this.amount = amount; + } + + public static Transfer create(Address address,long nonce, Address target, long amount) { + return new Transfer(address,nonce, target, amount); + } + + + @Override + public int encode(byte[] bs, int pos) { + bs[pos++]=Tag.TRANSFER; + return encodeRaw(bs,pos); + } + + @Override + public int encodeRaw(byte[] bs, int pos) { + pos = super.encodeRaw(bs,pos); // nonce, address + pos = target.encodeRaw(bs,pos); + pos = Format.writeVLCLong(bs, pos, amount); + return pos; + } + + /** + * Read a Transfer transaction from a ByteBuffer + * + * @param bb ByteBuffer containing the transaction + * @throws BadFormatException if the data is invalid + * @return The Transfer object + */ + public static Transfer read(ByteBuffer bb) throws BadFormatException { + Address address=Address.create(Format.readVLCLong(bb)); + long nonce = Format.readVLCLong(bb); + Address target = Address.readRaw(bb); + long amount = Format.readVLCLong(bb); + if (!RT.isValidAmount(amount)) throw new BadFormatException("Invalid amount: "+amount); + return create(address,nonce, target, amount); + } + + @SuppressWarnings("unchecked") + @Override + public Context apply(Context ctx) { + // consume juice, ensure we have enough to make transfer! + ctx = ctx.consumeJuice(Juice.TRANSFER); + if (!ctx.isExceptional()) { + ctx = ctx.transfer(target, amount); + } + return (Context) ctx; + } + + @Override + public int estimatedEncodingSize() { + // tag (1), nonce(<12) and target (33) + // plus allowance for Amount + return 1 + 12 + 33 + Format.MAX_VLC_LONG_LENGTH; + } + + @Override + public boolean isCanonical() { + return true; + } + + @Override + public void print(StringBuilder sb) { + sb.append("{"); + sb.append(":transfer-to "); + target.print(sb); + sb.append(','); + sb.append(":amount "+amount); + sb.append('}'); + } + + @Override + public void validateCell() throws InvalidDataException { + if ((amount<0)||(amount>Constants.MAX_SUPPLY)) throw new InvalidDataException("Invalid amount", this); + if (target == null) throw new InvalidDataException("Null Address", this); + } + + /** + * Gets the target address for this transfer + * @return Address of the destination for this transfer. + */ + public Address getTarget() { + return target; + } + + /** + * Gets the transfer amount for this transaction. + * @return Amount of transfer, as a long + */ + public long getAmount() { + return amount; + } + + @Override + public Long getMaxJuice() { + return Juice.TRANSFER; + } + + @Override + public int getRefCount() { + return 0; + } + + @Override + public Transfer withSequence(long newSequence) { + if (newSequence==this.sequence) return this; + return create(address,newSequence,target,amount); + } + + @Override + public Transfer withAddress(Address newAddress) { + if (newAddress==this.address) return this; + return create(newAddress,sequence,target,amount); + } + + @Override + public byte getTag() { + return Tag.TRANSFER; + } +} diff --git a/convex-core/src/main/java/convex/core/util/Bits.java b/convex-core/src/main/java/convex/core/util/Bits.java new file mode 100644 index 000000000..b485580f1 --- /dev/null +++ b/convex-core/src/main/java/convex/core/util/Bits.java @@ -0,0 +1,106 @@ +package convex.core.util; + +/** + * Static utility function for bitwise functions + */ +public class Bits { + + /** + * Returns the index from the present mask for the given hex digit (0-15), or -1 + * if not found + * + * @param digit Hex digit (0-15) + * @param mask Bitmask of hex digits + * @return The index of the appropriate child for this digit, or -1 if not found + */ + public static int indexForDigit(int digit, short mask) { + // check if digit is present in mask + if ((mask & (1 << digit)) == 0) return -1; + + // get the position of this digit (which must be present due to previous check) + return positionForDigit(digit, mask); + } + + /** + * Returns the array position for a given digit given a current mask. If not + * present, this is where the new array entry must be inserted. + * + * @param digit Hex digit (0-15) + * @param mask Bitmask of hex digits + * @return Array position for the given digit in the specified mask + */ + public static int positionForDigit(int digit, short mask) { + // count present bits before this digit + return Integer.bitCount(mask & ((1 << digit) - 1)); + } + + /** + * Get the number of leading zeros in the binary representation of an int + * @param x int value to check + * @return Number of leading zeros (0-32) + */ + public static int leadingZeros(int x) { + if (x == 0) return 32; + int result = 0; + if ((x & 0xFFFF0000) == 0) { + result += 16; + } else { + x >>>= 16; + } + if ((x & 0xFF00) == 0) { + result += 8; + } else { + x >>>= 8; + } + if ((x & 0xF0) == 0) { + result += 4; + } else { + x >>>= 4; + } + if ((x & 0xC) == 0) { + result += 2; + } else { + x >>>= 2; + } + if ((x & 0x2) == 0) { + result += 1; + } else { + x >>>= 1; + } + return result; + } + + /** + * Get the number of leading zeros in the binary representation of a long + * @param x long value to check + * @return Number of leading zeros (0-64) + */ + public static int leadingZeros(long x) { + int highWord = (int) (x >>> 32); // high 4 bytes, unsigned + if (highWord != 0) return leadingZeros(highWord); + int lowWord = (int) (x); + return 32 + leadingZeros(lowWord); + } + + /** + * Gets a bit mask for the specified number of low bits in an int + * + * @param numBits Number of bits to set to 1 + * @return int containing the specified number of set low bits + */ + public static int lowBitMask(int numBits) { + return (1 << numBits) - 1; + } + + /** + * Gets the specified number of low Bits in an integer. Other bits are zeroed. + * + * @param numBits Number of bits to get + * @param val Value to extract bits from + * @return Masked in with the specified number of low bits + */ + public static int lowBits(int numBits, int val) { + return val & lowBitMask(numBits); + } + +} diff --git a/convex-core/src/main/java/convex/core/util/Counters.java b/convex-core/src/main/java/convex/core/util/Counters.java new file mode 100644 index 000000000..e621e08de --- /dev/null +++ b/convex-core/src/main/java/convex/core/util/Counters.java @@ -0,0 +1,25 @@ +package convex.core.util; + +/** + * Some event counters, for debugging and general metrics + */ +public class Counters { + + public static volatile long sendCount = 0; + public static volatile long beliefMerge = 0; + public static volatile long applyBlock = 0; + + public static volatile long etchRead = 0; + public static volatile long etchWrite = 0; + public static volatile long etchMiss =0; + + public String getStats() { + StringBuffer sb=new StringBuffer(); + + sb.append("Etch writes: "+etchWrite); + sb.append("Etch reads: "+etchRead); + sb.append("Etch hit(%): "+Text.toPercentString(100.0*(etchRead-etchMiss)/etchRead)); + + return sb.toString(); + } +} diff --git a/convex-core/src/main/java/convex/core/util/Economics.java b/convex-core/src/main/java/convex/core/util/Economics.java new file mode 100644 index 000000000..7324c4065 --- /dev/null +++ b/convex-core/src/main/java/convex/core/util/Economics.java @@ -0,0 +1,53 @@ +package convex.core.util; + +import java.math.BigInteger; + +/** + * Utility function for Convex Cryptoeconomics + */ +public class Economics { + + /** + * Computes the marginal exchange rate between assets A and B with pool quantities, + * such that a constant liquidity pool c = a * b is maintained. + * + * @param a Quantity of Asset A + * @param b Quantity of Asset B + * @return Price of A in terms of B + */ + public static double swapRate(long a, long b) { + if ((a<=0)||(b<=0)) throw new IllegalArgumentException("Pool quantities must be positive"); + return (double)b/(double)a; + } + + static final BigInteger MAX_POOL_SIZE=BigInteger.valueOf(Long.MAX_VALUE); + + + /** + * Computes the smallest price for d units of Asset A in terms of units of Asset B + * such that a constant liquidity pool c = a * b is increased + * + * @param a Quantity of Asset A in Pool + * @param b Quantity of Asset B in Pool + * @param delta Quantity of Unit A to buy (negative = sell) + * @return Price of A in terms of B + */ + public static long swapPrice(long delta,long a, long b) { + if ((a<=0)||(b<=0)) throw new IllegalArgumentException("Pool quantities must be positive"); + + BigInteger c = BigInteger.valueOf(a).multiply(BigInteger.valueOf(b)); + long newA = a-delta; + if (newA<=0) throw new IllegalArgumentException("Cannot buy entire Pool"); + + BigInteger newBigA=BigInteger.valueOf(newA); + if (newBigA.compareTo(MAX_POOL_SIZE)>=0) throw new IllegalArgumentException("Can't exceed Long pool size for A"); + + BigInteger newBigB = c.divide(newBigA); + if (newBigB.compareTo(MAX_POOL_SIZE)>=0) throw new IllegalArgumentException("Can't exceed Long pool size for B"); + + // Convert back to long, add one so pool size must strictly increase + long finalB=newBigB.longValueExact()+1; + + return finalB-b; + } +} diff --git a/convex-core/src/main/java/convex/core/util/Errors.java b/convex-core/src/main/java/convex/core/util/Errors.java new file mode 100644 index 000000000..8e2013770 --- /dev/null +++ b/convex-core/src/main/java/convex/core/util/Errors.java @@ -0,0 +1,54 @@ +package convex.core.util; + +import convex.core.data.ARecord; +import convex.core.data.Address; +import convex.core.data.Keyword; + +/** + * Utility class for generating appropriate error messages + * + * "I keep a list of all unresolved bugs I've seen on the forum. In some cases, + * I'm still thinking about the best design for the fix. This isn't the kind of + * software where we can leave so many unresolved bugs that we need a tracker for them." + * + * – Satoshi Nakamoto + */ +public class Errors { + + public static String immutable(Object a) { + return "Object is immutable: "+a.getClass(); + } + + public static String sizeOutOfRange(long i) { + return "Index out of range: "+i; + } + + public static String illegalPosition(long position) { + return "Illegal index position: "+position; + } + + public static String insufficientFunds(Address source, long amount) { + return "Insufficient funds in account ["+source+"] required="+amount; + } + + public static String unknownKey(Keyword key, ARecord record) { + return "Unknown key ["+key+"] for record type: "+record.getClass(); + } + + public static String badIndex(long i) { + return "Bad index: "+i; + } + + public static String badRange(long start, long length) { + return "Range out of bounds with offset="+start+" and length="+length; + } + + public static String negativeLength(long length) { + return "Negative length: "+length; + } + + public static String wrongLength(long expected, long count) { + return "Wrong length, expected="+expected+" and actual="+count; + } + +} diff --git a/convex-core/src/main/java/convex/core/util/Huge.java b/convex-core/src/main/java/convex/core/util/Huge.java new file mode 100644 index 000000000..ed23c1bf8 --- /dev/null +++ b/convex-core/src/main/java/convex/core/util/Huge.java @@ -0,0 +1,137 @@ +package convex.core.util; + +import convex.core.exceptions.TODOException; + +/** + * A 128-bit integer + */ +public class Huge { + + // Some useful constants + public static final Huge ZERO = create(0L); + public static final Huge ONE = create(1L); + + public final long hi; + public final long lo; + + private Huge(long hi,long lo) { + this.hi=hi; + this.lo=lo; + + } + + /** + * Creates a new Huge by sign extending a long to 128 bits + * @param a Any signed 64-bit long value + * @return New Huge instance + */ + public static Huge create(long a) { + return new Huge((a>=0)?0:-1,a); + } + + /** + * Creates a new Huge by multiplying two signed longs + * @param a Any signed 64-bit long value + * @param b Any signed 64-bit long value + * @return Huge product of arguments + */ + public static Huge multiply(long a, long b) { + long hi=Math.multiplyHigh(a, b); + return new Huge(hi,a*b); + } + + /** + * Creates a new Huge by multiplying a Huge with a signed long + * @param a Any signed 128-bit Huge value + * @param b Any signed 64-bit long value + * @return Huge product of arguments + */ + public static Huge multiply(Huge a, long b) { + long carry=Math.multiplyHigh(a.lo, b); + return new Huge(carry+a.hi*b,a.lo*b); + } + + /** + * Creates a Huge by adding two signed longs + * @param a Any signed 64-bit long value + * @param b Any signed 64-bit long value + * @return Huge sum of arguments + */ + public static Huge add(long a, long b) { + long carry = UMath.unsignedAddCarry(a,b); + long signSum = ((a<0)?-1:0) + ((b<0)?-1:0); + return new Huge(carry+signSum,a+b); + } + + /** + * Creates a Huge by adding a long value to this Huge + * @param b Any signed 64-bit long value + * @return Huge sum of arguments + */ + public Huge add(long b) { + long carry = UMath.unsignedAddCarry(lo,b); + long sign = ((b<0)?-1:0); + + return new Huge(hi+sign+carry,lo+b); + } + + /** + * Performs a fused multiply and divide (a * b) / c. Handles cases where (a*b) would overflow a single 64-bit long. + * + * @param a First multiplicand + * @param b Second multiplicand + * @param c Divisor + * @return Result of operation, of null if result overflows a Long + */ + public static Long fusedMultiplyDivide(long a, long b, long c) { + throw new TODOException(); + } + + /** + * Creates a Huge by adding another Huge + * @param b Any Huge value + * @return Huge sum of arguments + */ + public Huge add(Huge b) { + long carry = UMath.unsignedAddCarry(lo,b.lo); + return new Huge(hi+b.hi+carry,lo+b.lo); + } + + @Override + public boolean equals(Object a) { + if (a instanceof Huge) return equals((Huge)a); + return false; + } + + /** + * Tests if this Huge is equal to another Huge + * @param a Another Huge instance (must not be null) + * @return true is the Huge values are equal, false otherwise + */ + public boolean equals(Huge a) { + return (lo==a.lo)&&(hi==a.hi); + } + + @Override + public String toString() { + return "#huge 0x"+Utils.toHexString(hi)+Utils.toHexString(lo); + } + + public Huge sub(Huge b) { + return add(b.negate()); + } + + /** + * Negates this Huge value + * @return Huge negation of this value + */ + public Huge negate() { + return new Huge(-hi-((lo!=0L)?1L:0L),-lo); + } + + public Huge mul(Huge b) { + throw new TODOException(); + // Broken because of carrying + // return new Huge((hi*b.lo) + (lo*b.hi) + UMath.multiplyHigh(lo, b.lo),lo*b.lo); + } +} diff --git a/convex-core/src/main/java/convex/core/util/MergeFunction.java b/convex-core/src/main/java/convex/core/util/MergeFunction.java new file mode 100644 index 000000000..3e4777ec1 --- /dev/null +++ b/convex-core/src/main/java/convex/core/util/MergeFunction.java @@ -0,0 +1,16 @@ +package convex.core.util; + +public abstract interface MergeFunction { + + public abstract V merge(V a, V b); + + /** + * Reverse a MergeFunction so that it can be applied with opposite ordering. + * This is useful for handling merge functions that are not commutative. + * + * @return A MergeFunction that merges the arguments in the reverse order. + */ + public default MergeFunction reverse() { + return (a, b) -> merge(b, a); + } +} diff --git a/convex-core/src/main/java/convex/core/util/Shutdown.java b/convex-core/src/main/java/convex/core/util/Shutdown.java new file mode 100644 index 000000000..9117d90d6 --- /dev/null +++ b/convex-core/src/main/java/convex/core/util/Shutdown.java @@ -0,0 +1,77 @@ +package convex.core.util; + +import java.util.Collection; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.TreeMap; + +/** + * So the JVM doesn't give us a nice way to run shutdown hooks in a defined order. + * + * This class enables us to do just that! + */ +public class Shutdown { + + public static final int CLIENTHTTP = 60; + public static final int SERVER = 80; + public static final int ETCH = 100; + public static final int CLI = 120; + + static { + try { + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + Shutdown.runHooks(); + } + })); + } catch(IllegalStateException e) { + // Ignore, already shutting down + } + } + + private static class Group { + private final IdentityHashMap hookSet=new IdentityHashMap<>(); + + public synchronized void addHook(Runnable r) { + hookSet.put(r, r); + } + + public synchronized void runHooks() { + Collection hooks=hookSet.keySet(); + hooks.stream().forEach(r->{ + r.run(); + }); + hookSet.clear(); + } + + } + + private static final TreeMap order=new TreeMap<>(); + + /** + * Add a Runnable shutdown hook with the given priority. Lower priority numbers will + * be executed first. + * + * @param priority Priority number for shutdown hook + * @param shutdownTask Runnable instance to execute on shutdown + */ + public static synchronized void addHook(int priority,Runnable shutdownTask) { + Group g=order.get(priority); + if (g==null) { + g=new Group(); + order.put(priority, g); + } + g.addHook(shutdownTask); + } + + /** + * Execute all hooks. Called by standard Java shutdown process. + */ + private synchronized static void runHooks() { + for (Map.Entry me: order.entrySet()) { + me.getValue().runHooks(); + } + order.clear(); + } +} diff --git a/convex-core/src/main/java/convex/core/util/Text.java b/convex-core/src/main/java/convex/core/util/Text.java new file mode 100644 index 000000000..51a464594 --- /dev/null +++ b/convex-core/src/main/java/convex/core/util/Text.java @@ -0,0 +1,71 @@ +package convex.core.util; + +import java.text.DecimalFormat; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; + +public class Text { + private static final int WHITESPACE_LENGTH = 32; + private static String WHITESPACE_32 = " "; // 32 spaces + + public static String whiteSpace(int length) { + if (length < 0) throw new IllegalArgumentException("Negative whitespace requested!"); + if (length == 0) return ""; + + if (length <= WHITESPACE_LENGTH) { + return WHITESPACE_32.substring(0, length); + } + + StringBuilder sb = new StringBuilder(length); + for (int i = WHITESPACE_LENGTH; i <= length; i += WHITESPACE_LENGTH) { + sb.append(WHITESPACE_32); + } + sb.append(whiteSpace(length & 0x1F)); + return sb.toString(); + } + + public static String leftPad(String s, int length) { + if (s == null) s = ""; + int spaces = length - s.length(); + if (spaces < 0) throw new IllegalArgumentException("String [" + s + "] too long for pad length: " + length); + return whiteSpace(spaces) + s; + } + + public static String leftPad(long value, int length) { + return leftPad(Long.toString(value), length); + } + + public static String rightPad(String s, int length) { + if (s == null) s = ""; + int spaces = length - s.length(); + if (spaces < 0) throw new IllegalArgumentException("String [" + s + "] too long for pad length: " + length); + return s + whiteSpace(spaces); + } + + public static String rightPad(long value, int length) { + return rightPad(Long.toString(value), length); + } + + static DecimalFormat balanceFormatter = new DecimalFormat("#,###"); + + public static String toFriendlyBalance(long value) { + return balanceFormatter.format(value); + } + + static DecimalFormat percentFormatter = new DecimalFormat("##.###%"); + public static String toPercentString(double value) { + return percentFormatter.format(value); + } + + public static String toFriendlyBalance(double value) { + return toFriendlyBalance((long) value); + } + + static final DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendInstant(3).toFormatter(); + + public static String dateFormat(long timestamp) { + return formatter.format(Instant.ofEpochMilli(timestamp)); + } + +} diff --git a/convex-core/src/main/java/convex/core/util/UMath.java b/convex-core/src/main/java/convex/core/util/UMath.java new file mode 100644 index 000000000..2ee7cac58 --- /dev/null +++ b/convex-core/src/main/java/convex/core/util/UMath.java @@ -0,0 +1,35 @@ +package convex.core.util; + +/** + * Functions for unsigned maths. + * + * It would be nice if Java included these by default. + */ +public class UMath { + + /** + * Gets the high 64 bits of an unsigned multiply + * @param a 64-bit long interpreted as unsigned value + * @param b 64-bit long interpreted as unsigned value + * @return High 64 bits of unsigned multiply + */ + public static long multiplyHigh(long a, long b) { + long r=Math.multiplyHigh(a, b); + if ((a<0)^(b<0)) r=-r; + return r; + } + + /** + * Gets the carry of an unsigned addition of two longs + * + * @param a 64-bit long interpreted as unsigned value + * @param b 64-bit long interpreted as unsigned value + * @return 1 if the addition carries, 0 otherwise + */ + public static long unsignedAddCarry(long a, long b) { + boolean sa=(a<0); // high bit of a + boolean sb=(b<0); // high bit of b + return (sa&&sb)||((sa||sb)&&(!((a+b)<0))) ? 1 : 0; + } + +} diff --git a/convex-core/src/main/java/convex/core/util/Utils.java b/convex-core/src/main/java/convex/core/util/Utils.java new file mode 100644 index 000000000..feb987abe --- /dev/null +++ b/convex-core/src/main/java/convex/core/util/Utils.java @@ -0,0 +1,1339 @@ +package convex.core.util; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Array; +import java.math.BigInteger; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import convex.core.Constants; +import convex.core.State; +import convex.core.data.AArrayBlob; +import convex.core.data.ABlob; +import convex.core.data.ACell; +import convex.core.data.AObject; +import convex.core.data.ASequence; +import convex.core.data.AVector; +import convex.core.data.Blob; +import convex.core.data.IRefFunction; +import convex.core.data.Ref; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMChar; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.TODOException; +import convex.core.lang.RT; + +public class Utils { + public static final byte[] EMPTY_BYTES = new byte[0]; + + /** + * Converts an array of bytes into an unsigned BigInteger + * + * Assumes big-endian format as per new BigInteger(int, byte[]); + * + * @param data Array of bytes containing an unsigned integer (big-endian) + * @return A new non-negative BigInteger + */ + public static BigInteger toBigInteger(byte[] data) { + return new BigInteger(1, data); + } + + /** + * Converts an array of bytes into a signed BigInteger + * + * Assumes two's-complement big-endian binary representation format as per new + * BigInteger(byte[]); + * + * @param data Byte array to convert to BigInteger + * @return A signed BigInteger + */ + public static BigInteger toSignedBigInteger(byte[] data) { + return new BigInteger(data); + } + + /** + * Converts an int to a hex string e.g. "80cafe80" + * + * @param val Value to convert + * @return Lowercase hex string + */ + public static String toHexString(int val) { + StringBuffer sb = new StringBuffer(8); + for (int i = 0; i < 8; i++) { + sb.append(Utils.toHexChar((val >> ((7 - i) * 4)) & 0xf)); + } + return sb.toString(); + } + + public static String toHexString(short val) { + StringBuffer sb = new StringBuffer(4); + for (int i = 0; i < 4; i++) { + sb.append(Utils.toHexChar((val >> ((3 - i) * 4)) & 0xf)); + } + return sb.toString(); + } + + /** + * Converts a byte to a two-character hex string + * + * @param value Value to convert + * @return Lowercase hex string + */ + public static String toHexString(byte value) { + StringBuffer sb = new StringBuffer(2); + sb.append(toHexChar((((int) value) & 0xF0) >>> 4)); + sb.append(toHexChar(((int) value) & 0xF)); + return sb.toString(); + } + + /** + * Converts a long value to a 16 character hex string + * + * @param x Value to convert + * @return Hex string for the given long + */ + public static String toHexString(long x) { + StringBuffer sb = new StringBuffer(16); + for (int i = 15; i >= 0; i--) { + sb.append(toHexChar(((int) (x >> (4 * i))) & 0xF)); + } + return sb.toString(); + } + + /** + * Converts a hex string to a friendly version ( first x chars). + * SECURITY; do not use this output for any comparison. + * + * @param hexString String to show in friendly format. + * @param size Number of hex chars to output. + * @return Hex String + */ + public static String toFriendlyHexString(String hexString, int size) { + String cleanHexString = hexString.replaceAll("^0[Xx]", ""); + String result = cleanHexString.substring(0, size); + // + ".." + cleanHexString.substring(cleanHexString.length() - size); + return result; + } + /** + * Reads an int from a specified location in a byte array Assumes 4-byte + * big-endian representation + * + * @param data Byte array from which to read the 4-byte int representation + * @param offset Offset into byte array to read + * @return int value from array + */ + public static int readInt(byte[] data, int offset) { + int result = data[offset]; + for (int i = 1; i <= 3; i++) { + result = (result << 8) + (data[offset + i] & 0xFF); + } + return result; + } + + public static long readLong(byte[] data, int offset) { + long result = data[offset]; + for (int i = 1; i <= 7; i++) { + result = (result << 8) + (data[offset + i] & 0xFF); + } + return result; + } + + /** + * Reads a short from a specified location in a byte array Assumes 2-byte + * big-endian representation + * + * @param data Byte array from which to read the 2-byte short representation + * @param offset Offset into byte array to read + * @return short value from array + */ + public static short readShort(byte[] data, int offset) { + int result = ((data[offset] & 0xFF) << 8) + (data[offset + 1] & 0xFF); + return (short) result; + } + + /** + * Writes an char to a byte array in 2 byte big-endian representation + * + * @param value int value to write to the array + * @param data Byte array into which to write the given int + * @param offset Offset into the array at which the int will be written + * @return Offset after writing + */ + public static int writeChar(byte[] data, int offset,char value) { + data[offset++]=(byte)(value>>8); + data[offset++]=(byte)(value); + return offset; + } + + /** + * Writes an char to a byte array in 2 byte big-endian representation + * + * @param value int value to write to the array + * @param data Byte array into which to write the given int + * @param offset Offset into the array at which the int will be written + * @return Offset after writing + */ + public static int writeShort(byte[] data, int offset,short value) { + data[offset++]=(byte)(value>>8); + data[offset++]=(byte)(value); + return offset; + } + + /** + * Writes an int to a byte array in 4 byte big-endian representation + * + * @param value int value to write to the array + * @param data Byte array into which to write the given int + * @param offset Offset into the array at which the int will be written + * @return Offset after writing + */ + public static int writeInt(byte[] data, int offset,int value) { + for (int i = 0; i <= 3; i++) { + data[offset + i] = (byte) ((value >> (8 * (3 - i))) & 0xFF); + } + return offset+4; + } + + /** + * Writes a long to a byte array in 8 byte big-endian representation. + * + * @param value long value to write to the array + * @param data Byte array into which to write the given long + * @param offset Offset into the array at which the long will be written + * + * @throws IndexOutOfBoundsException If long reaches outside the destination + * byte array + * @return Offset after writing 8 bytes + */ + public static int writeLong(byte[] data, int offset,long value) { + for (int i = 0; i <= 7; i++) { + data[offset + i] = (byte) (value >> (8 * (7 - i))); + } + return offset+8; + } + + /** + * Reads ByteBuffer contents into a new byte array + * + * @param bb ByteBuffer + * @return New byte array + */ + public static byte[] toByteArray(ByteBuffer bb) { + int len = bb.remaining(); + byte[] bytes = new byte[len]; + bb.get(bytes); + return bytes; + } + + /** + * Reads ByteBuffer contents into a new Data object + * + * @param bb ByteBuffer + * @return Blob extracted from ByteBuffer + */ + public static AArrayBlob toData(ByteBuffer bb) { + return Blob.wrap(toByteArray(bb)); + } + + /** + * Converts an int value in the range 0..15 to a hexadecimal character + * + * @param i Value to convert + * @return Hex digit value (lowercase) + */ + public static char toHexChar(int i) { + if (i >= 0) { + if (i <= 9) return (char) (i + 48); + if (i <= 15) return (char) (i + 87); + } + throw new IllegalArgumentException("Unable to convert to single hex char: " + i); + } + + /** + * Converts a hex string to a byte array. Must contain an even number of hex + * digits, or else null will be returned + * + * @param hex String containing Hex digits + * @return byte array with the given hex value, or null if string is not valid + */ + public static byte[] hexToBytes(String hex) { + byte[] bs= hexToBytes(hex, hex.length()); + return bs; + } + + /** + * Converts a hex string to a byte array. Must contain an the expected number of + * hex digits, or else null will be returned + * + * @param hex String containing Hex digits + * @param stringLength number of hex digits in the string to use + * @return byte array with the given hex value, or null if not valud + */ + public static byte[] hexToBytes(String hex, int stringLength) { + if (hex.length() != stringLength) { + return null; + } + int N = stringLength / 2; + if (N * 2 != stringLength) { + return null; + } + byte[] result = new byte[N]; + + for (int i = 0; i < N; i++) { + char high = hex.charAt(2 * i); + char low = hex.charAt(2 * i + 1); + int lowD = Utils.hexVal(low); + if (lowD < 0) return null; + int highD = Utils.hexVal(high); + if (highD < 0) return null; + result[i] = (byte) (highD * 16 + lowD); + } + + return result; + } + + /** + * Converts a hex string to an unsigned big Integer + * + * @param hex Value to convert + * @return BigInteger + */ + public static BigInteger hexToBigInt(String hex) { + return new BigInteger(1, hexToBytes(hex)); + } + + /** + * Gets the value of a single hex car e.g. hexVal('c') => 12 + * + * @param c Character representing a hex digit + * @return int in the range 0..15 inclusive, or -1 if not a hex char + */ + public static int hexVal(char c) { + int v = (int) c; + if (v <= 102) { + if (v >= 97) return v - 87; // lowercase + if ((v >= 65) && (v <= 70)) return v - 55; // uppercase + if ((v >= 48) && (v <= 57)) return v - 48; // digit + } + return -1; + } + + /** + * Converts a byte array of length N to a hex string of length 2N + * + * @param data Array of bytes + * @return Hex String + */ + public static String toHexString(byte[] data) { + return toHexString(data, 0, data.length); + } + + /** + * Converts a slice of a byte array to a hex string of length 2N + * + * @param data Array of bytes + * @param offset Start offset to read from byte array + * @param length Length in bytes to read from byte array + * @return Hex String + */ + public static String toHexString(byte[] data, int offset, int length) { + char[] hexDigits = new char[length * 2]; + for (int i = 0; i < length; i++) { + int v = ((int) data[i + offset]) & 0xFF; + hexDigits[i * 2] = toHexChar(v >>> 4); + hexDigits[i * 2 + 1] = toHexChar(v & 0xF); + } + return new String(hexDigits); + } + + /** + * Gets the Java hashCode of any value. + * + * The hashCode of null is defined as zero + * + * @param a Any Java Object, may be null + * @return hash code + */ + public static int hashCode(Object a) { + if (a == null) return 0; + return a.hashCode(); + } + + /** + * Tests if two byte array regions are identical + * + * @param a First array + * @param aOffset Offset into first array + * @param b Second array + * @param bOffset Offset into second array + * @param length Number of bytes to compare + * @return true if array regions are equal, false otherwise + */ + public static boolean arrayEquals(byte[] a, int aOffset, byte[] b, int bOffset, int length) { + return Arrays.equals(a, aOffset, aOffset+length, b, bOffset, bOffset+length); + } + + /** + * Compares two byte arrays on an unsigned basis. Shorter arrays will be + * considered "smaller" if they match in all other positions. + * + * @param a First array + * @param aOffset Offset into first array + * @param b Second array + * @param bOffset Offset into second array + * @param maxLength The maximum size for comparison. If arrays are equal up to + * this length, will return 0 + * @return Negative if a is 'smaller', 0 if a 'equals' b, positive if a is + * 'larger'. + */ + public static int compareByteArrays(byte[] a, int aOffset, byte[] b, int bOffset, int maxLength) { + int length = Math.min(maxLength, a.length - aOffset); + length = Math.min(maxLength, b.length - bOffset); + for (int i = 0; i < length; i++) { + int ai = 0xFF & a[aOffset + i]; + int bi = 0xFF & b[bOffset + i]; + if (ai < bi) return -1; + if (ai > bi) return 1; + } + if (length < a.length) return 1; // longer a considered larger + if (length < b.length) return -1; // shorter a considered smaller + return 0; + } + + /** + * Converts an unsigned BigInteger to a hex string with the given number of + * digits Truncates any high bytes beyond the given digits. + * + * @param a Value to convert + * @param digits Number of hex digits to produce + * @return String containing the hex representation + */ + public static String toHexString(BigInteger a, int digits) { + if (a.signum() < 0) + throw new IllegalArgumentException("toHexString requires a non-negative BigInteger, got :" + a); + String s = a.toString(16); // note: only works with unsigned big integers otherwise we get "-2a" etc. + int slen = s.length(); + if (slen > digits) throw new IllegalArgumentException("toHexString number of digits exceeded, got :" + slen); + if (slen == digits) return s; + StringBuffer sb = new StringBuffer(digits); + while (slen < digits) { + sb.append('0'); + slen++; + } + sb.append(s); + return sb.toString(); + } + + /** + * Writes an unsigned big integer to a specific segment of a byte[] array. Pads + * with zeros if necessary to fill the specified length. + * + * @param a Value to write + * @param dest Destination array + * @param offset Offset into destination array + * @param length Length to write + */ + public static void writeUInt(BigInteger a, byte[] dest, int offset, int length) { + if (a.signum() < 0) throw new IllegalArgumentException("Non-negative big integer expected!"); + if ((offset + length) > dest.length) { + throw new IllegalArgumentException( + "Insufficient buffer space in byte array, available = " + (dest.length - offset)); + } + byte[] bs = a.toByteArray(); + int bl = bs.length; + if (bl == length) { + // expected case, correct number of bytes in unsigned representation + System.arraycopy(bs, 0, dest, offset, length); + } else if ((bl == (length + 1)) && (bs[0] == 0)) { + // OK because this is just an overflow of sign bit + // We just need to skip the zero bute that includes the sign + System.arraycopy(bs, 1, dest, offset, length); + } else if (bl < length) { + // rare case, our representation is too short, so need to pad + int pad = length - bl; + Arrays.fill(dest, offset, offset + pad, (byte) 0); + System.arraycopy(bs, 0, dest, offset + pad, bl); + } else { + throw new IllegalArgumentException("Insufficient buffer size, was " + length + " but needed " + bl); + } + } + + /** + * Converts a String to a byte array using UTF-8 encoding + * + * @param s Any String + * @return Byte array + */ + public static byte[] toByteArray(String s) { + return s.getBytes(StandardCharsets.UTF_8); + } + + /** + * Converts any array to an Object[] array + * + * @param anyArray Array to convert + * @return Object[] array + */ + public static Object[] toObjectArray(Object anyArray) { + if (anyArray instanceof Object[]) return (Object[]) anyArray; + int n = Array.getLength(anyArray); + Object[] result = new Object[n]; + for (int i = 0; i < n; i++) { + result[i] = Array.get(anyArray, i); + } + return result; + } + + /** + * Converts any array to an ACell[] array. Elements must be Cells. + * + * @param anyArray Array to convert + * @return ACell[] array + */ + public static ACell[] toCellArray(Object anyArray) { + int n = Array.getLength(anyArray); + ACell[] result = new ACell[n]; + for (int i = 0; i < n; i++) { + result[i] = (ACell) Array.get(anyArray, i); + } + return result; + } + + /** + * Equality method allowing for nulls + * + * @param a First value + * @param b Second value + * @return true if arguments are equal, false otherwise + */ + public static boolean equals(Object a, Object b) { + if (a == b) return true; + if (a == null) return false; // b can't be null because of above line + return a.equals(b); // fall back to Object equality + } + + /** + * Equality method allowing for nulls + * + * @param a First value + * @param b Second value + * @return true if arguments are equal, false otherwise + */ + public static boolean equals(ACell a, ACell b) { + if (a == b) return true; + if (a == null) return false; // b can't be null because of above line + return a.equals(b); // fall back to Object equality + } + + /** + * Gets a hex digit as an integer 0-15 value from a Data object + * + * @param data Blob containing byte values + * @param hexDigit Position of hex digit to extract (from start of blob) + * @return Hex digit value as an integer 0..15 inclusive + */ + public static int extractDigit(ABlob data, int hexDigit) { + return data.getHexDigit(hexDigit); + } + + /** + * Gets the class of an Object, or null if the value is null + * + * @param o Object to examine + * @return Class of the Object + */ + public static Class getClass(Object o) { + if (o == null) return null; + return o.getClass(); + } + + /** + * Gets the class name of an Object, or "null" if the value is null + * + * @param o Object to examine + * @return Class name of the Object + */ + public static String getClassName(Object o) { + Class klass = getClass(o); + return (klass == null) ? "null" : klass.getName(); + } + + /** + * Converts a long to an int, throws error if out of allowable range. + * + * @param a Value to convert + * @return int value of the long if in valid Integer range + */ + public static int checkedInt(long a) { + int i = (int) a; + if (a != i) throw new IllegalArgumentException(Errors.sizeOutOfRange(a)); + return i; + } + + /** + * Converts a long to a short, throws error if out of allowable range. + * + * @param a Value to convert + * @return short value of the long if in valid Short range + */ + public static short checkedShort(long a) { + short s = (short) a; + if (s != a) throw new IllegalArgumentException(Errors.sizeOutOfRange(a)); + return s; + } + + /** + * Converts a long to a byte, throws error if out of allowable range. + * + * @param a Value to convert + * @return byte value of the long if in valid Byte range + */ + public static byte checkedByte(long a) { + byte b = (byte) a; + if (b != a) throw new IllegalArgumentException(Errors.sizeOutOfRange(a)); + return b; + } + + /** + * Writes an unsigned BigInteger as 32 bytes into a ByteBuffer + * + * @param b A ByteBuffer with at least 32 bytes capacity + * @param v A BigInteger in the unsigned 256 bit integer range + * @return The ByteBuffer with 32 bytes written + */ + public static ByteBuffer writeUInt256(ByteBuffer b, BigInteger v) { + if (v.signum() < 0) throw new IllegalArgumentException("Non-negative integer expected"); + byte[] bs = v.toByteArray(); + byte[] buf = new byte[32]; + int blen = bs.length; // length to use + if (blen <= 32) { + System.arraycopy(bs, 0, buf, 32 - blen, blen); + } else if ((blen == 33) && (bs[0] == 0)) { + // OK since this is UInt256 range, take last 32 bytes + System.arraycopy(bs, blen - 32, buf, 0, 32); + } else { + throw new IllegalArgumentException("BigInteger too large for UInt256, length in bytes=" + blen); + } + return b.put(buf); + } + + /** + * Reads an unsigned BigInteger as 32 bytes from a ByteBuffer + * + * @param b ByteBuffer from which to extract 32 bytes + * @return A non-negative BigInteger containing the unsigned big-endian value + * from the 32 bytes read + */ + public static BigInteger readUInt256(ByteBuffer b) { + byte[] buf = new byte[32]; + b.get(buf); + return new BigInteger(1, buf); + } + + /** + * Returns the minimal number of bits to represent the signed twos complement + * long value. Return value will be at least 1, max 64 + * + * @param x Long value + * @return Number of bits required for representation, in the range 1..64 + * inclusive + */ + public static int bitLength(long x) { + long ux = (x >= 0) ? x : -x - 1; + return 1 + (64 - Bits.leadingZeros(ux)); // sign bit plus number of used bits in positive representation + } + + /** + * Converts an object to an int value, handling Strings and arbitrary numbers. + * + * @param v An object representing a valid int value + * @return The converted int value of the object + * @throws IllegalArgumentException If the argument cannot be converted to an + * int + */ + public static int toInt(Object v) { + if (v instanceof Integer) return (Integer) v; + if (v instanceof String) { + return Integer.parseInt((String) v); + } + if (v instanceof Number) { + Number number = (Number) v; + int value = (int) number.longValue(); + // following is safe, because double can represent any int + if (value != number.doubleValue()) throw new IllegalArgumentException("Cannot coerce to int without loss:"); + return value; + } + throw new IllegalArgumentException("Can't convert to int: " + v); + } + + /** + * Gets a resource as a String. + * + * @param path Path to resource, e.g "actors/token.con" + * @return String content of resource file + * @throws IOException If an IO error occurs + */ + public static String readResourceAsString(String path) throws IOException { + ClassLoader classLoader = ClassLoader.getSystemClassLoader(); + try (InputStream inputStream = classLoader.getResourceAsStream(path)) { + if (inputStream == null) throw new IOException("Resource not found: " + path); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + return reader.lines().collect(Collectors.joining(System.lineSeparator())); + } + } + } + + /** + * Extract a number of bits (up to 32) from a big-endian byte array, shifting + * right by the specified amount. Sign extends for bits beyond range of array. + * @param bs Source byte array + * @param numBits Number of bits to extract (0-32) + * @param shift Number of bits to shift + * @return Bits returned + */ + public static int extractBits(byte[] bs, int numBits, int shift) { + if ((numBits < 0) || (numBits > 32)) throw new IllegalArgumentException("Invalid number of bits: " + numBits); + + if (numBits > 8) { + return extractBits(bs, 8, shift) | (extractBits(bs, numBits - 8, shift + 8) << 8); + } + if (shift < 0) throw new IllegalArgumentException("Negative shift: " + shift); + int bslen = bs.length; + + int bshift = shift >> 3; // shift in number of bytes + + if (bshift >= bslen) { + // beyond end of array, so sign extend last byte + return ((bs[0] >= 0) ? 0 : -1) & Bits.lowBitMask(numBits); + } + + int lowShift = (shift - (bshift << 3)); + int ix = bslen - bshift - 1; // index of low byte + + int val = bs[ix]; // low byte from array into val, sign extend + if (ix > 0) { + val = val & 0xFF; // clear top 3 bytes of val + val = val | (((int) bs[ix - 1]) << 8); // high byte, sign extended + } + val = val >> lowShift; // shift val to position low bits correctly + return val & Bits.lowBitMask(numBits); // return just the requested bits + } + + /** + * Sets a number of bits (up to 32) in a big-endian byte array, shifting by the + * specified amount Ignores bits set outside the byte array + * @param bs Target byte array + * @param numBits Number of bits to set (0-32) + * @param shift Number of bits to shift + * @param bits Bits to set + */ + public static void setBits(byte[] bs, int numBits, int shift, int bits) { + if ((numBits < 0) || (numBits > 32)) { + throw new IllegalArgumentException("Invalid number of bits: " + numBits); + } + if (numBits > 8) { + setBits(bs, 8, shift, bits); + setBits(bs, numBits - 8, shift + 8, bits >> 8); + return; + } + if (shift < 0) throw new IllegalArgumentException("Negative shift: " + shift); + int bslen = bs.length; + int bshift = shift >> 3; // shift in number of bytes + if (bshift >= bslen) return; // nothing to do, beyond end of byte array + int ix = bslen - bshift - 1; // index of low byte + + // setup val with bits to set, others zero + int lowShift = (shift - (bshift << 3)); + int lowBitMask = Bits.lowBitMask(numBits); + int val = (bits & lowBitMask) << lowShift; + + // setup keep with bits to keep in low 16 bits + int keepBitMask = ~(lowBitMask << lowShift); // bits to keep from original array + int keep = (bs[ix] & 0xFF); + if (ix > 0) { + keep = keep | (((bs[ix - 1]) & 0xFF) << 8); + } + keep = keep & keepBitMask; + + val = val | keep; + bs[ix] = (byte) (val & 0xFF); + if (ix > 0) { + bs[ix - 1] = (byte) ((val >> 8) & 0xFF); + } + } + + /** + * Reads data from the Byte Buffer buffer, up to the limit. + * @param bb ByteBuffer to read from + * @return Blob containing bytes read from buffer + */ + public static AArrayBlob readBufferData(ByteBuffer bb) { + bb.position(0); + int len = bb.remaining(); + byte[] bytes = new byte[len]; + bb.get(bytes); + return Blob.wrap(bytes); + } + + /** + * Prints an Object in readable String representation + * @param v Object to print + * @return String representation of value + */ + public static String print(Object v) { + StringBuilder sb=new StringBuilder(); + print(sb,v); + return sb.toString(); + } + + /** + * Prints an Object in readable String representation + * @param sb StringBuilder to append to + * @param v Object to print + */ + public static void print(StringBuilder sb,Object v) { + if (v == null) { + sb.append("nil"); + } else if (v instanceof AObject) { + ((AObject)v).print(sb); + } else if (v instanceof Boolean || v instanceof Number){ + sb.append(v.toString()); + } else if (v instanceof String) { + sb.append('"'); + sb.append((String)v); + sb.append('"'); + } else if (v instanceof Instant) { + sb.append(((Instant)v).toEpochMilli()); + } else if (v instanceof Character) { + CVMChar.create((long)v).print(sb); + } else { + throw new TODOException("Can't print: " + Utils.getClass(v)); + } + } + + /** + * Converts a Object to an InetSocketAddress + * + * @param o An Object to convert to a socket address. May be a String or existing InetSocketAddress + * @return A valid InetSocketAddress, or null if not in valid format + */ + public static InetSocketAddress toInetSocketAddress(Object o) { + if (o instanceof InetSocketAddress) { + return (InetSocketAddress) o; + } else if (o instanceof String) { + return toInetSocketAddress((String)o); + } else if (o instanceof URL) { + return toInetSocketAddress((URL)o); + } else { + return null; + } + } + + /** + * Converts a String to an InetSocketAddress + * + * @param s A string in the format of a valid URL or "myhost.com:17888" + * @return A valid InetSocketAddress, or null if not in valid format + */ + public static InetSocketAddress toInetSocketAddress(String s) { + if (s==null) return null; + try { + // Try URL parsing first + URL url=new URL(s); + return toInetSocketAddress(url); + } catch (MalformedURLException ex) { + // Try to parse as host:port + int colon = s.lastIndexOf(':'); + if (colon < 0) return null; + try { + String hostName = s.substring(0, colon); // up to last colon + int port = Utils.toInt(s.substring(colon + 1)); // after last colon + InetSocketAddress addr = new InetSocketAddress(hostName, port); + return addr; + } catch (Exception e) { + return null; + } + } + } + + /** + * Converts a URL to an InetSocketAddress. Will assume default port if not specified. + * + * @param url A valid URL + * @return A valid InetSocketAddress for the URL + */ + public static InetSocketAddress toInetSocketAddress(URL url) { + String host=url.getHost(); + int port=url.getPort(); + if (port<0) port=Constants.DEFAULT_PEER_PORT; + return new InetSocketAddress(host,port); + } + + /** + * Filters the array, returning an array containing only the elements where the + * predicate returns true. May return the same array if all elements are + * included. + * + * @param arr Array to filter + * @param predicate Predicate to test array elements + * @return Filtered array. + */ + public static T[] filterArray(T[] arr, Predicate predicate) { + if (arr.length <= 32) return filterSmallArray(arr, predicate); + throw new TODOException("Filter large arrays"); + } + + /** + * Return a list of values, sorted according to the score computed using the + * provided function, in ascending order. Ignores elements where score is null + * (will not be included in the resulting list) + * + * @param scorer a Function mapping collection elements to Long values + * @param coll Collection of values to compare + * @return The sorted collection values as an ArrayList, in ascending score + * order. + */ + public static ArrayList sortListBy(Function scorer, Collection coll) { + // TODO can probably improve efficiency + ArrayList result = new ArrayList<>(coll.size()); + HashMap scores = new HashMap<>(coll.size()); + for (T c : coll) { + Long score = scorer.apply(c); + if (score == null) continue; + scores.put(c, score); + result.add(c); + } + result.sort(new Comparator<>() { + @Override + public int compare(T a, T b) { + long comp = scores.get(a) - scores.get(b); + return Long.signum(comp); + } + }); + return result; + } + + /** + * Filters the array, returning an array containing only the elements where the + * predicate returns true. May return the same array if all elements are + * included. + * + * Array must have a maximum of 32 elements + * + * @param arr + * @param predicate + * @return + */ + private static T[] filterSmallArray(T[] arr, Predicate predicate) { + int mask = 0; + int n = arr.length; + for (int i = 0; i < n; i++) { + if (predicate.test(arr[i])) mask |= (1 << i); + } + return filterSmallArray(arr, mask); + } + + @SuppressWarnings("unchecked") + public static T[] filterSmallArray(T[] arr, int mask) { + int n = arr.length; + if (n > 32) throw new IllegalArgumentException("Array too long to filter: " + n); + int fullMask = (1 << n) - 1; + if (mask == fullMask) return arr; + int nn = Integer.bitCount(mask); + T[] result = (T[]) Array.newInstance(arr.getClass().getComponentType(), nn); + if (nn == 0) return result; + int ix = 0; + for (int i = 0; i < n; i++) { + if ((mask & (1 << i)) != 0) { + result[ix++] = arr[i]; + } + } + assert (ix == nn); + return result; + } + + /** + * Computes a bit mask of up to 16 bits by scanning a full array for which + * elements are included in the subset, comparing using object identity + * + * Subset must be an ordered subset of of the full array + * + * @param set Array of elements + * @param subset Array of element subset (must be identical) + * @return Bit mask as a short + */ + public static short computeMask(T[] set, T[] subset) { + int n = set.length; + if (n > 16) throw new IllegalArgumentException("Max length of 16 for mask computation, got: " + n); + int mask = 0; + int ix = 0; + int subsetLength = subset.length; + for (int i = 0; i < n; i++) { + if (ix == subsetLength) break; // no more items to find + if (set[i] == subset[ix]) { + mask |= (1 << i); + ix++; + } + } + if (ix != subsetLength) throw new IllegalArgumentException("Subset not all found"); + return (short) mask; + } + + /** + * Hack to convert a checked exception into an unchecked exception. + * + * @param Type of exception to return + * @param t Any Throwable instance + * @return Throwable instance + * @throws T In all cases + */ + @SuppressWarnings("unchecked") + public static T sneakyThrow(Throwable t) throws T { + throw (T) t; + } + + @SuppressWarnings("unchecked") + public static T[] copyOfRangeExcludeNulls(T[] entries, int offset, int length) { + int newLen = length; + for (int i = 0; i < length; i++) { + if (entries[offset + i] == null) newLen--; + } + if (newLen < length) { + T[] result = (T[]) Array.newInstance(entries.getClass().getComponentType(), newLen); + int ix = 0; + for (int i = 0; i < length; i++) { + T v = entries[offset + i]; + if (v != null) { + result[ix++] = v; + } + } + assert (ix == newLen); + return result; + } else { + return Arrays.copyOfRange(entries, offset, offset + length); + } + } + + /** + * Reverse an array in place + * @param arr Array to reverse + */ + public static void reverse(T[] arr) { + reverse(arr, arr.length); + } + + /** + * Reverse the first n elements of an array in place + * @param arr Array to reverse + * @param n Number of elements to reverse + */ + public static void reverse(T[] arr, int n) { + for (int i = 0; i < (n / 2); i++) { + T val = arr[i]; + arr[i] = arr[n - i - 1]; + arr[n - i - 1] = val; + } + } + + /** + * Reads the full contents of an input stream into a new byte array. + * + * @param is An arbitrary InputStream + * @return A byte array containing the full contents of the given InputStream + * @throws IOException If IO error occurs + */ + public static byte[] readBytes(InputStream is) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + int bytesRead; + while ((bytesRead = is.read(buf)) >= 0) { + bos.write(buf, 0, bytesRead); + } + return bos.toByteArray(); + } + + public static boolean isOdd(long x) { + return (x & 1L) != 0; + } + + /** + * Displays a String representing the given Object, printing null as "nil" + * + * SECURITY: should *not* be used in Actor code, use RT.str(...) instead. + * + * @param o Object to convert + * @return String representation of object + */ + public static String toString(Object o) { + if (o == null) return "nil"; + return o.toString(); + } + + /** + * Removes all spaces from a String + * @param s String to strip + * @return String without spaces + */ + public static String stripWhiteSpace(String s) { + return s.replaceAll("\\s+", ""); + } + + /** + * Gets the number of Refs directly contained in a Cell (will be zero if the + * Cell is not a Ref container) + * + * @param a Cell to check (may be null) + * @return Number of Refs in the object. + */ + public static int refCount(ACell a) { + if (a==null) return 0; + return a.getRefCount(); + } + + /** + * Counts the total number of Refs contained in a data object recursively. Will + * count duplicate children multiple times. + * + * @param a Object to count Refs in + * @return Total number of Refs found + */ + public static long totalRefCount(Object a) { + if (!(a instanceof ACell)) return 0; + + ACell ra = (ACell) a; + long[] count = new long[] { 0L }; + + ACell ra2; + ra2 = ra.updateRefs(r -> { + count[0] += 1 + totalRefCount(r.getValue()); + + return r; + }); + assert (ra == ra2); // check we didn't change anything! + return count[0]; + } + + public static Ref getRef(ACell o, int i) { + if (o ==null) throw new IllegalArgumentException("Bad ref index: " + i+ " called on null"); + return o.getRef(i); + } + + @SuppressWarnings("unchecked") + public static T updateRefs(T o, IRefFunction func) { + if (o==null) return o; + return (T) o.updateRefs(func); + } + + public static int bitCount(short mask) { + return Integer.bitCount(mask & 0xFFFF); + } + + /** + * Runs test repeatedly, until it returns true or the timeout has elapsed + * + * @param timeoutMillis Timeout interval + * @param test Test to run until true + * @return True if the operation timed out, false otherwise + */ + public static boolean timeout(int timeoutMillis, Supplier test) { + long start = getTimeMillis(); + long end=start+timeoutMillis; + long now = start; + + // loop until either test succeeds (return false) or the timeout happens (return true) + while (true) { + if (test.get()) return false; + + // test failed, so sleep + try { + // compute sleep time + long nextInterval=(long) ((now - start) * 0.3 + 1); + long sleepTime=Math.min(nextInterval, end-now); + if (sleepTime<0L) return true; + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + // ignore? Probably shouldn't happen though + // But should set interrupt flag as below; + Thread.currentThread().interrupt(); + } + now = getTimeMillis(); + } + } + + private static long lastTimestamp = Instant.now().toEpochMilli(); + + /** + * Gets the current system timestamp. Guaranteed monotonic within this JVM. + * + * Should be used for timestamps that need to be persisted or communicated + * Should not be used for timing - use Utils.getTimeMillis() instead + * + * + * @return Long representation of Timestamp + */ + public static long getCurrentTimestamp() { + // Use Instant milliseconds + long ts = Instant.now().toEpochMilli(); + if (ts > lastTimestamp) { + lastTimestamp = ts; + return ts; + } else { + return lastTimestamp; + } + } + + private static final long startupTimestamp=getCurrentTimestamp(); + private static final long startupNanos=System.nanoTime(); + + /** + * Gets the a millisecond accurate time suitable for use in timing. + * + * Should not be used for timestamps + * + * + * @return long + */ + public static long getTimeMillis() { + // Use nanoTime() for precision and guaranteed monotonicity + long elapsedMillis = (System.nanoTime()-startupNanos)/1000000; + return startupTimestamp+elapsedMillis; + } + + /** + * Test if the first hex digits of two bytes match + * + * @param a Any byte value + * @param b Any byte value + * @return true if the first hex digit (high nibble) of the two bytes is equal, + * false otherwise. + */ + public static boolean firstDigitMatch(byte a, byte b) { + return (a & 0xF0) == (b & 0xF0); + } + + /** + * Leftmost Binary Search. + * + * Generic method to search for an exact or approximate (leftmost) value. + * + * Examples: + * Given a vector [1, 2, 3] and target 2: returns 2. + * Given a vector [1, 2, 3] and target 5: returns 3. + * Given a vector [1, 2, 3] and target 0: returns null. + * + * @param L Items. + * @param value Function to get the value for comparison with target. + * @param comparator How to compare value with target. + * @param target Value being searched for. + * @param Type of the elements in L. + * @param Type of the target value. + * @return Target, or leftmost value, or null if there isn't a match. + */ + public static T binarySearchLeftmost(ASequence L, Function value, Comparator comparator, U target) { + long min = 0; + long max = L.count(); + + while (min < max) { + long midpoint = (min + max) / 2; + + if (comparator.compare(value.apply(L.get(midpoint)), target) < 0) + min = midpoint + 1; + else + max = midpoint; + } + + // Match can be exact or approximate. + // In case there isn't an exact match, + // a leftmost search returns a rank (min) + // which is used to get the leftmost value. + if (min < L.count() && comparator.compare(value.apply(L.get(min)), target) == 0) { + return L.get(min); + } else { + if (min - 1 == -1) + return null; + + return L.get(min - 1); + } + + } + + @SuppressWarnings("unchecked") + public static CompletableFuture> completeAll(java.util.List> futures) { + CompletableFuture[] fs = futures.toArray(new CompletableFuture[futures.size()]); + + return CompletableFuture.allOf(fs).thenApply(e -> futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList()) + ); + } + + public static State stateAsOf(AVector states, CVMLong timestamp) { + return binarySearchLeftmost(states, State::getTimeStamp, Comparator.comparingLong(CVMLong::longValue), timestamp); + } + + public static AVector statesAsOfRange(AVector states, CVMLong timestamp, long interval, int count) { + AVector v = Vectors.empty(); + + for (int i = 0; i < count; i++) { + v = v.conj(stateAsOf(states, timestamp)); + + timestamp = CVMLong.create(timestamp.longValue() + interval); + } + + return v; + } + + public static boolean bool(Object a) { + if (a==null) return false; + if (a instanceof ACell) return (RT.bool((ACell)a)); + if (a instanceof Boolean) return ((Boolean)a); + return true; // consider other values truthy + } + + @SafeVarargs + public static List listOf(T... values) { + return Arrays.asList(values); + } + + private static final ExecutorService executor=Executors.newCachedThreadPool(); + + /** + * Executes functions on a thread pool for each element of a collection + * @param Result type of function + * @param Argument type + * @param func Function to run + * @param items Collection of items to run futures on + * @return List of futures for each item + */ + public static ArrayList> futureMap(Function func, Collection items) { + ArrayList> futures=new ArrayList<>(items.size()); + for (T item: items) { + futures.add(executor.submit(()->func.apply(item))); + } + return futures; + } + + +} diff --git a/convex-core/src/main/java/etch/Etch.java b/convex-core/src/main/java/etch/Etch.java new file mode 100644 index 000000000..1f7ab2d7c --- /dev/null +++ b/convex-core/src/main/java/etch/Etch.java @@ -0,0 +1,909 @@ +package etch; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.nio.channels.FileLock; +import java.util.ArrayList; +import java.util.Arrays; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import convex.core.Constants; +import convex.core.data.AArrayBlob; +import convex.core.data.ACell; +import convex.core.data.Blob; +import convex.core.data.Hash; +import convex.core.data.Ref; +import convex.core.data.RefSoft; +import convex.core.util.Counters; +import convex.core.util.Shutdown; +import convex.core.util.Utils; + +/** + * A stupid, fast database for immutable data you want carved in stone. + * + * We solve the cache invalidation problem, quite effectively, by never changing anything. Once a value + * is written for a given key, it cannot be changed. Etch is indifferent to the exact meaning of keys, + * but they must have a fixed length of 32 bytes (256 bits). + * + * It is intended that keys are pseudo-random hash values, which will result in desirable distributions + * of data for the radix tree structure. + * + * Radix tree index blocks are 256-way arrays of 8 byte pointers. + * + * To avoid creating too many index blocks when collisions occur, a chained entry list inside is created + * in unused space in index blocks. Once there is no more space, chains are collapsed to a new index block. + * + * Pointers in index blocks are of 4 possible types, determined by the two high bits (MSBs): + * - 00 high bits: pointer to data + * - 01 high bits: pointer to next index node + * - 10 high bits: start of chained entry list + * - 11 high bits: continuation of chained entry list + * + * Data is stored as: + * - 32 bytes key + * - X bytes monotonic label of which + * - 1 byte status + * - 8 bytes Memory Size (TODO: might be negative for unknown?) + * - 2 bytes data length N (a short) + * - N byes actual data + */ +public class Etch { + // structural constants for data block + private static final int KEY_SIZE=32; + private static final int LABEL_SIZE=1+8; // Flags (byte) plus Memory Size (long) + private static final int LENGTH_SIZE=2; + private static final int POINTER_SIZE=8; + + /** + * Index block is fixed size with 256 entries + */ + private static final int INDEX_BLOCK_SIZE=POINTER_SIZE*256; + + // constants for memory mapping buffers into manageable regions + private static final int MAX_REGION_SIZE=1<<30; // 1GB seems reasonable + private static final int REGION_MARGIN=65536; // 64k margin for writes past end of current buffer + + /** + * Magic number for Etch files, must be first 2 bytes + */ + private static final byte[] MAGIC_NUMBER=Utils.hexToBytes("e7c6"); + + private static final int SIZE_HEADER_MAGIC=2; + private static final int SIZE_HEADER_FILESIZE=8; + private static final int SIZE_HEADER_ROOT=32; + + /** + * Length of header, including: + * - Magic number "e7c6" (2 bytes) + * - File size (8 bytes) + * - Root hash (32 bytes) + * + * "The Ultimate Answer to Life, The Universe and Everything is... 42!" + * - Douglas Adams, The Hitchhiker's Guide to the Galaxy + */ + private static final int SIZE_HEADER=SIZE_HEADER_MAGIC+SIZE_HEADER_FILESIZE+SIZE_HEADER_ROOT; + + protected static final long OFFSET_FILE_SIZE = SIZE_HEADER_MAGIC; + protected static final long OFFSET_ROOT_HASH = SIZE_HEADER_MAGIC+SIZE_HEADER_FILESIZE; + + /** + * Start position of first index block + * This is immediately after a long data length pointer at the start of the file + */ + private static final long INDEX_START=SIZE_HEADER; + + private static final long TYPE_MASK= 0xC000000000000000L; + private static final long PTR_PLAIN=0x0000000000000000L; // direct pointer to data + private static final long PTR_INDEX=0x4000000000000000L; // pointer to index block + private static final long PTR_START=0x8000000000000000L; // start of chained entries + private static final long PTR_CHAIN=0xC000000000000000L; // chained entries after start + + private static final Logger log=LoggerFactory.getLogger(Etch.class.getName()); + + /** + * Temporary byte array for writer. Must not be used by readers. + */ + private final ThreadLocal tempArray=new ThreadLocal<>() { + @Override + public byte[] initialValue() { + return new byte[INDEX_BLOCK_SIZE]; + } + }; + + /** + * Internal pointer to end of database + */ + private static long tempIndex=0; + + private final File file; + private final RandomAccessFile data; + + /** + * List of MappedByteBuffers for each region of the database file. + */ + private final ArrayList regionMap=new ArrayList<>(); + + private long dataLength=0; + + private boolean BUILD_CHAINS=true; + private EtchStore store; + + private Etch(File dataFile) throws IOException { + // Ensure we have a RandomAccessFile that exists + this.file=dataFile; + if (!dataFile.exists()) dataFile.createNewFile(); + this.data=new RandomAccessFile(dataFile,"rw"); + + // Try to exclusively lock the Etch database file + FileChannel fileChannel=this.data.getChannel(); + FileLock lock=fileChannel.tryLock(); + if (lock==null) { + log.error("Unable to obtain lock on file: {}",dataFile); + throw new IOException("File lock failed"); + } + + // at this point, we have an exclusive lock on the database file. + + if (dataFile.length()==0) { + // Need to populate new file, with data length long and initial index block + MappedByteBuffer mbb=seekMap(0); + mbb.put(MAGIC_NUMBER); + + // write zeros (temp is newly empty) for file size and root. Will fix later + int headerZeros=SIZE_HEADER_FILESIZE+SIZE_HEADER_ROOT; + byte[] temp=new byte[headerZeros]; + mbb.put(temp,0,headerZeros); + dataLength=SIZE_HEADER; // advance past initial long + + // add an index block + long indexStart=appendNewIndexBlock(); + assert(indexStart==INDEX_START); + + // ensure data length is initially correct + mbb=seekMap(SIZE_HEADER_MAGIC); + mbb.putLong(dataLength); + } else { + // existing file, so need to read the length pointer + MappedByteBuffer mbb=seekMap(0); + byte[] check=new byte[2]; + mbb.get(check); + if(!Arrays.equals(MAGIC_NUMBER, check)) { + throw new IOException("Bad magic number! Probably not an Etch file: "+dataFile); + } + + long length = mbb.getLong(); + dataLength=length; + } + + // shutdown hook to close file / release lock + convex.core.util.Shutdown.addHook(Shutdown.ETCH,new Runnable() { + public void run() { + close(); + } + }); + } + + /** + * Create an Etch instance using a temporary file. + * @return The new Etch instance + * @throws IOException If an IO error occurs + */ + public static Etch createTempEtch() throws IOException { + Etch newEtch = createTempEtch("etch-"+tempIndex); + tempIndex++; + return newEtch; + } + + /** + * Create an Etch instance using a temporary file with a specific file prefix. + * @param prefix temporary file prefix to use + * @return The new Etch instance + * @throws IOException If an IO error occurs + */ + public static Etch createTempEtch(String prefix) throws IOException { + File data = File.createTempFile(prefix+"-", null); + if (Constants.ETCH_DELETE_TEMP_ON_EXIT) data.deleteOnExit(); + return new Etch(data); + } + + /** + * Create an Etch instance using the specified file + * @param file File with which to create Etch instance + * @return The new Etch instance + * @throws IOException If an IO error occurs + */ + public static Etch create(File file) throws IOException { + Etch etch= new Etch(file); + log.debug("Etch created on file: {} with data length: {}"+file,etch.dataLength); + return etch; + } + + /** + * Gets the MappedByteBuffer for a given position, seeking to the specified location. + * Type flags are ignored if included in the position pointer. + * + * @param position Target position for the MappedByteBuffer + * @return MappedByteBuffer instance with correct position. + * @throws IOException + */ + private MappedByteBuffer seekMap(long position) throws IOException { + position=slotPointer(position); // ensure we don't have any pesky type bits + + if ((position<0)||(position>dataLength)) { + throw new Error("Seek out of range in Etch file: position="+Utils.toHexString(position)+ " dataLength="+Utils.toHexString(dataLength)+" file="+file.getName()); + } + int mapIndex=Utils.checkedInt(position/MAX_REGION_SIZE); // 1GB chunks + + MappedByteBuffer mbb=(MappedByteBuffer) getBuffer(mapIndex).duplicate(); + mbb.position(Utils.checkedInt(position-MAX_REGION_SIZE*(long)mapIndex)); + return mbb; + } + + private MappedByteBuffer getBuffer(int regionIndex) throws IOException { + // Get current mapped region, or null if out of range + int regionMapSize=regionMap.size(); + MappedByteBuffer mbb=(regionIndex write(AArrayBlob key, Ref value) throws IOException { + Counters.etchWrite++; + return write(key,0,value,INDEX_START); + } + + private Ref write(AArrayBlob key, int keyOffset, Ref value, long indexPosition) throws IOException { + if (keyOffset>=KEY_SIZE) { + throw new Error("Offset exceeded for key: "+key); + } + + final int digit=key.byteAt(keyOffset)&0xFF; + long slotValue=readSlot(indexPosition,digit); + long type=slotType(slotValue); + + if (slotValue==0L) { + // empty location, so simply write new value + return writeNewData(indexPosition,digit,key,value,PTR_PLAIN); + + } else if (type==PTR_INDEX) { + // recursively check next level of index + long newIndexPosition=slotPointer(slotValue); // clear high bits + return write(key,keyOffset+1,value,newIndexPosition); + + } else if (type==PTR_PLAIN) { + // existing data pointer (non-zero) + // check if we have the same value first, otherwise need to resolve conflict + // This should have the current (potential collision) key in tempArray + if (checkMatchingKey(key,slotValue)) { + return updateInPlace(slotValue,value); + } + byte[] temp=tempArray.get(); + + // we need to check the next slot position to see if we can extend to a chain + int nextDigit=digit+1; + long nextSlotValue=readSlot(indexPosition,nextDigit); + + // if next slot is empty, we can make a chain! + if (BUILD_CHAINS&&(nextSlotValue==0L)) { + // update current slot to be the start of a chain + writeSlot(indexPosition,digit,slotValue|PTR_START); + + // write new data pointer to next slot + long newDataPointer=appendData(key,value); + writeSlot(indexPosition,nextDigit,newDataPointer|PTR_CHAIN); + + return value; + } + + if (keyOffset>=KEY_SIZE-1) { + throw new Error("Unexpected collision at max offset for key: "+key+" with existing key: "+Blob.wrap(temp,0,KEY_SIZE)); + } + + // have collision, so create new index node including the existing pointer + int nextDigitOfCollided=temp[keyOffset+1]&0xFF; + long newIndexPosition=appendLeafIndex(nextDigitOfCollided,slotValue); + + // put index pointer into this index block, setting flags for index node + writeSlot(indexPosition,digit,newIndexPosition|PTR_INDEX); + + // recursively write this key + return write(key,keyOffset+1,value,newIndexPosition); + } else if (type==PTR_START) { + // first check if the start pointer is the right value. if so, bail out with nothing to do + if (checkMatchingKey(key, slotValue)) { + return updateInPlace(slotValue,value); + } + + // now scan slots, looking for either the right value or an empty space + int i=1; + while (i<256) { + slotValue=readSlot(indexPosition,digit+i); + + // if we reach an empty location simply write new value as a chain continuation (PTR_CHAIN) + if (slotValue==0L) { + return writeNewData(indexPosition,digit+i,key,value,PTR_CHAIN); + } + + // if we are not in a chain, we have reached the maximum chain length. Exit loop and compress. + if (slotType(slotValue)!=PTR_CHAIN) break; + + // if we found the key itself, return since already stored. + if (checkMatchingKey(key, slotValue)) { + return updateInPlace(slotValue,value); + } + + i++; + } + + // we now need to collapse the chain, since it cannot be extended. + // System.out.println("Compressing chain, offset="+keyOffset+" chain length="+i+" with key "+key+ " indexDat= "+readBlob(indexPosition,2048)); + + // first we build a new index block, containing our new data + long newDataPointer=appendData(key,value); + long newIndexPos=appendLeafIndex(key.byteAt(keyOffset+1),newDataPointer); + + // for each element in chain, move existing data to new index block. i is the length of chain + for (int j=0; j=KEY_SIZE) { + Blob bx=readBlob(indexPosition,2048); + Blob bx2=readBlob(currentSlot,34); + Blob bx3=readBlob(dp,34); + throw new Error("Overflowing key size - key collision? index="+Utils.toHexString(indexPosition)+" dataPointer="+Utils.toHexString(dp)+" Key: "+readBlob(dp,32)); + } + + // expand to a new index block for collision + long newIndexPosition=appendNewIndexBlock(); + writeExistingData(newIndexPosition,keyOffset+1,dp); + writeExistingData(newIndexPosition,keyOffset+1,currentSlot); + writeSlot(indexPosition,digit,newIndexPosition|PTR_INDEX); + } else { + throw new Error("Unexpected type: "+type); + } + } + + /** + * Reads a blob of the specified length from storage + * @param pointer + * @param length + * @return + * @throws IOException + */ + private Blob readBlob(long pointer, int length) throws IOException { + MappedByteBuffer mbb=seekMap(pointer); + byte[] bs=new byte[length]; + mbb.get(bs); + return Blob.wrap(bs); + } + + /** + * Gets the type of a slot, given the slot value + * @param slotValue + * @return + */ + private long slotType(long slotValue) { + return slotValue&TYPE_MASK; + } + + /** + * Utility function to truncate file. Won't work if mapped byte buffers are active? + * @throws FileNotFoundException + * @throws IOException + */ + protected void truncateFile() throws FileNotFoundException, IOException { + try (FileOutputStream fos=new FileOutputStream(file, true)) { + FileChannel outChan = fos.getChannel() ; + outChan.truncate(dataLength); + } + } + + /** + * Close all files resources with this Etch store, including writing the final + * data length. + */ + synchronized void close() { + if (!(data.getChannel().isOpen())) return; // already closed + try { + // write final data length + MappedByteBuffer mbb=seekMap(OFFSET_FILE_SIZE); + mbb.putLong(dataLength); + mbb=null; + + // Force writes to disk. Probably useful. + for (MappedByteBuffer m: regionMap) { + m.force(); + } + regionMap.clear(); + System.gc(); + + data.close(); + + log.debug("Etch closed on file: "+data+" with data length: "+dataLength); + } catch (IOException e) { + log.error("Error closing Etch file: "+file); + e.printStackTrace(); + } + } + + /** + * Gets the raw pointer for, given the slot value (clears high bits) + * @param slotValue + * @return + */ + private long slotPointer(long slotValue) { + return slotValue&~TYPE_MASK; + } + + /** + * Checks if the key matches the data at the specified pointer position + * @param key + * @param dataPointer Pointer to data. Type bits in MSBs will be ignored. + * @return + * @throws IOException + */ + private boolean checkMatchingKey(AArrayBlob key, long dataPointer) throws IOException { + long dataPosition=dataPointer&~TYPE_MASK; + MappedByteBuffer mbb=seekMap(dataPosition); + byte[] temp=tempArray.get(); + mbb.get(temp, 0, KEY_SIZE); + if (key.equalsBytes(temp,0)) { + // key already in store + return true; + } + return false; + } + + /** + * Appends a leaf index block including exactly one data pointer, at the specified digit position + * @param digit Digit position for the data pointer to be stored at (0..255, high bits ignored) + * @param dataPointer Single data pointer to include in new index block + * @return the position of the new index block + * @throws IOException + */ + private long appendLeafIndex(int digit, long dataPointer) throws IOException { + long position=dataLength; + byte[] temp=tempArray.get(); + Arrays.fill(temp, (byte)0x00); + int ix=POINTER_SIZE*(digit&0xFF); + Utils.writeLong(temp, ix,dataPointer); // single node + MappedByteBuffer mbb=seekMap(position); + mbb.put(temp); // write full index block + dataLength=position+INDEX_BLOCK_SIZE; + return position; + } + + /** + * Reads a Blob from the database, returning null if not found + * @param key Key to read from Store + * @return Blob containing the data, or null if not found + * @throws IOException If an IO error occurs + */ + public Ref read(AArrayBlob key) throws IOException { + Counters.etchRead++; + + long pointer=seekPosition(key); + if (pointer<0) { + Counters.etchMiss++; + return null; // not found + } + + // seek to correct position, skipping over key + MappedByteBuffer mbb=seekMap(pointer+KEY_SIZE); + + // get flags byte + byte flagByte=mbb.get(); + + // Get memory size + long memorySize=mbb.getLong(); + + // get Data length + short length=mbb.getShort(); + byte[] bs=new byte[length]; + mbb.get(bs); + Blob encoding= Blob.wrap(bs); + try { + Hash hash=Hash.wrap(key); + ACell cell=store.decode(encoding); + cell.getEncoding().attachContentHash(hash); + + if (memorySize>0) { + // need to attach memory size for cell + cell.attachMemorySize(memorySize); + } + + Ref ref=RefSoft.create(cell, (int)flagByte); + cell.attachRef(ref); + + return ref; + } catch (Exception e) { + throw new Error("Failed to read data in etch store: "+encoding.toHexString()+" flags = "+Utils.toHexString(flagByte)+" length ="+length+" pointer = "+Utils.toHexString(pointer)+ " memorySize="+memorySize,e); + } + } + + /** + * Flushes any changes to persistent storage. + * @throws IOException If an IO error occurs + */ + public synchronized void flush() throws IOException { + for (MappedByteBuffer mbb: regionMap) { + if (mbb!=null) mbb.force(); + } + data.getChannel().force(false); + } + + /** + * Gets the position of a value in the data file from the index + * @param key Key value + * @return data file offset or -1 if not found + * @throws IOException + */ + private long seekPosition(AArrayBlob key) throws IOException { + return seekPosition(key,0,INDEX_START); + } + + /** + * Gets the slot value at the specified digit position in an index block. + * @param indexPosition Position of index block + * @param digit Digit of value 0..255 (high bits will be ignored) + * @return Pointer value (including type bits in MSBs) + * @throws IOException + */ + private long readSlot(long indexPosition, int digit) throws IOException { + long pointerIndex=indexPosition+POINTER_SIZE*(digit&0xFF); + MappedByteBuffer mbb=seekMap(pointerIndex); + long pointer=mbb.getLong(); + return pointer; + } + + /** + * Creates and writes a new data pointer at the specified position, storing the key/value + * and applying the specified type to the pointer stored in the slot + * + * @param position Position to write the data pointer + * @param key Key for the data + * @param value Value of the data + * @return + * @throws IOException + */ + private Ref writeNewData(long indexPosition, int digit, AArrayBlob key, Ref value, long type) throws IOException { + long newDataPointer=appendData(key,value)|type; + writeSlot(indexPosition, digit, newDataPointer); + return value; + } + + /** + * Updates a Ref in place at the specified position. Assumes data not changed. + * @param position Data position in storage file + * @param ref + * @return + * @throws IOException + */ + private Ref updateInPlace(long position, Ref ref) throws IOException { + // Seek to status location + MappedByteBuffer mbb=seekMap(position+KEY_SIZE); + + // Get current stored values + int currentFlags=mbb.get(); + int newFlags=Ref.mergeFlags(currentFlags,ref.getFlags()); // idempotent flag merge + + long currentSize=mbb.getLong(); + + if (currentFlags==newFlags) return ref; + + // We have a status change, need to increase status of store + mbb=seekMap(position+KEY_SIZE); + mbb.put((byte)newFlags); + + // maybe update size, if not already persisted + if ((currentSize==0L)&&((newFlags&Ref.STATUS_MASK)>=Ref.PERSISTED)) { + mbb.putLong(ref.getValue().getMemorySize()); + } + + return ref.withFlags(newFlags); // reflect merged flags + } + + /** + * Writes a slot value to an index block. + * + * @param indexPosition + * @param digit Digit radix position in index block (0..255), high bits are ignored + * @param slotValue + * @throws IOException + */ + private void writeSlot(long indexPosition, int digit, long slotValue) throws IOException { + long position=indexPosition+(digit&0xFF)*POINTER_SIZE; + MappedByteBuffer mbb=seekMap(position); + mbb.putLong(slotValue); + } + + /** + * Gets the position of a data block from the given offset into the key + * @param key Key value + * @param offset Offset in number of bytes into key value for next step of search + * @param indexPosition offset of the current index block + * @return data position for data block or -1 if not found + * @throws IOException + */ + private long seekPosition(AArrayBlob key, int offset, long indexPosition) throws IOException { + if (offset>=KEY_SIZE) { + throw new Error("Offset exceeded for key: "+key); + } + + int digit=key.byteAt(offset)&0xFF; + long slotValue=readSlot(indexPosition,digit); + long type=(slotValue&TYPE_MASK); + if (slotValue==0) { + // Empty slot i.e. not found + return -1; + } else if (type==PTR_INDEX) { + // recursively check next index node + long newIndexPosition=slotPointer(slotValue); + return seekPosition(key,offset+1,newIndexPosition); + } else if (type==PTR_PLAIN) { + if (checkMatchingKey(key,slotValue)) return slotValue; + return -1; + } else if (type==PTR_CHAIN) { + // continuation of chain from some previous index, therefore key can't be present + return -1; + } else if (type==PTR_START) { + synchronized (this) { + // start of chain, so scan chain of entries + int i=0; + while (i<256) { + long ptr=slotValue&(~TYPE_MASK); + if (checkMatchingKey(key,ptr)) return ptr; + + i++; // advance to next position + slotValue=readSlot(indexPosition,digit+i); + type=(slotValue&TYPE_MASK); + if (!(type==PTR_CHAIN)) return -1; // reached end of chain + } + } + return -1; + } else { + throw new Error("Shouldn't be possible!"); + } + } + + /** + * Append a new index block to the store file. The new Index block will be initially empty, + * i.e. filled completely with zeros. + * @return The location of the newly added index block. + * @throws IOException + */ + private long appendNewIndexBlock() throws IOException { + long position=dataLength; + byte[] temp=tempArray.get(); + MappedByteBuffer mbb=seekMap(position); + Arrays.fill(temp,(byte)0); + mbb.put(temp); + dataLength=position+INDEX_BLOCK_SIZE; + return position; + } + + /** + * Appends a new key / value data block. Returns a pointer to the data, with cleared type bits (PTR_PLAIN) + * + * @param key The key to include in the data block + * @param a the Blob representing the new data value + * @return The position of the new data block + * @throws IOException + */ + private long appendData(AArrayBlob key,Ref value) throws IOException { + assert(key.count()==KEY_SIZE); + + // Get relevant values for writing + // probably need to call these first, might move mbb position? + ACell cell=value.getValue(); + Blob encoding=cell.getEncoding(); + int status=value.getStatus(); + + long memorySize=0L; + if (status>=Ref.PERSISTED) { + memorySize=cell.getMemorySize(); + } + + // position ready for append + final long position=dataLength; + MappedByteBuffer mbb=seekMap(position); + + // append key + mbb.put(key.getInternalArray(),key.getInternalOffset(),KEY_SIZE); + + // append flags (1 byte) + int flags=value.flagsWithStatus(Math.max(value.getStatus(),Ref.STORED)); + mbb.put((byte)(flags)); // currently all flags fit in one byte + + // append Memory Size (8 bytes). Initialised to 0L if STORED only. + mbb.putLong(memorySize); + + // append blob length + short length=Utils.checkedShort(encoding.count()); + if (length==0) { + // Blob b=cell.createEncoding(); + throw new Error("Etch trying to write zero length encoding for: "+Utils.getClassName(cell)); + } + mbb.putShort(length); + + // append blob value + mbb.put(encoding.getInternalArray(),encoding.getInternalOffset(),length); + + // update total data length + dataLength=mbb.position(); + + + + if (dataLength!=position+KEY_SIZE+LABEL_SIZE+LENGTH_SIZE+length) { + System.out.println("PANIC!"); + } + + // return file position for added data + return position; + } + + public File getFile() { + return file; + } + + public synchronized Hash getRootHash() throws IOException { + MappedByteBuffer mbb=seekMap(OFFSET_ROOT_HASH); + byte[] bs=new byte[Hash.LENGTH]; + mbb.get(bs); + return Hash.wrap(bs); + } + + public synchronized void setRootHash(Hash h) throws IOException { + MappedByteBuffer mbb=seekMap(OFFSET_ROOT_HASH); + byte[] bs=h.getBytes(); + assert(bs.length==Hash.LENGTH); + mbb.put(bs); + } + + public void setStore(EtchStore etchStore) { + this.store=etchStore; + } +} diff --git a/convex-core/src/main/java/etch/EtchStore.java b/convex-core/src/main/java/etch/EtchStore.java new file mode 100644 index 000000000..3795e29a7 --- /dev/null +++ b/convex-core/src/main/java/etch/EtchStore.java @@ -0,0 +1,218 @@ +package etch; + +import java.io.File; +import java.io.IOException; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import convex.core.data.ACell; +import convex.core.data.Hash; +import convex.core.data.IRefFunction; +import convex.core.data.Ref; +import convex.core.store.AStore; +import convex.core.util.Utils; + +/** + * Class implementing on-disk memory-mapped storage of Convex data. + * + * + * "There are only two hard things in Computer Science: cache invalidation and + * naming things." - Phil Karlton + * + * Objects are keyed by cryptographic hash. That solves naming. Objects are + * immutable. That solves cache invalidation. + * + * Garbage collection is left as an exercise for the reader. + */ +public class EtchStore extends AStore { + private static final Logger log = LoggerFactory.getLogger(EtchStore.class.getName()); + + /** + * Etch Storage of persisted Refs for each hash value + */ + private Etch etch; + + public EtchStore(Etch etch) { + this.etch = etch; + etch.setStore(this); + } + + /** + * Creates an EtchStore using a specified file. + * + * @param file File to use for storage. Will be created it it does not already + * exist. + * @return EtchStore instance + * @throws IOException If an IO error occurs + */ + public static EtchStore create(File file) throws IOException { + Etch etch = Etch.create(file); + return new EtchStore(etch); + } + + /** + * Create an Etch store using a new temporary file with the given prefix + * + * @param prefix String prefix for temporary file + * @return New EtchStore instance + */ + public static EtchStore createTemp(String prefix) { + try { + Etch etch = Etch.createTempEtch(prefix); + return new EtchStore(etch); + } catch (IOException e) { + throw Utils.sneakyThrow(e); + } + } + + /** + * Create an Etch store using a new temporary file with a generated prefix + * + * @return New EtchStore instance + */ + public static EtchStore createTemp() { + try { + Etch etch = Etch.createTempEtch(); + return new EtchStore(etch); + } catch (IOException e) { + throw Utils.sneakyThrow(e); + } + } + + @SuppressWarnings("unchecked") + @Override + public Ref refForHash(Hash hash) { + try { + Ref existing = etch.read(hash); + return (Ref) existing; + } catch (IOException e) { + throw new Error("IO exception from Etch", e); + } + } + + @Override + public Ref storeRef(Ref ref, int status, Consumer> noveltyHandler) { + return storeRef(ref, noveltyHandler, status, false); + } + + @Override + public Ref storeTopRef(Ref ref, int status, Consumer> noveltyHandler) { + return storeRef(ref, noveltyHandler, status, true); + } + + @SuppressWarnings("unchecked") + public Ref storeRef(Ref ref, Consumer> noveltyHandler, int requiredStatus, + boolean topLevel) { + // first check if the Ref is already persisted to required level + if (ref.getStatus() >= requiredStatus) return ref; + + final ACell cell = ref.getValue(); + // Quick handling for null + if (cell == null) return (Ref) Ref.NULL_VALUE; + + // check store for existing ref first. + boolean embedded = cell.isEmbedded(); + Hash hash = null; + // if not embedded, worth checking store first for existing value + if (!embedded) { + hash = ref.getHash(); + Ref existing = refForHash(hash); + if (existing != null) { + // Return existing ref if status is sufficient + if (existing.getStatus() >= requiredStatus) { + cell.attachRef(existing); + return existing; + } + } + } + + // beyond STORED level, need to recursively persist child refs if they exist + if ((requiredStatus > Ref.STORED)&&(cell.getRefCount()>0)) { + IRefFunction func = r -> { + return storeRef((Ref) r, noveltyHandler, requiredStatus, false); + }; + + // need to do recursive persistence + // TODO: maybe switch to a queue? Mitigate risk of stack overflow? + ACell newObject = cell.updateRefs(func); + + // perhaps need to update Ref + if (cell != newObject) ref = ref.withValue((T) newObject); + } + + if (topLevel || !embedded) { + // Do actual write to store + final Hash fHash = (hash != null) ? hash : ref.getHash(); + if (log.isTraceEnabled()) { + log.trace( "Etch persisting at status=" + requiredStatus + " hash = 0x" + + fHash.toHexString() + " ref of class " + Utils.getClassName(cell) + " with store " + this); + } + + Ref result; + try { + // ensure status is set when we write to store + ref = ref.withMinimumStatus(requiredStatus); + result = etch.write(fHash, (Ref) ref); + } catch (IOException e) { + throw Utils.sneakyThrow(e); + } + + // call novelty handler if newly persisted + if (noveltyHandler != null) noveltyHandler.accept(result); + return (Ref) result; + } else { + // no need to write, just tag updated status + return ref.withMinimumStatus(requiredStatus); + } + } + + @Override + public String toString() { + return "EtchStore at: " + etch.getFile().getName(); + } + + /** + * Gets the database file name for this EtchStore + * + * @return File name as a String + */ + public String getFileName() { + return etch.getFile().toString(); + } + + public void close() { + etch.close(); + } + + /** + * Ensure the store is fully persisted to disk + * @throws IOException If an IO error occurs + */ + public void flush() throws IOException { + etch.flush(); + } + + public File getFile() { + return etch.getFile(); + } + + @Override + public Hash getRootHash() throws IOException { + return etch.getRootHash(); + } + + @Override + public void setRootHash(Hash h) throws IOException { + etch.setRootHash(h); + } + + /** + * Gets the underlying Etch instance + * @return Etch instance + */ + public Etch getEtch() { + return etch; + } +} diff --git a/convex-core/src/test/cvx/test/asset/box.cvx b/convex-core/src/test/cvx/test/asset/box.cvx new file mode 100644 index 000000000..997359186 --- /dev/null +++ b/convex-core/src/test/cvx/test/asset/box.cvx @@ -0,0 +1,301 @@ +;; +;; +;; Testing the `asset.box` actor. +;; +;; + +;;;;;;;;;; Setup + + +;; Deploying test version of `asset.box` instead of importing stable version. +;; +($.file/read "src/main/cvx/asset/box/actor.cvx") + + +(def box.actor + (deploy (cons 'do + (next $/*result*)))) + + +($.file/read "src/main/cvx/asset/box.cvx") + + +(def box + (deploy (concat (cons 'do + (next $/*result*)) + `((def box.actor + ~box.actor))))) + + +;; Requiring test suites for `convex.asset` since this library implement that interface. +;; +($.file/read "src/test/cvx/test/convex/asset.cvx") + +(def asset.test + (deploy (cons 'do + $/*result*))) + + +(asset.test/setup) + + +;; Other imports + +(import convex.fungible :as fungible) + +(def T + $.test) + + +;; Setup. Print test results at the end +;; +($.stream/out! (str $.term/clear.screen + "Testing `asset.box`")) + + +;; Default account is an actor, key is set to transform it into a user account. +;; +(set-key $.account/fake-key) + + +;;;;;;;;;; Helpers + + +(defmacro create-box+ + + ^{:doc {:description "Creates `n-box` boxes defined under `total`, each being also defined under `b-X` where X is a number starting at 0." + :signature [{:params [n-box]}]}} + + [n-box] + + (loop [acc [] + n 0] + (if (< n + n-box) + (recur (conj acc + (box/create)) + (inc n)) + `(do + (def total + ~acc) + ~(cons 'do + (loop [acc [] + n 0] + (if (< n + n-box) + (recur (conj acc + `(def ~(symbol (str "b-" + n)) + (total ~n))) + (inc n)) + acc))))))) + + +;;;;;;;;;; Test suites + + +(defn suite.asset + + ^{:doc {:description "Ensures `convex.asset` interface is respected."}} + + [] + + (T/group '((T/path.conj 'asset) + + (create-box+ 4) + + (T/trx '(= (set total) + (asset/balance box.actor)) + {:description "Box actor owns all its boxes."}) + + (T/trx '(address? (def user-1 + ($.account/zombie))) + {:description "User 1 created."}) + + (T/trx '(address? (def user-2 + ($.account/zombie))) + {:description "User 2 created."}) + + + (T/trx '(= #{b-0 + b-1 + b-2} + (asset/transfer user-1 + [box.actor + #{b-0 + b-1 + b-2}])) + {:description "Transfer of 3 boxes to user 1."}) + + (T/trx '(= #{b-3} + (asset/transfer user-2 + [box.actor + #{b-3}])) + {:description "Transfer of last box to user 2."}) + + (asset.test/suite.main box.actor + user-1 + user-2)))) + + + +(defn suite.content + + ^{:doc {:description "Inserting assets in boxes and removing them."}} + + [] + + (T/group '((T/path.conj 'content) + + (create-box+ 4) ;; defines `total` containing all boxes and `b-X` for each numbered box + + (T/trx '(asset/owns? *address* + [box.actor + (set total)]) + {:description "Initially, creator owns all boxes."}) + + (T/trx '(= {0 {box.actor #{b-1 + b-2}} + 1 {} + 2 {} + 3 {}} + (box/insert b-0 + [box.actor + #{b-1 + b-2}])) + {:description "Put b-1 and b-2 in b-0 since boxes are assets themselves."}) + + + (T/trx '(not (asset/owns? *address* + #{b-1 + b-2})) + {:description "Loose ownership of inserted assets."}) + + (T/trx '(asset/owns? box.actor + [box.actor + #{b-1 + b-2}]) + {:description "Box actor takes ownership of inserted assets."}) + + (T/trx '(asset/owns? *address* + [box.actor + #{b-0 + b-3}]) + {:description "Creator still owns non-inserted assets."}) + + (T/trx '(= #{b-0 + b-3} + (asset/balance box.actor + *address*)) + {:description "Don't own inserted boxes anymore."}) + + (T/trx '(= #{b-1 + b-2} + (asset/balance box.actor + box.actor)) + {:description "Box actor owns inserted boxes."}) + + (T/trx '(= #{b-1 + b-2} + (box/remove b-0 + [box.actor + #{b-1 + b-2}])) + {:description "Remove boxes from b-0."}) + + (T/trx '(= #{b-0 + b-1 + b-2 + b-3} + (asset/balance box.actor + *address*)) + {:description "Removed boxes owned again."}) + + (T/fail.code #{:TRUST} + '(box/insert b-0 + [box.actor + #{b-0}]) + {:description "Cannot insert a box into itself."}) + + ;; Using a fungible token + + (T/trx '(address? (def foocoin + (deploy (fungible/build-token {:supply 1000000})))) + {:description "Fungible token created."}) + + (T/trx '(= {0 {} + 1 {foocoin 1000} + 2 {} + 3 {}} + (box/insert b-1 + [foocoin + 1000])) + {:description "Put 1000 tokens into b-1."}) + + (T/trx '(= {0 {} + 1 {foocoin 1000} + 2 {foocoin 2000} + 3 {}} + (box/insert b-2 + [foocoin + 2000])) + {:description "Put 2000 tokens into b-2."}) + + (T/trx '(= 3000 + (asset/balance foocoin + box.actor)) + {:description "Box actor holds 3000 tokens after insertions."}) + + (T/fail.code #{:ASSERT} + '(box/remove b-1 + [foocoin + 1001]) + {:description "Cannot remove too much from a box."}) + + (T/fail.code #{:ASSERT} + '(box/remove b-1 + [foocoin + -1]) + {:description "Cannot remove negative amount from a box."}) + + (def expected-balance + (+ (- 1000000 + 3000) + 500)) + + (T/trx '(= expected-balance + (box/remove b-1 + [foocoin + 500])) + {:description "Remove 500 foocoin from b-1."}) + + (T/trx '(= expected-balance + (asset/balance foocoin + *address*)) + {:description "Regain foocoins after removing them from box actor."}) + + (T/trx '(= 2500 + (asset/balance foocoin + box.actor)) + {:description "Box balance in foocoin adjusted after removing some."})))) + + +;;; + + +(defn suite.main + + ^{:doc {:description "Main suite gathering all other suites."}} + + [] + + (T/group '((T/path.conj 'asset.box) + (suite.asset) + (suite.content)))) + + +;;; + +;($.repl/start) +(suite.main) +(T/print "asset.box") diff --git a/convex-core/src/test/cvx/test/asset/nft/simple.cvx b/convex-core/src/test/cvx/test/asset/nft/simple.cvx new file mode 100644 index 000000000..9fffbe231 --- /dev/null +++ b/convex-core/src/test/cvx/test/asset/nft/simple.cvx @@ -0,0 +1,278 @@ +;; +;; +;; Testing `asset.simple-nft`. +;; +;; + + +;;;;;;;;;; Setup + +($.stream/out! (str $.term/clear.screen + "Testing `asset.nft.simple`")) + + +;; Deploying test version of `asset.simple-nft` instead of importing stable version. +;; +($.file/read "src/main/cvx/asset/nft/simple.cvx") + + +(eval `(def nft + (deploy (quote ~(cons 'do + (next $/*result*)))))) + +;; Requiring test suites for `convex.asset` since this library implement that interface. +;; +($.file/read "src/test/cvx/test/convex/asset.cvx") + + +(def asset.test + (deploy (cons 'do + $/*result*))) + + +(asset.test/setup) + + +;; Other libraries +;; +(def T + $.test) + + +;; Default account is an actor, key is set to transform it into a user account. +;; +(set-key $.account/fake-key) + + +;;;;;;;;;; Test suites + + +(defn suite.asset + + ^{:doc {:decription "Ensures the `convex.asset` interface is respected."}} + + [] + + (T/group '((T/path.conj 'asset) + + ;; Quantities + + (T/trx '(= #{} + (asset/quantity-zero nft))) + + (T/trx '(= #{1 2 3 4} + (asset/quantity-add nft + #{1 2} + #{3 4}))) + (T/trx '(= #{1 2 3 4} + (asset/quantity-add nft + #{1 2 3} + #{2 3 4}))) + + (T/trx '(= #{1 2} + (asset/quantity-sub nft + #{1 2 3} + #{3 4 5}))) + (T/trx '(= #{1 2} + (asset/quantity-add nft + #{1 2} + nil))) + (T/trx '(= #{1 2} + (asset/quantity-add nft + nil + #{1 2}))) + + (T/trx '(= #{1 2} + (asset/quantity-sub nft + #{1 2} + nil))) + + (T/trx '(= #{} + (asset/quantity-sub nft + nil + #{1 2}))) + + (T/trx '(asset/quantity-contains? nft + #{1 2 3} + #{2 3})) + + (T/trx '(not (asset/quantity-contains? nft + #{1 2 3} + #{3 4}))) + + (T/trx '(not (asset/quantity-contains? nft + #{1 2 3} + #{4 5 6}))) + + ;; Transfer and ownership + + (T/trx '(vector? (def total + (loop [acc []] + (if (= (count acc) + 4) + acc + (recur (conj acc + (call nft + (create)))))))) + {:description "4 NFTs created."}) + + (T/trx '(= (set total) + (asset/balance nft + *address*)) + {:description "Creator balance contain all NFTs."}) + + (T/trx '(asset/owns? *address* + [nft + (set total)]) + {:description "Initially, creator owns all NFTs."}) + + (T/trx '(address? (def user-1 + ($.account/zombie))) + {:description "User 1 deployed."}) + + (T/trx '(address? (def user-2 + ($.account/zombie))) + {:description "User 2 deployed."}) + + (def balance-creator + #{(total 0)}) + + (def balance-user-1 + (set (next total))) + + (T/trx '(= balance-user-1 + (asset/transfer user-1 + [nft + balance-user-1])) + {:description "Transfer almost all NFTs to user 1."}) + + (T/trx '(= balance-user-1 + (asset/balance nft + user-1)) + {:description "Balance of user 1 updated."}) + + (T/trx '(= balance-creator + (asset/balance nft + *address*)) + {:description "Balance of created updated."}) + + (T/trx '(not (asset/owns? *address* + [nft + balance-user-1])) + {:description "After transfer, creator do not own user 1 NFTs."}) + + (T/trx '(asset/owns? user-1 + [nft + balance-user-1]) + {:description "After transfer, user 1 owns expected NFTs."}) + + (T/trx '(asset/owns? *address* + [nft + balance-creator]) + {:description "Creator still owns 1 NFT."}) + + (T/trx '(= #{} + (asset/balance nft + user-2)) + {:description "User 2 balance is still empty."}) + + (def balance-user-2 + balance-creator) + + (T/trx '(= balance-user-2 + (asset/transfer user-2 + [nft + balance-user-2])) + {:description "Transfer remaining NFT to user 2."}) + + (T/trx '(= balance-user-2 + (asset/balance nft + user-2)) + {:description "Balance of user 2 updated."}) + + (T/trx '(= #{} + (asset/balance nft + *address*)) + {:description "Balance of creator is empty."}) + + (T/trx '(asset/owns? user-2 + [nft + balance-user-2]) + {:description "User 2 owns remaining NFT."}) + + + (T/trx '(not (asset/owns? *address* + [nft + balance-user-2])) + {:description "Creator lost ownership of its last NFT."}) + + (T/trx '(= balance-user-1 + (asset/balance nft + user-1)) + {:description "Balance of user 1 unchanged."}) + + (asset.test/suite.main nft + user-1 + user-2)))) + + + +(defn suite.burn + + ^{:doc {:description "Burning NFTs."}} + + [] + + (T/group '((T/path.conj 'burn) + + (T/fail.code #{:TRUST} + '(call nft + (burn #{-1})) + {:description "Cannot burn inexistant NFT."}) + + (T/trx '(long? (def piece + (call nft + (create)))) + {:description "NFT created."}) + + (T/trx '(= #{piece} + (call nft + (burn #{piece}))) + {:description "NFT burned."}) + + (T/fail.code #{:TRUST} + '(call nft + (burn #{piece})) + {:description "Impossible to double-burn."}) + + (T/trx '(= #{} + (asset/balance nft + *address*)) + {:description "Balance is empty after burning."}) + + (T/trx '(not (asset/owns? *address* + [nft + #{piece} ])) + {:description "Cannot own burned NFT."})))) + + +;;; + + +(defn suite.main + + ^{:doc {:description "Main suite gathering all other suites."}} + + [] + + (T/group '((T/path.conj 'asset.simple-nft) + (suite.asset) + (suite.burn)))) + + +;;; + + +(T/report.clear) +(suite.main) +(T/print "suite.main") diff --git a/convex-core/src/test/cvx/test/asset/nft/tokens.cvx b/convex-core/src/test/cvx/test/asset/nft/tokens.cvx new file mode 100644 index 000000000..9385203e7 --- /dev/null +++ b/convex-core/src/test/cvx/test/asset/nft/tokens.cvx @@ -0,0 +1,1042 @@ +;; +;; +;; Testing the `asset.nft-tokens`. +;; +;; + + +;;;;;;;;;; Setup + + +($.stream/out! (str $.term/clear.screen + "Testing `asset.nft.tokens`")) + + +;; Deploying test version of `asset.simple-nft` instead of importing stable version. +;; +($.file/read "src/main/cvx/asset/nft/tokens.cvx") + + +(eval `(def nft + (deploy (quote ~(cons 'do + (next $/*result*)))))) + + +($.file/exec "src/test/cvx/test/convex/asset/quantity/set-long.cvx") + + + +;; Importing stable versions +;; +(import convex.asset :as asset) + +(def T + $.test) + + +;; Default account is an actor, key is set to transform it into a user account. +;; +(set-key $.account/fake-key) + + +;;;;;;;;;; Reusable + + +(defn create.check + + ^{:doc {:description "Checks generic facts about `token`, a newly created token."}} + + [prepare-trx-pop] + + (T/group '((T/path.conj 'create.check) + + (T/trx '(asset/owns? *address* + [nft + #{token}]) + {:description "Creater owns token."}) + + (T/trx '(= #{token} + (asset/balance nft + *address*)) + {:description "Token is in creator balance."}) + + (T/trx '(= *address* + (call nft + (get-token-creator token))) + {:description "Creator is retrieved."}) + + (T/trx '(= *address* + (call nft + (get-token-owner token))) + {:description "Initially, creator is owner."}) + + (T/trx '(= data + (call nft + (get-token-data token))) + {:description "Token data is retrieved."}) + + (T/trx '(call nft + (check-trusted? *address* + :destroy + token)) + {:description "Can destroy token."}) + + (T/trx '(call nft + (check-trusted? *address* + :transfer + token)) + {:description "Can transfer token."}) + + (T/trx '(call nft + (check-trusted? *address* + :update + token)) + {:description "Can update token."}) + + (T/trx '(call nft + (check-trusted? *address* + [:update :name] + token)) + {:description "Can update token name."}) + + (def data-new + (assoc data + :foo + :bar)) + + (T/trx '(= data-new + (call nft + (merge-token-data token + {:foo :bar}))) + {:description "Can merge data to token."}) + + (T/trx '(= data-new + (call nft + (get-token-data token))) + {:description "Data merged permanently."}) + + (T/trx '(= 42 + (call nft + (set-token-data token + 42))) + {:description "Can replace token data."}) + + (T/trx '(= 42 + (call nft + (get-token-data token))) + {:description "Data replaced permanently."}) + + (T/trx '(nil? (call nft + (check-transfer *address* + nil + token))) + {:description "No transfer issued."})) + prepare-trx-pop)) + + + +(defn destroy.check + + ^{:doc {:description "Destroys token interned as `token` and checks generic facts."}} + + [prepare-trx-pop] + + (T/group '((T/path.conj 'destroy.check) + + (T/trx '(= "No right to transfer token -1" + (call nft + (check-transfer *address* + nil + -1))) + {:description "Cannot check transfer on inexistent token."}) + + (T/trx '(call nft + (destroy-token token)) + {:description "Token destroyed."}) + + (T/fail.code #{:ASSERT} + '(call nft + (destroy-token -1)) + {:description "Cannot destroy inexistent token."}) + + (T/trx '(not (asset/owns? *address* + [nft + token])) + {:description "Cannot own destroyed token."}) + + (T/trx '(= #{} + (asset/balance nft + *address*)) + {:description "Balance is empty after destroying token."}) + + (T/trx '(nil? (get nft/token-records + token)) + {:description "Token record destroyed."})) + prepare-trx-pop)) + + + +(defn separate-offer-accept + + ^{:doc {:description "Tests transfer scenario where the offer is accepted separatelu from emission."}} + + [receiver] + + (T/group `((T/path.conj 'separate-offer-accept) + + (def receiver + ~receiver) + + (T/trx '(zero? (def token + (call nft + (create-token {:name "Token"} + nil)))) + {:description "Token created."}) + + (T/trx '(= {*address* {receiver #{token}}} + (asset/offer receiver + [nft + token])) + {:description "Send offer."}) + + (T/trx '(asset/owns? *address* + [nft + token]) + {:description "Still owner, offer not yet accepted."}) + + (T/trx '(not (asset/owns? receiver + [nft + token])) + {:description "Receiver not yet owner, has not accepted yet."}) + + (T/trx '(= [nft + #{token}] + (eval-as receiver + `(~asset/accept ~*address* + [~nft + ~token]))) + {:description "Receiver accepts offer."}) + + (T/trx '(not (asset/owns? *address* + [nft + token])) + {:description "Lost ownership after offer got accepted."}) + + (T/trx '(asset/owns? receiver + [nft + token]) + {:description "Receiver gained ownership after accepting."})))) + + + +(defn transfer.check + + ^{:doc {:description "Ensures basic facts are respected after a preliminary transfer between `sender` and `receiver`."}} + + + ([token] + + (transfer.check token + *address* + *address* + receiver)) + + + ([token creator sender receiver] + + (T/group `((T/path.conj 'transfer.check) + + (do + (def creator + ~creator) + (def receiver + ~receiver) + (def sender + ~sender) + (def token + ~token)) + + (T/trx `(not (asset/owns? ~sender + ~[nft + token])) + {:description "Lost ownership after transfer."}) + + (T/trx '(asset/owns? receiver + [nft + token]) + {:description "Receiver is now the owner."}) + + (T/trx '(not (subset? #{token} + (asset/balance nft + sender))) + {:description "Balance reflects lost ownership."}) + + (T/trx '(subset? #{token} + (asset/balance nft + receiver)) + {:description "Balance of receiver reflects ownership."}) + + (T/trx '(= creator + (call nft + (get-token-creator token))) + {:description "Creator did not change during transfer."}) + + (T/trx '(= receiver + (call nft + (get-token-owner token))) + {:description "Token owner adjusted."}) + + (T/trx '(not (call nft + (check-trusted? sender + :destroy + token))) + {:description "Lost ability to destroy."}) + + (T/trx '(not (call nft + (check-trusted? sender + :transfer + token))) + {:description "Lost ability to transfer."}) + + (T/trx '(not (call nft + (check-trusted? sender + :update + token))) + {:description "Lost ability to update."}) + + (T/trx '(not (call nft + (check-trusted? sender + [:update :name] + token))) + {:description "Lost ability to update name."}) + + (T/trx '(call nft + (check-trusted? receiver + :destroy + token)) + {:description "Receiver gained ability to destroy."}) + + (T/trx '(call nft + (check-trusted? receiver + :transfer + token)) + {:description "Receiver gained ability to transfer."}) + + (T/trx '(call nft + (check-trusted? receiver + :update + token)) + {:description "Receiver gained ability to update."}) + + (T/trx '(call nft + (check-trusted? receiver + [:update :name] + token)) + {:description "Receiver gained ability to update name."}))))) + + + +(defn transfer.do + + ^{:doc {:description "Performs a transfer to internet `receiver`."}} + + [load] + + (T/trx `(= [nft + ~(if (set? load) + load + #{load})] + (asset/transfer receiver + [nft + ~load])) + {:description "Token(s) transferred."})) + + + +(defn transfer.mono + + ^{:doc {:description "Tests scenarios of a single token transfer."}} + + [path-item make-account] + + (T/group `((T/path.conj (quote ~path-item)) + + (def receiver + ~make-account) + + (T/trx '(zero? (def token + (call nft + (create-token {:name "Token"} + nil)))) + {:description "First token created."}) + + (transfer.do token) + + (transfer.check token)))) + + +;;;;;;;;;; Test suites + + +(defn suite.class + + ^{:doc {:description "Tests creating a token with a class that logs all events."}} + + [] + + (T/group '((T/path.conj 'class) + + (def receiver + ($.account/zombie)) + + (def uri-logger + "http://www.logger.com") + + (def logger + (deploy + `(do + + (def nft + ~nft) + + (def token-history + {}) + + (defn check-trusted? + ^{:callable? true} + [addr policy-key id] + (if (= policy-key + :destroy) + (= (call nft + (get-token-creator id)) + addr) + (= (call nft + (get-token-owner id)) + addr))) + + (defn create-token + ^{:callable? true} + [caller id initial-data] + (def token-history + (assoc token-history + id + [{:caller caller + :data initial-data + :event :create-token}]))) + + (defn destroy-token + ^{:callable? true} + [caller id] + (def token-history + (dissoc token-history + id))) + + (defn get-token-history + ^{:callable? true} + [id] + (get token-history + id)) + + (defn get-uri + ^{:callable? true} + [id] + ~uri-logger) + + (defn merge-token-data + ^{:callable? true} + [caller id data] + (def token-history + (assoc token-history + id + (conj (get token-history + id) + {:caller caller + :data data + :event :merge-token-data})))) + + (defn set-token-data + ^{:callable? true} + [caller id data] + (def token-history + (assoc token-history + id + (conj (get token-history + id) + {:caller caller + :data data + :event :set-token-data})))) + + + (defn perform-transfer + ^{:callable? true} + [caller sender receiver id-set] + (def token-history + (reduce (fn [history id] + (assoc history id + (conj (get history + id) + {:caller caller + :event :transfer + :sender sender + :receiver receiver}))) + token-history + id-set)))))) + + (def data + {:name "Token" + :uri "https://www.mysite.com"}) + + (T/trx '(zero? (def token + (call nft + (create-token data + logger)))) + {:description "Token created."}) + + (T/trx '(= {:class logger + :creator *address* + :data data + :owner *address*} + (get nft/token-records + token)) + {:description "Accurate token record."}) + + (T/trx '(= logger + (call nft + (get-token-class token))) + {:description "Class reported."}) + + (create.check (fn [] + `(def history + ~(call logger + (get-token-history token))))) + + (T/trx `(= ~[{:caller *address* + :data data + :event :create-token} + {:caller *address* + :data {:foo :bar} ;; Done by `create.check` + :event :merge-token-data} + {:caller *address* + :data 42 ;; Done by `create.check` + :event :set-token-data}] + ~history) + {:description "History tracked by logger."}) + + (T/trx '(= [nft + #{token}] + (asset/transfer receiver + [nft + token])) + {:description "Token transferred."}) + + (destroy.check (fn [] + `(def history-2 + ~(call logger + (get-token-history token))))) + + (T/trx '(nil? history-2) + {:description "History removed at destruction."})))) + + + +(defn suite.class.restrict + + ^{:doc {:description "Tests creating a token with a class that limits the number of token transfer to 2."}} + + [] + + (T/group '((T/path.conj 'class.restrict) + + (def receiver + ($.account/zombie)) + + (def transfer-max-twice + (deploy + `(do + + (def nft + ~nft) + + (def transfer-count + {}) + + (defn check-transfer + ^{:callable? true} + [caller sender receiver id-set] + (reduce (fn [_ id] + (when (>= (get transfer-count + id) + 2) + (reduced (str "Token " id " has already been transferred twice")))) + nil + id-set)) + + (defn check-trusted? + ^{:callable? true} + [addr policy-key id] + (if (contains-key? #{:destroy + :transfer} + policy-key) + (= (call nft + (get-token-creator id)) + addr) + (= (call nft + (get-token-owner id)) + addr))) + + (defn create-token + ^{:callable? true} + [caller id initial-data] + (def transfer-count + (assoc transfer-count + id + 0))) + + (defn destroy-token + ^{:callable? true} + [caller id] + (def transfer-count + (dissoc transfer-count + id))) + + (defn perform-transfer + ^{:callable? true} + [caller sender receiver id-set] + (def transfer-count + (reduce (fn [tc id] + (assoc tc + id + (inc (get tc + id)))) + transfer-count + id-set)))))) + + (def data + {:name "Token" + :uri "https://www.mysite.com"}) + + (T/trx '(zero? (def token + (call nft + (create-token data + transfer-max-twice)))) + {:description "Token created."}) + + (T/trx '(= {:class transfer-max-twice + :creator *address* + :data data + :owner *address*} + (get nft/token-records + token)) + {:description "Accurate token record."}) + + (T/trx '(= transfer-max-twice + (call nft + (get-token-class token))) + {:description "Class reported."}) + + (create.check nil) + + (T/trx '(= [nft + #{token}] + (asset/transfer receiver + [nft + token])) + {:description "First transfer."}) + + (T/trx '(= [nft + #{token}] + (asset/transfer *address* + [nft + token])) + {:description "First transfer."}) + + + (T/fail.code #{:ASSERT} + '(= [nft + #{token}] + (asset/transfer receiver + [nft + token])) + {:description "Fail, already transferred twice."})))) + + + +(defn suite.policy + + ^{:doc {:description "Ensures token policies are respected."}} + + [] + + (T/group '((T/path.conj 'policy) + + (def receiver + ($.account/zombie)) + + (def someone + ($.account/zombie)) + + (T/trx '(zero? (def token + (call nft + (create-token {:name "Token" + :status false} + {:destroy :creator + :transfer :owner + :update :none + [:update :status] someone})))) + {:description "Created token with specific policies."}) + + (T/trx '(= [nft + #{token}] + (asset/transfer receiver + [nft + token])) + {:description "Token transferred."}) + + (T/trx '(call nft + (check-trusted? *address* + :destroy + token)) + {:description "Can still destroy in spite of losing ownership."}) + + (T/trx '(not (call nft + (check-trusted? receiver + :destroy + token))) + {:description "Receiver cannot destroy in spite of ownership."}) + + (T/trx '(not (call nft + (check-trusted? *address* + :transfer + token))) + {:description "Cannot transfer since not the owner."}) + + (T/trx '(call nft + (check-trusted? receiver + :transfer + token)) + {:description "New owner can transfer."}) + + (T/trx '(not (call nft + (check-trusted? *address* + :update + token))) + {:description "Cannot update."}) + + (T/trx '(not (call nft + (check-trusted? receiver + :update + token))) + {:description "Receiver cannot update either."}) + + (T/trx '(not (call nft + (check-trusted? *address* + [:update :name] + token))) + {:description "Cannot update name."}) + + (T/trx '(not (call nft + (check-trusted? receiver + [:update :name] + token))) + {:description "Receiver cannot update name either."}) + + (T/trx '(not (call nft + (check-trusted? *address* + [:update :status] + token))) + {:description "Cannot update status."}) + + (T/trx '(not (call nft + (check-trusted? receiver + [:update :status] + token))) + {:description "Receiver cannot update status either."}) + + (T/trx '(call nft + (check-trusted? someone + [:update :status] + token)) + {:description "Someone else can update status as decided."}) + + (T/trx '(= {:name "Token" + :status true} + (eval-as someone + `(call ~nft + (merge-token-data ~token + {:status true})))) + {:description "Status changed."}) + + (T/trx '(= {:name "Token" + :status true} + (call nft + (get-token-data token))) + {:description "Status permanently updated."}) + + + (T/trx '(call nft + (destroy-token token)) + {:description "Token destroyed."}) + + (T/trx '(not (asset/owns? receiver + [nft + token])) + {:description "Receiver cannot own destroyed token."}) + + (T/trx '(not (asset/owns? *address* + [nft + token])) + {:description "Original owner cannot own destroyed token eiter."}) + + (T/trx '(empty? (asset/balance nft + *address*)) + {:description "Empty balance."}) + + (T/trx '(empty? (asset/balance nft + receiver)) + {:description "Receiver has empty balance after destruction."})))) + + + +(defn suite.self + + ^{:doc {:description "Ensures basic facts about a token, without any transfer or more sophisticated operations."}} + + [] + + (T/group '((T/path.conj 'self) + + (def uri + "https://www.mysite.com") + + (def data + {:name "Token-1" + :uri uri}) + + (T/trx '(zero? (def token + (call nft + (create-token data + nil)))) + {:description "Token created."}) + + (T/trx '(= {:creator *address* + :data data + :owner *address*} + (get nft/token-records + token)) + {:description "Accurate token record."}) + + (create.check nil) + + (T/trx '(nil? (call nft + (get-token-class token))) + {:description "No class associated with token."}) + + (T/trx '(= uri + (call nft + (get-uri token))) + {:description "URI of token is retrieved."}) + + (destroy.check nil)))) + + + +(defn suite.separate-offer-accept + + ^{:doc {:description "Tests transferring a token while separating offer and accepting the offer."}} + + [] + + (separate-offer-accept ($.account/zombie))) + + + +(defn suite.separate-offer-accept.actor + + ^{:doc {:description "Like `suite.separate-offer-accept` but with an actor."}} + + [] + + (separate-offer-accept (deploy `(set-controller ~*address*)))) + + + +(defn suite.transfer + + ^{:doc {:description "Applies `transfer.mono` to a user account."}} + + [] + + (transfer.mono 'transfer + '($.account/zombie))) + + + +(defn suite.transfer.actor + + ^{:doc {:description "Applies `transfer.mono` to an actor."}} + + [] + + (transfer.mono 'transfer.actor + `(deploy + '(defn receive-asset + ^{:callable? true} + [offer _data] + (~asset/accept *caller* + offer))))) + + + +(defn suite.transfer.multi + + ^{:doc {:description "Tests transferring several tokens."}} + + [] + + (T/group '((T/path.conj 'transfer.multi) + + (def receiver + ($.account/zombie)) + + (T/trx '(zero? (def token-1 + (call nft + (create-token {:name "Token 1" + :status false} + nil)))) + {:description "Token 1 created."}) + + + (T/trx '(= 1 + (def token-2 + (call nft + (create-token {:name "Token 2" + :status false} + nil)))) + {:description "Token 2 created."}) + + (T/trx '(= 2 + (def token-3 + (call nft + (create-token {:name "Token 3" + :status false} + nil)))) + {:description "Token 3 created."}) + + (T/trx '(= [nft + #{token-1 + token-2 + token-3}] + (asset/transfer receiver + [nft + #{token-1 + token-2 + token-3}])) + {:description "Partial transfer."}) + + (transfer.check token-1) + + (transfer.check token-2) + + (transfer.check token-3)))) + + + +(defn suite.transfer.multi.partial + + ^{:doc {:description "Like `suite.transfer.multi` but not all tokens are accepted."}} + + [] + + (T/group '((T/path.conj 'transfer.multi.partial) + + (def receiver + (deploy + '(do + + (import convex.asset :as asset) + + (defn receive-asset + ^{:callable? true} + [[addr id-set] data] + (asset/accept *caller* + [addr + (let [id-min (apply min + (vec id-set))] + #{id-min + (inc id-min)})]))))) + + (T/trx '(zero? (def token-1 + (call nft + (create-token {:name "Token 1" + :status false} + nil)))) + {:description "Token 1 created."}) + + + (T/trx '(= 1 + (def token-2 + (call nft + (create-token {:name "Token 2" + :status false} + nil)))) + {:description "Token 2 created."}) + + (T/trx '(= 2 + (def token-3 + (call nft + (create-token {:name "Token 3" + :status false} + nil)))) + {:description "Token 3 created."}) + + (T/trx '(= [nft + #{token-1 + token-2}] + (asset/transfer receiver + [nft + #{token-1 + token-2 + token-3}])) + {:description "Partial transfer."}) + + (transfer.check token-1) + + (transfer.check token-2) + + (transfer.check token-3 + *address* + receiver + *address*)))) + + +;;; + + +(defn suite.main + + ^{:doc {:description "Main suite gathering other suites."}} + + [] + + (T/group '((T/path.conj 'asset.nft-tokens) + (suite.class) + (suite.class.restrict) + (suite.policy) + (suite.quantity nft) ;; From `asset/quantity/set-long` test file. + (suite.self) + (suite.separate-offer-accept) + (suite.separate-offer-accept.actor) + (suite.transfer) + (suite.transfer.actor) + (suite.transfer) + (suite.transfer.multi) + (suite.transfer.multi.partial)))) + + +;;; + + +(T/report.clear) +(suite.main) +(T/print "asset.nft-tokens") +nil diff --git a/convex-core/src/test/cvx/test/convex/asset.cvx b/convex-core/src/test/cvx/test/convex/asset.cvx new file mode 100644 index 000000000..f26ea088c --- /dev/null +++ b/convex-core/src/test/cvx/test/convex/asset.cvx @@ -0,0 +1,167 @@ +;; +;; +;; Test suites meant to be executed in implementations of the `convex.asset` interface. +;; +;; Meant to be deployed as a library. +;; +;; + + +;;;;;;;;;; Setup + + +(import convex.run.test :as T) +(import convex.run.trx :as $.trx) + + +(defn setup + + ^{:doc {:description "Must be called before using any of test suites from this library."}} + + [] + + ($.trx/prepend '(import convex.asset :as asset))) + + +;;;;;;;;;; Test suites meant to be used for accounts that implements the `convex.asset` interface + + +(defn suite.balance + + ^{:doc {:description "Used internally for ensuring that balance is not nil nor quantity zero."}} + + [path quantity-zero balance] + + (T/group `((T/path.conj ['convex.asset.test + 'suite.balance + ~path]) + + (T/trx '(not (nil? ~balance)) + {:description "Balance is not nil."}) + + (T/trx '(not (= ~quantity-zero + ~balance)) + {:description "Balance is not empty"})))) + + + +(defn suite.user + + ^{:doc {:description "Used internally for testing user balance and ownership."}} + + [path token user known-balance] + + (T/group `((T/path.conj ['convex.asset.test + 'suite.user + ~path]) + + (T/trx '(= ~known-balance + (asset/balance ~token + ~user)) + {:description "Balance is as expected."}) + + (T/trx '(= ~known-balance + (asset/quantity-add ~token + ~known-balance + nil) + (asset/quantity-add ~token + nil + ~known-balance)) + {:description "Adding nothing to a quantity does not change the quantity."}) + + (T/trx '(= (asset/quantity-zero ~token) + (asset/quantity-sub ~token + ~known-balance + ~known-balance)) + {:description "Subtracting a quantity from itself equals the empty value."}) + + (T/trx '(asset/owns? ~user + [~token + ~known-balance]) + {:description "User owns what is reported by `balance`."}) + + (T/trx '(asset/owns? ~user + [~token + nil]) + {:description "User owns at least \"nothing\"."})))) + + +;;; + + +(defn suite.main + + ^{:doc {:description ["Main suite which gathers all other suites from this library." + "Together, both given user owns total supply."]}} + + [token user-1 user-2] + + (T/group `((T/path.conj '[convex.asset.test + suite.main]) + (def tester + ($.account/zombie)) + + (def quantity-zero + (asset/quantity-zero ~token)) + + (T/trx '(= quantity-zero + (asset/balance ~token + tester)) + {:description "Initially, tester balance is empty."}) + + (def balance-1 + (asset/balance ~token + ~user-1)) + + (~suite.balance "user-1" + quantity-zero + balance-1) + + (def balance-2 + (asset/balance ~token + ~user-2)) + + (~suite.balance "user-2" + quantity-zero + balance-2) + + (def supply + (asset/quantity-add ~token + balance-1 + balance-2)) + + (~suite.balance "supply" + quantity-zero + supply) + + (~suite.user "user-1" + ~token + ~user-1 + balance-1) + + (~suite.user "user-2" + ~token + ~user-2 + balance-2) + + (eval-as ~user-1 + (list asset/transfer + tester + [~token + balance-1])) + + (T/trx '(= quantity-zero + (asset/balance ~token + ~user-1)) + {:description "Balance of user 1 has been emptied."}) + + (eval-as ~user-2 + (list asset/transfer + tester + [~token + balance-2])) + + (T/trx '(= supply + (asset/balance ~token + tester)) + {:description "Tester has whole supply."})))) diff --git a/convex-core/src/test/cvx/test/convex/asset/quantity/set-long.cvx b/convex-core/src/test/cvx/test/convex/asset/quantity/set-long.cvx new file mode 100644 index 000000000..6998f0f71 --- /dev/null +++ b/convex-core/src/test/cvx/test/convex/asset/quantity/set-long.cvx @@ -0,0 +1,59 @@ +(defn suite.quantity + + ^{:doc {:description ["Asset quantity tests for when quantities are long, which is quite common." + "Assumes `convex.asset` is imported as `asset`."] + :signature [{:params [token]}]}} + + [token] + + (T/group `((T/path.conj "convex/asset/quantity/set-long") + + (def token + ~token) + + (T/trx '(= #{} + (asset/quantity-zero token))) + + (T/trx '(= #{1 2 3 4} + (asset/quantity-add token + #{1 2} + #{3 4}))) + (T/trx '(= #{1 2 3 4} + (asset/quantity-add token + #{1 2 3} + #{2 3 4}))) + + (T/trx '(= #{1 2} + (asset/quantity-sub token + #{1 2 3} + #{3 4 5}))) + (T/trx '(= #{1 2} + (asset/quantity-add token + #{1 2} + nil))) + (T/trx '(= #{1 2} + (asset/quantity-add token + nil + #{1 2}))) + + (T/trx '(= #{1 2} + (asset/quantity-sub token + #{1 2} + nil))) + + (T/trx '(= #{} + (asset/quantity-sub token + nil + #{1 2}))) + + (T/trx '(asset/quantity-contains? token + #{1 2 3} + #{2 3})) + + (T/trx '(not (asset/quantity-contains? token + #{1 2 3} + #{3 4}))) + + (T/trx '(not (asset/quantity-contains? token + #{1 2 3} + #{4 5 6})))))) diff --git a/convex-core/src/test/cvx/test/convex/fungible.cvx b/convex-core/src/test/cvx/test/convex/fungible.cvx new file mode 100644 index 000000000..dba458538 --- /dev/null +++ b/convex-core/src/test/cvx/test/convex/fungible.cvx @@ -0,0 +1,509 @@ +;; +;; +;; Testing `convex.fungible`. +;; +;; + + +;;;;;;;;;; Setup + + +($.stream/out! (str $.term/clear.screen + "Testing `convex.fungible`")) + + +;; Deploying test version of `convex.fungible` instead of importing stable version. +;; +($.file/read "src/main/cvx/convex/fungible.cvx") + + +(eval `(def fungible + (deploy (quote ~(cons 'do + (next $/*result*)))))) + +($.file/exec "src/test/cvx/test/convex/fungible/generic.cvx") + + +;; Requiring test suites for `convex.asset` since fungible tokens implement that interface. +;; +($.file/read "src/test/cvx/test/convex/asset.cvx") + + +(def asset.test + (deploy (cons 'do + $/*result*))) + + +(asset.test/setup) + + +;; Other imports. +;; +(import convex.asset :as asset) + + +(def T + $.test) + + +;; Default account is an actor, key is set to transform it into a user account. +;; +(set-key $.account/fake-key) + + +;;;;;;;;;; Test suites + + +(defn suite.asset + + ^{:doc {:description "Testing that the library follows the `convex.asset` interface."}} + + [] + + (T/group `((T/path.conj 'suite.asset) + + (def supply + 1000000) + + (T/trx '(address? (def token + (deploy (fungible/build-token {:supply supply})))) + {:description "Token deployed"}) + + (suite.fungible token + supply + *address*) + + ;; Transfers + + (def receiver + ($.account/zombie)) + + (T/trx '(= 500 + (asset/offer receiver + [token + 500])) + {:description "Offer receiver some tokens."}) + + (T/trx '(= 500 + (asset/get-offer token + *address* + receiver)) + {:description "First offer has been issued."}) + + (T/trx '(= 1000 + (asset/offer receiver + [token + 1000])) + {:description "Updating offer."}) + + (T/trx '(= 1000 + (asset/get-offer token + *address* + receiver)) + {:description "Offer has been updated."}) + + (T/trx '(= 250 + (eval-as receiver + `(~asset/accept ~*address* + [~token + 250]))) + {:description "Partly accepting an offer, returns current balance."}) + + (T/trx '(= 750 + (asset/get-offer token + *address* + receiver)) + {:description "Remaining offer is still valid."}) + + (T/trx '(= 1000 + (eval-as receiver + `(~asset/accept ~*address* + [~token + 750]))) + {:description "Accepting remaining offer, returns current balance."}) + + (T/trx '(= 2000 + (asset/transfer receiver + [token + 1000])) + {:description "Transfer some tokens to receiver (arity 2), returns current balance."}) + + (T/trx '(= {token 3000} + (asset/transfer receiver + {token 1000})) + {:description "Transfer some tokens to receiver (arity 3), returns current balance."}) + + (T/trx '(= (- supply + 3000) + (asset/balance token + *address*)) + {:description "Villain received tokens."}) + + (T/trx '(= 3000 + (asset/balance token + receiver)) + {:description "Balance updated after transferring to receiver."}) + + + ;; Ownership + + (def balance-receiver + (asset/balance token + receiver)) + + (T/trx '(asset/owns? receiver + [token + (dec balance-receiver)]) + {:description "Villain owns at least less than its balance."}) + + (T/trx '(asset/owns? receiver + [token + balance-receiver]) + {:description "Villain owns its balance."}) + + (T/trx '(not (asset/owns? receiver + [token + (inc balance-receiver)])) + {:description "Villain cannot own more than its balance."}) + + + ;; Token arithmetics + + (T/trx '(= 0 + (asset/quantity-zero token)) + {:description "Empty balance is 0."}) + + (T/trx '(= 110 + (asset/quantity-add token + 100 + 10)) + {:description "Adding tokens."}) + + (T/trx '(= 100 + (asset/quantity-add token + 100 + nil)) + {:description "Adding nil."}) + + (T/trx '(= 100 + (asset/quantity-add token + nil + 100)) + {:description "Adding to nil."}) + + (T/trx '(= 90 + (asset/quantity-sub token + 100 + 10)) + {:description "Subtracting tokens."}) + + (T/trx '(= 100 + (asset/quantity-sub token + 100 + nil)) + {:description "Subtracting nil."}) + + (T/trx '(zero? (asset/quantity-sub token + nil + 100)) + {:description "Subtracting from nil."}) + + (T/trx '(= 0 + (asset/quantity-sub token + 10 + 1000)) + {:description "Subtracting does not go below 0."}) + + + ;; Token comparisons + + (T/trx '(asset/quantity-contains? [token + 110] + [token + 100])) + + (T/trx '(asset/quantity-contains? [token + 110] + nil)) + + (T/trx '(asset/quantity-contains? nil + nil)) + + (T/trx '(asset/quantity-contains? token + 110 + 100)) + + (T/trx '(asset/quantity-contains? token + 110 + nil)) + + (T/trx '(asset/quantity-contains? token + nil + nil)) + + (T/trx '(not (asset/quantity-contains? [token + 100] + [token + 110]))) + + (T/trx '(not (asset/quantity-contains? nil + [token + 110]))) + + (T/trx '(not (asset/quantity-contains? token + 100 + 110))) + + (T/trx '(not (asset/quantity-contains? token + nil + 110)))))) + + + +(defn suite.build-token + + ^{:doc {:description "Builds a token and ensures good fungability behavior."}} + + [] + + (T/group '((T/path.conj 'suite.build-token) + + (T/trx '(address? (def token + (deploy (fungible/build-token {})))) + {:description "Token deployed"}) + + (def receiver + ($.account/zombie)) + + (def supply + (fungible/balance token + *address*)) + + (def amount + (long (/ supply + 10))) + + (T/trx '(= amount + (fungible/transfer token + receiver + amount)) + {:description "Transferring tokens to receiver."}) + + (T/trx '(= (- supply + amount) + (fungible/balance token + *address*)) + {:description "Balance of sender updated after transfer."}) + + (T/trx '(= amount + (fungible/balance token + receiver)) + {:description "Balance of receiver updated after transfer."}) + + (T/fail.code #{:ASSERT} + '(fungible/transfer token + receiver + -1) + {:description "Cannot transfer negative amount."}) + + (T/fail.code #{:ASSERT} + '(fungible/transfer token + receiver + (inc (fungible/balance token + *address*))) + {:description "Cannot transfer more than hold."}) + + (T/group '((T/trx '(= supply + (fungible/transfer token + receiver + (fungible/balance token + *address*))) + {:description "Transferring all tokens to receiver."}) + + (T/trx '(= 0 + (fungible/balance token + *address*)) + {:description "Sender has transferred all owned tokens."}) + + (T/trx '(= supply + (fungible/balance token + receiver)) + {:description "Receiver holds whole supply."}) + ))))) + + + +(defn suite.mint + + ^{:doc {:description "Builds a mintable token and ensures minting rules are enforced."}} + + [] + + (T/group `((T/path.conj 'suite.mint) + + (def receiver + ($.account/zombie)) + + (def supply.init + 100) + + (def supply.max + 1000) + + (def supply.mintable + (- supply.max + supply.init)) + + (T/trx `(boolean (def token + (deploy '(do + ~(fungible/build-token {:supply supply.init}) + ~(fungible/add-mint {:max-supply supply.max}))))) + {:description "Token deployed."}) + + (suite.fungible token + supply.init + *address*) + + ;; Minting + + (T/fail.code #{:ASSERT} + '(fungible/mint token (inc supply.mintable)) + {:description "Cannot mint more than max supply."}) + + (T/trx '(= supply.max + (fungible/mint token + supply.mintable)) + {:description "Minting and augmenting total supply."}) + + (T/trx '(= supply.max + (fungible/balance token + *address*)) + {:description "Minted tokens added to balance."}) + + (T/group `((T/path.conj :negative-minting) + + (T/trx '(= supply.init + (fungible/mint token + (- supply.mintable))) + {:description "Negative minting and reducing total supply."}) + + (T/trx '(= supply.init + (fungible/balance token + *address*)) + {:description "Balance adjusted after negative minting."}) + + (T/trx '(= 0 + (fungible/mint token + (- supply.init))) + {:description "Negative mint down to 0."}) + + (T/fail.code #{:ASSERT} + '(fungible/mint token + -1) + {:description "Cannot negative mint more than currently hold."}))) + + (T/group `((T/path.conj :burning) + + (T/trx '(= supply.init + (fungible/burn token + supply.mintable)) + {:description "Burning and reducing total supply."}) + + (T/trx '(= supply.init + (fungible/balance token + *address*)) + {:description "Balance adjusted after burning."}) + + (T/trx '(= 0 + (fungible/burn token + supply.init)) + {:description "Burning down to 0."}) + + (T/fail.code #{:ASSERT} + '(fungible/burn token + 1) + {:description "Cannot burn more than currently hold."}))) + (def supply.receiver + (long (/ supply.max + 5))) + + (T/trx '(= supply.receiver + (fungible/transfer token + receiver + supply.receiver)) + {:description "Transferring part of supply to receiver."}) + + (T/trx '(= supply.receiver + (fungible/balance token + receiver)) + {:description "Balance of receiver updated after transfer."}) + + (T/trx '(= (- supply.max + supply.receiver) + (fungible/burn token + supply.receiver)) + {:description "Can still burn hold tokens."}) + + (T/fail.code #{:ASSERT} + '(fungible/burn token + (inc (fungible/balance token + *address*))) + {:description "Cannot burn more than currently holding."}) + + (T/fail.code #{:ASSERT} + '(fungible/mint token + (inc supply.receiver)) + {:description "After transfer, still Cannot mint more than max supply."}) + + (T/trx '(= supply.receiver + (query (fungible/burn token + (fungible/balance token + *address*)))) + {:description "Can burn all currently hold tokens."}) + + (T/fail.code #{:TRUST} + '(eval-as receiver + `(~fungible/mint ~token + 1)) + {:description "Non-owner cannot mint anything."}) + + (T/fail.code #{:TRUST} + '(eval-as receiver + `(~fungible/mint ~token + ~(inc supply.max))) + {:description "Trust verified before amount during minting."}) + + (T/fail.code #{:TRUST} + `(eval-as receiver + `(~fungible/burn ~token + 1)) + {:description "Non-owner cannot burn anything."}) + + (T/fail.code #{:TRUST} + '(eval-as receiver + `(~fungible/burn ~token + ~(inc supply.max))) + {:description "Trust verified before amount during minting."})))) + + + +;;; + + +(defn suite.main + + ^{:doc {:description "Main suite gathering all other test suites."}} + + [] + + (T/group '((T/path.conj 'convex.fungible) + (suite.asset) + (suite.build-token) + (suite.mint)))) + + +;;; + + +(suite.main) +(T/print "convex.fungible") diff --git a/convex-core/src/test/cvx/test/convex/fungible/generic.cvx b/convex-core/src/test/cvx/test/convex/fungible/generic.cvx new file mode 100644 index 000000000..45809417c --- /dev/null +++ b/convex-core/src/test/cvx/test/convex/fungible/generic.cvx @@ -0,0 +1,46 @@ +(defn suite.fungible + + ^{:doc {:description ["Reusable fungability tests with the `convex.asset` interface." + "Requires asset test suites account interned as `asset.test`."]}} + + [token supply user] + + (T/group `((T/path.conj 'suite.fungible) + + (T/trx '(= ~supply + (asset/balance ~token + ~user)) + {:description "Owns total supply."}) + + (T/trx '(= 0 + (asset/balance ~token + (deploy nil))) + {:description "Newly created account has no token."}) + + (T/trx '(do + (asset/transfer ~user + [~token + (asset/balance ~token + ~user)]) + (= ~supply + (asset/balance ~token + ~user))) + {:description "Self-transfer does not affect balance."}) + + (T/trx '(do + (asset/transfer ~user + [~token + nil]) + (= ~supply + (asset/balance ~token + ~user))) + {:description "Self-transfer of nothing does not affect balance."}) + + (asset.test/suite.main ~token + ~user + (let [user-2 ($.account/zombie)] + (asset/transfer user-2 + [~token + (long (/ ~supply + 3))]) + user-2))))) diff --git a/convex-core/src/test/cvx/test/convex/play.cvx b/convex-core/src/test/cvx/test/convex/play.cvx new file mode 100644 index 000000000..0c8dc6cef --- /dev/null +++ b/convex-core/src/test/cvx/test/convex/play.cvx @@ -0,0 +1,97 @@ +;; +;; +;; Testing `convex.play`. +;; +;; + + +;;;;;;;;;; Setup + + +($.stream/out! (str $.term/clear.screen + "Testing `convex.play`")) + + +($.file/read "src/main/cvx/convex/play.cvx") + + +(def play + (deploy (cons 'do + $/*result*))) + + +(def T + $.test) + + +;;;;;;;;;; Test suites + + +(defn suite.main + + ^{:doc {:description "Only test suite."}} + + [] + + (T/group '((T/path.conj 'convex.play) + + (def env + (deploy + `(do + + (def *log* + []) + + (defn command + ^{:callable? true} + [trx] + (when-not (= trx + 42) + (def *log* + (conj *log* + trx)))) + + (defn start + ^{:callable? true} + [] + (def *log* + [:join]))))) + + + (T/group '((T/path.conj "Actor returns nil") + + (play/start env) + 1 + 2 + 3 + 42 ;; Supposed to stop at 42 + + (T/trx '(= "Stop." + $/*result*) + {:description "Stops."}) + + (T/trx '(= [:join 1 2 3] + env/*log*) + {:description "Commands executed properly."}))) + + (T/group '((T/path.conj "Stops on user request") + + (play/start env) + :a + :b + stop + + (T/trx '(= "Stop." + $/*result*) + {:description "Stops."}) + + (T/trx '(= [:join :a :b] + env/*log*) + {:description "Commands executed properly."})))))) + + +;;; + + +(suite.main) +(T/print "convex.play") diff --git a/convex-core/src/test/cvx/test/convex/registry.cvx b/convex-core/src/test/cvx/test/convex/registry.cvx new file mode 100644 index 000000000..4d6c4b3d8 --- /dev/null +++ b/convex-core/src/test/cvx/test/convex/registry.cvx @@ -0,0 +1,153 @@ +;; +;; +;; Testing `convex.registry`. +;; +;; + + +;;;;;;;;;; Setup + + +($.file/read "src/main/cvx/convex/registry.cvx") + + +(def registry + (deploy (concat ['do] + $/*result* + `[(do + (def cns-database + {}) + (def trust + ~(call *registry* + (cns-resolve 'convex.trust))))]))) + + + +(def T + $.test) + + +;; Easier to interact with the Trust library if operating as a user account. +;; +(set-key $.account/fake-key) + + +;;;;;;;;;; Test suites + + +(defn suite.main + + ^{:doc {:description "Only test suite."}} + + [] + + (T/group '((T/path.conj 'convex.registry) + + (T/trx '(nil? (call registry + (cns-resolve 'some.name))) + {:description "Not found."}) + + (T/trx '(address? (def addr + (deploy `(~registry/meta.set 42)))) + {:description "Actor deployed with metadata."}) + + (T/trx '(= 42 + (registry/meta.get addr)) + {:description "Actor metadata retrieved."}) + + (T/trx `(= [addr + *address*] + (get (call registry + (cns-update 'some.name + addr)) + 'some.name)) + {:description "Registering address."}) + + (T/trx '(= addr + (call registry + (cns-resolve 'some.name))) + {:description "Address registered."}) + + (def addr-2 + ($.account/zombie)) + + + (T/trx '(= [addr-2 + *address*] + (get (call registry + (cns-update 'some.name + addr-2)) + 'some.name)) + {:description "Controller can update name."}) + + (def villain + ($.account/zombie)) + + (T/fail.code #{:TRUST} + `(eval-as villain + '(call ~registry + (cns-update 'some.name + *address*))) + {:description "Cannot update existing name unless controller."}) + + (T/trx '(= addr-2 + (call registry + (cns-resolve 'some.name))) + + {:description "Original mapping left intact."}) + + (T/trx '(= [addr-2 + villain] + (get (call registry + (cns-control 'some.name + villain)) + 'some.name)) + {:description "Control transfered."}) + + (T/fail.code #{:TRUST} + '(call registry + (cns-update 'some.name + addr)) + {:description "Cannot update after transferring control."}) + + (T/fail.code #{:TRUST} + '(call registry + (cns-control 'some.name + *address*)) + {:description "Cannot transfer control when control is lost."}) + + (T/trx '(= [addr + villain] + (get (eval-as villain + `(call ~registry + (cns-update 'some.name + ~addr))) + 'some.name)) + {:description "Villain can update after gaining control."}) + + (T/trx '(= addr + (call registry + (cns-resolve 'some.name))) + {:description "Villain updated."}) + + (T/trx '(= [addr-2 + villain] + (get (eval-as villain + `(call ~registry + (cns-update 'another.name + ~addr-2))) + 'another.name)) + {:description "Villain can create new mapping."}) + + (T/trx '(= addr-2 + (call registry + (cns-resolve 'another.name))) + {:description "Villain created new mapping."})))) + + + +;;; + + +(suite.main) +(T/print "convex.registry") diff --git a/convex-core/src/test/cvx/test/convex/trust.cvx b/convex-core/src/test/cvx/test/convex/trust.cvx new file mode 100644 index 000000000..210e3ccda --- /dev/null +++ b/convex-core/src/test/cvx/test/convex/trust.cvx @@ -0,0 +1,390 @@ +;; +;; +;; Testing `convex.trust`. +;; +;; + + +($.stream/out! (str $.term/clear.screen + "Testing `convex.trust`")) + + +;;;;;;;;;; Setup + + +(def T + $.test) + + +($.file/read "src/main/cvx/convex/trust.cvx") + + +(eval `(def trust + (deploy (quote ~(cons 'do + (next $/*result*)))))) + + +;; Default account is an actor, key is set to transform it into a user account. +;; +(set-key $.account/fake-key) + + +;;;;;;;;;; Test suites + + +(defn suite.blacklist + + ^{:doc {:description "Creates a blacklist and ensures access is managed accordingly."}} + + [] + + (T/group '((T/path.conj 'black-list) + + (T/trx '(address? (def bl + (deploy (trust/build-blacklist nil)))) + {:description "Blacklist deployed."}) + + (T/trx '(not (trust/trusted? bl + *address*)) + {:description "Controller is not trusted by default when no list is provided."}) + + (T/fail.code #{:CAST} + '(trust/trusted? bl + nil) + {:description "Cannot check nil."}) + + (T/fail.code #{:CAST} + '(trust/trusted? bl + []) + {:description "Cannot trust something that is not an address."}) + + (T/fail.code #{:CAST} + '(trust/trusted? nil + *address*) + {:description "List cannot be nil."}) + + (T/fail.code #{:CAST} + '(trust/trusted? [] + *address*) + {:description "List must be an address, obviously."}) + + (T/trx '(address? (def user-1 + ($.account/zombie))) + {:description "User 1 deployed."}) + + (T/trx '(trust/trusted? bl + user-1) + {:description "Cannot trust new account."}) + + (T/trx '(= #{*address* + user-1} + (call bl + (set-trusted user-1 + false))) + {:description "Removing trust for new account."}) + + (T/trx '(not (trust/trusted? bl + user-1)) + {:description "Excluded account is not trusted."}) + + (T/trx '(= #{*address*} + (call bl + (set-trusted user-1 + true))) + {:description "Restoring trust in new account."}) + + (T/trx '(trust/trusted? bl + user-1) + {:description "New account is trusted again."}) + + (T/trx '(address? (def user-2 + ($.account/zombie))) + {:description "User 2 deployed."}) + + (T/trx '(= #{*address* + user-2} + (call bl + (set-trusted user-2 + false))) + {:description "Removing trust in user 2."}) + + (T/trx '(eval-as user-1 + `(~trust/trusted? ~bl + ~user-1)) + {:description "Non-controller can check monitor for trusted address."}) + + (T/trx '(not (eval-as user-1 + `(~trust/trusted? ~bl + ~user-2))) + {:description "Non-controller can check monitor for untrusted address."}) + + (T/fail.code #{:TRUST} + '(eval-as user-1 + `(call ~bl + (set-trusted ~user-1 + false))) + {:description "Non-controller trying to remove trust for itself fails."}) + + (T/fail.code #{:TRUST} + '(eval-as user-1 + `(call ~bl + (set-trusted ~user-1 + true))) + {:description "Non-controller cannot grant trust in itself."}) + + (T/fail.code #{:TRUST} + '(eval-as user-1 + `(call ~bl + (set-trusted ~user-2 + true))) + {:description "Non-controller cannot grant trust to another account."}) + + (T/fail.code #{:TRUST} + '(eval-as user-1 + `(call ~bl + (set-trusted ~user-2 + false))) + {:description "Non-controller trying to remove trust for another account fails."}) + + (T/fail.code #{:TRUST} + '(eval-as user-1 + `(call ~bl + (set-trusted ~*address* + true))) + {:description "Non-controller cannot grant trust to trusted account."}) + + (T/fail.code #{:TRUST} + '(eval-as user-1 + `(call ~bl + (set-trusted ~*address* + false))) + {:description "Non-controller trying to remove trust for trusted account fails."})))) + + + +(defn suite.self-trust + + ^{:doc {:description "Basic truths about trust."}} + + [] + + (T/group '((T/path.conj 'self-trust) + + (T/trx '(trust/trusted? *address* + *address*) + {:description "Self trust."}) + + (T/trx '(not (trust/trusted? *address* + nil)) + {:description "Never trust \"nothing\"."}) + + (T/trx '(not (trust/trusted? *address* + :foo)) + {:description "Cannot trust something that is not an address."}) + + (T/trx '(and (nil? (account #666666)) + (not (trust/trusted? *address* + #666666))) + {:description "Cannot trust an inexistent address."})))) + + + +(defn suite.upgrade + + ^{:doc {:description "Adding and removing upgradability from an account (ie. ability to eval arbitrary code)."}} + + [] + + (T/group '((T/path.conj 'upgrade) + + (T/trx '(address? (def target + (deploy `(do + (def bar + 1) + ~(trust/add-trusted-upgrade nil))))) + {:description "Upgradable whiteliste deployed."}) + + (T/trx '(do + (call target + (upgrade '(def foo + 42))) + (= 42 + target/foo)) + {:description "Eval arbitrary code in list actor."}) + + (T/trx '(address? (def not-root + ($.account/zombie))) + {:description "Accoutn deployed."}) + + (T/fail.code #{:TRUST} + '(eval-as not-root + `(call ~target + (upgrade '(def foo + 100)))) + {:description "Non-root account cannot upgrade."}) + + (T/trx '(nil? (trust/remove-upgradability! target)) + {:description "Upgrade utilities removed (returns nil)."}) + + (T/fail.code #{:STATE} + '(call target + (updade '(def foo + 1001))) + {:description "Impossible to upgrade."}) + + (T/trx '(= 1 + target/bar) + {:description "Rest of environment is preserved after loosing upgradability."})))) + + + +(defn suite.whitelist + + ^{:doc {:description "Builds whitelist and ensures access is managed accordingly."}} + + [] + + (T/group '((T/path.conj 'white-list) + + (T/trx '(address? (def wl + (deploy (trust/build-whitelist nil)))) + {:description "Whitelist deployed."}) + + (T/trx '(trust/trusted? wl + *address*) + {:description "Controller is trusted by default when no list is provided."}) + + (T/fail.code #{:CAST} + '(trust/trusted? wl + nil) + {:description "Cannot check nil."}) + + (T/fail.code #{:CAST} + '(trust/trusted? wl + []) + {:description "Cannot trust something that is not an address."}) + + (T/fail.code #{:CAST} + '(trust/trusted? nil + *address*) + {:description "List cannot be nil."}) + + (T/fail.code #{:CAST} + '(trust/trusted? [] + *address*) + {:description "List must be an address, obviously."}) + + (T/trx '(address? (def user-1 + ($.account/zombie))) + {:description "User 1 deployed."}) + + (T/trx '(not (trust/trusted? wl + user-1)) + {:description "Cannot trust new account."}) + + (T/trx '(= #{*address* + user-1} + (call wl + (set-trusted user-1 + true))) + {:description "Trusting new account."}) + + (T/trx '(trust/trusted? wl + user-1) + {:description "New account is trusted."}) + + (T/trx '(= #{*address*} + (call wl + (set-trusted user-1 + false))) + {:description "Removing trust in new account."}) + + (T/trx '(not (trust/trusted? wl + user-1)) + {:description "New account is not trusted anymore."}) + + (T/trx '(address? (def user-2 + ($.account/zombie))) + {:description "User 2 deployed."}) + + (T/trx '(= #{*address* + user-2} + (call wl + (set-trusted user-2 + true))) + {:description "Trusting user 2."}) + + (T/trx '(not (eval-as user-1 + `(~trust/trusted? ~wl + ~user-1))) + {:description "Non-controller can check monitor for untrusted address."}) + + (T/trx '(eval-as user-1 + `(~trust/trusted? ~wl + ~user-2)) + {:description "Non-controller can check monitor for trusted address."}) + + (T/fail.code #{:TRUST} + '(eval-as user-1 + `(call ~wl + (set-trusted ~user-1 + true))) + {:description "Non-controller cannot grant trust to itself."}) + + (T/fail.code #{:TRUST} + '(eval-as user-1 + `(call ~wl + (set-trusted ~user-1 + false))) + {:description "Non-controller trying to remove trust for itself fails."}) + + (T/fail.code #{:TRUST} + '(eval-as user-1 + `(call ~wl + (set-trusted ~user-2 + true))) + {:description "Non-controller cannot grant trust to another account."}) + + (T/fail.code #{:TRUST} + '(eval-as user-1 + `(call ~wl + (set-trusted ~user-2 + false))) + {:description "Non-controller trying to remove trust for another account fails."}) + + (T/fail.code #{:TRUST} + '(eval-as user-1 + `(call ~wl + (set-trusted ~*address* + true))) + {:description "Non-controller cannot grant trust to trusted account."}) + + (T/fail.code #{:TRUST} + '(eval-as user-1 + `(call ~wl + (set-trusted ~*address* + false))) + {:description "Non-controller trying to remove trust for trusted account fails."})))) + + +;;; + + +(defn suite.main + + ^{:doc {:description "Main suite gathering all other suites."}} + + [] + + (T/group '((T/path.conj 'convex.trust) + (suite.blacklist) + (suite.self-trust) + (suite.upgrade) + (suite.whitelist)))) + + +;;; + + +(suite.main) +(T/print "convex.trust") diff --git a/convex-core/src/test/cvx/test/convex/trusted-oracle.cvx b/convex-core/src/test/cvx/test/convex/trusted-oracle.cvx new file mode 100644 index 000000000..5c85ea5e7 --- /dev/null +++ b/convex-core/src/test/cvx/test/convex/trusted-oracle.cvx @@ -0,0 +1,109 @@ +;; +;; +;; Testing `convex.trusted-oracle`. +;; +;; + + +;;;;;;;;;; Setup:w + + +($.file/read "src/main/cvx/convex/trusted-oracle/actor.cvx") + + +(def default-actor + (deploy (cons 'do + (next $/*result*)))) + + +($.file/read "src/main/cvx/convex/trusted-oracle.cvx") + + +(def oracle + (deploy (concat (cons 'do + (next $/*result*)) + `((def default-actor + ~default-actor))))) + + +(def T + $.test) + + +;;;;;;;;;; Test suites + + +(defn suite.main + + ^{:doc {:description "Only test suite."}} + + [] + + (T/group '((T/path.conj 'convex.trusted-oracle) + + (T/trx '(oracle/register :foo + {:a :b + :trust #{*address*}}) + {:description "Registering oracle."}) + + (T/trx '(= false + (oracle/register :foo + {:bar :baz})) + {:description "Cannot register same key twice."}) + + (T/trx '(= {:a :b + :trust #{*address*}} + (oracle/data :foo)) + {:description "Overwrite did not work."}) + + (T/trx '(= false + (oracle/finalized? :foo)) + {:description "Not yet finalized."}) + + (T/trx '(nil? (oracle/read :foo)) + {:description "Nothing to read, not yet finalized."}) + + (def villain + ($.account/zombie)) + + (T/fail.code #{:TRUST} + '(eval-as villain + `(~oracle/provide :foo + :bar)) + {:description "Villain is not trusted for providing a value."}) + + (T/trx '(nil? (oracle/read :foo)) + {:description "Vilain did not manage to provide a value."}) + + (T/trx '(= :baz + (oracle/provide :foo + :baz)) + {:description "Trusted account provides value."}) + + (T/trx '(oracle/finalized? :foo) + {:description "Key is finalized."}) + + (T/trx '(= :baz + (oracle/read :foo)) + {:description "Finalized key is read."}) + + (T/trx '(= :baz + (oracle/provide :foo + 42)) + {:description "Cannot overwrite value."}) + + (T/trx '(= :baz + (oracle/read :foo)) + {:description "Value was not overwritten."}) + + (T/fail.code #{:STATE} + '(oracle/provide :bar + 42) + {:description "Cannot provide a result for an inexistent key."})))) + + +;;; + + +(suite.main) +(T/print "convex.trusted-oracle") diff --git a/convex-core/src/test/cvx/test/torus/exchange.cvx b/convex-core/src/test/cvx/test/torus/exchange.cvx new file mode 100644 index 000000000..aa8cc3276 --- /dev/null +++ b/convex-core/src/test/cvx/test/torus/exchange.cvx @@ -0,0 +1,499 @@ +;; +;; +;; Testing `torus.exchange`. +;; +;; + + +;;;;;;;;;; Setup + + +($.file/read "src/main/cvx/torus/exchange.cvx") + + +(def torus + (deploy (cons 'do + (next $/*result*)))) + + +(def T + $.test) + + +(import convex.asset :as asset) +(import convex.fungible :as fungible) + + +($.stream/out! (str $.term/clear.screen + "Testing `torus.exchange`")) + + +;; Generic fungiblity suites. +;; +($.file/exec "src/test/cvx/test/convex/fungible/generic.cvx") + + +;; Requiring test suites for `convex.asset` as required for generic fungibility tests. +;; +($.file/read "src/test/cvx/test/convex/asset.cvx") + + +(def asset.test + (deploy (cons 'do + $/*result*))) + + +(asset.test/setup) + + +;; Easier to buy/sell as a user account, no need to implement `receive-coin`. +;; +(set-key $.account/fake-key) + + +;;;;;;;;;; Deploying currencies - 1e6 each with 2 decimal places + + +(def GBP.token + (deploy (fungible/build-token {:supply 1000000000}))) + + +(def USD.token + (deploy (fungible/build-token {:supply 1000000000}))) + + +(def USD.market + (call torus + (create-market USD.token))) + + +;; Only market for USD at the moment. + + +;;;;;;;;;; Test suites + + +(defn suite.api + + ^{:doc {:description "Testing core API functions."}} + + [] + + (T/group '((T/path.conj 'api) + + (T/trx '(address? (def GBP.market + (call torus + (create-market GBP.token)))) + {:description "Create market for GBP."}) + + (T/trx '(= GBP.market + (torus/get-market GBP.token)) + {:description "Retrieve market for GBP."}) + + (T/trx '(= USD.market + (torus/get-market USD.token)) + {:description "Retrieve market for USD."}) + + (T/trx '(nil? (torus/price GBP.token)) + {:description "No price for GBP yet, market exists but no liquidity."}) + + (T/trx '(nil? (torus/price USD.token)) + {:description "No price for USD yet, market exists but no liquidity."}) + + ;; Liquidity + + (T/trx '(<= (- 1.0 + 0.00001) + (/ (sqrt (* 5000000.0 + 1000000000000.0)) + (torus/add-liquidity GBP.token + 5000000 + 1000000000000)) + (+ 1.0 + 0.00001)) + {:description "Initial deposit of 50K GBP token liquidity and checking result."}) + + (T/trx '(<= (- 1.0 + 0.00001) + (/ (sqrt (* 10000000.0 + 1000000000000.0)) + (torus/add-liquidity USD.token + 10000000 + 1000000000000)) + (+ 1.0 + 0.00001)) + {:description "Initial deposit of 100K USD token liquidity and checking result."}) + + ;; Prices + + (T/trx '(= 200000.0 + (torus/price GBP.token)) + {:description "50 GBP token for 1e12 CVX Gold = 2e5 CVX / Penny sterling."}) + + (T/trx '(= 100000.0 + (torus/price USD.token)) + {:description "100K USD token for 1e12 CVX Gold = 1e5 CVX / US Cent."}) + + (T/trx '(= 1.0 + (torus/price GBP.token + GBP.token)) + {:description "Rate GBP / GBP."}) + + (T/trx '(= 2.0 + (torus/price GBP.token + USD.token)) + {:description "Rate GBP / USD."}) + + (T/trx '(= 0.5 + (torus/price USD.token + GBP.token)) + {:description "Rate USD / GBP."}) + + ;; Marginal buy trades for 1 GBP / 1 USD + + (T/trx '(= 101 + (torus/buy GBP.token + 100 + GBP.token))) + + (T/trx '(= 101 + (torus/buy-quote GBP.token + 100 + GBP.token))) + + (T/trx '(= 51 + (torus/buy USD.token + 100 + GBP.token))) + + (T/trx '(= 51 + (torus/buy-quote USD.token + 100 + GBP.token))) + + (T/trx '(= 201 + (torus/buy GBP.token + 100 + USD.token))) + + (T/trx '(= 201 + (torus/buy-quote GBP.token + 100 + USD.token))) + + ;; Marginal sell trades for 1 GBD / 1 USD + + (T/trx '(= 99 + (torus/sell GBP.token + 100 + GBP.token))) + + (T/trx '(= 99 + (torus/sell-quote GBP.token + 100 + GBP.token))) + + (T/trx '(= 49 + (torus/sell USD.token + 100 + GBP.token))) + + (T/trx '(= 49 + (torus/sell-quote USD.token + 100 + GBP.token))) + + (T/trx '(= 199 + (torus/sell GBP.token + 100 + USD.token))) + + (T/trx '(= 199 + (torus/sell-quote GBP.token + 100 + USD.token))) + + ;; Failures + ;; TODO. Review, errors in original Java tests were actually failing only because of arity exceptions. + + (T/fail.code #{:LIQUIDITY} + '(torus/buy USD.token + 1000000000000000 + USD.token) + {:description "Trade too big, not enough liquidity."})))) + + + +(defn suite.deployed-currencies + + ^{:doc {:description "Testing already deployed currencies."}} + + [] + + (T/group '((T/path.conj 'deployed-currencies) + + (import currency.GBP :as GBP.default) + (import currency.USD :as USD.default) + (import torus.exchange :as torus-old) + + ;; TODO. Missing `double?` in core. See https://github.com/Convex-Dev/convex/issues/92 + ;; + (T/trx '(number? (torus-old/price GBP.default + USD.default)) + {:description "Price of USD per GBP token."})))) + + + +(defn suite.initial-token-market + + ^{:doc {:description "Testing common operations (adding liquidity, buy, sell, ...) on a new token market."}} + + [] + + (T/group '((T/path.conj 'initial-token-market) + + (T/trx '(nil? (call USD.market + (price))) + {:description "No price since zero liquidity."}) + + (T/trx '(= 20000000 + (asset/offer USD.market + [USD.token + 20000000])) + {:description "Initial token offering for market (200K USD)."}) + + (T/trx '(= 20000000 + (asset/get-offer USD.token + *address* + USD.market)) + {:description "Accept initial offer."}) + + ;; Adding liquidity. + + (T/trx '(do + (def shares.initial + (call USD.market + 1000000000000 + (add-liquidity 10000000))) + true) + {:description "Initial deposit of 100K USD for 1000 CVX Gold."}) + + (T/trx '(= 10000000 + (asset/balance USD.token + USD.market)) + {:description "Market has balance of 100K USD."}) + + (T/trx '(= 1000000000000 + (balance USD.market)) + {:description "Market has CVX balance of 1000 Gold."}) + + (T/trx '(= shares.initial + (asset/balance USD.market + *address*)) + {:description "Initial pool shares, accessible as a fungible asset balance."}) + + (T/trx '(= 10000000 + (asset/get-offer USD.token + *address* + USD.market)) + {:description "Consumed half the full offer of tokens."}) + + (T/trx '(= 100000.0 + (call USD.market + (price))) + {:description "Price should be 100000 CVX / US Cent."}) + + ;; More liquidity. + + (T/trx '(do + (def shares.new + (call USD.market + 1000000000000 + (add-liquidity 10000000))) + true) + {:description "Initial deposit of 100K USD for 1000 CVX Gold."}) + + (T/trx '(= 20000000 + (asset/balance USD.token + USD.market)) + {:description "Market has balance of 100K USD."}) + + (T/trx '(= (+ shares.initial + shares.new) + (asset/balance USD.market + *address*)) + {:description "New pool shares, accessible as a fungible asset balance."}) + + (T/trx '(= 100000.0 + (call USD.market + (price))) + {:description "Price remains unchanged with new pool."}) + + (T/trx '(zero? (asset/get-offer USD.token + *address* + USD.market)) + {:description "Whole offer consumed."}) + + ;; Withdraw half of liquidity + + (def balance-before-withdrawal + *balance*) + + (T/trx '(= shares.new + (call USD.market + (withdraw-liquidity shares.new))) + {:description "Withdrawing half of liquidify."}) + + (T/trx '(= shares.initial + (asset/balance USD.market + *address*)) + {:description "Remaining liquidity."}) + + (T/trx '(= 10000000 + (asset/balance USD.token + USD.market))) + + (T/trx '(= 990000000 + (asset/balance USD.token + *address*))) + + (T/trx '(> *balance* + balance-before-withdrawal)) + + (T/trx '(= 1000000000000 + (balance USD.market)) + {:description "CVX balance of market back to start."}) + + ;; Generic fungible tests on shares + + (suite.fungible USD.market + shares.initial + *address*) + + ;; Buy half of all tokens (50K USD) + + (T/trx '(long? (def cvx.paid + (call USD.market + *balance* + (buy-tokens 5000000)))) + {:description "Buy half of all tokens."}) + + (T/trx '(> cvx.paid + 1000000000000) + {:description "Should cost more than pool CVX balance after fee."}) + + (T/trx '(< cvx.paid + 1100000000000) + {:description "Should cost less than 10% fee."}) + + (T/trx '(= 5000000 + (asset/balance USD.token + USD.market)) + {:description "Half tokens remaining in the market."}) + + (T/trx '(= 995000000 + (asset/balance USD.token + *address*))) + + (T/trx '(= shares.initial + (asset/balance USD.market + *address*))) + + ;; Sell back tokens (50K USD) + + (T/trx '(= 5000000 + (asset/offer USD.market + [USD.token + 5000000])) + {:description "Sell back tokens."}) + + (T/trx '(long? (def cvx.gained + (call USD.market + (sell-tokens 5000000))))) + + (T/trx '(> cvx.gained + 900000000000) + {:description "Should gain most of money bach."}) + + (T/trx '(< cvx.gained + cvx.paid) + {:description "Gain less than cost because of fees."}) + + (T/trx '(= 10000000 + (asset/balance USD.token + USD.market))) + + (T/trx '(= 990000000 + (asset/balance USD.token + *address*))) + + (T/trx '(= shares.initial + (asset/balance USD.market + *address*))) + + ;; Withdraw all liquidity + + (T/trx '(long? (def shares.remaining + (asset/balance USD.market + *address*)))) + + (T/trx '(> shares.remaining + 0)) + + (T/trx '(= shares.remaining + (torus/withdraw-liquidity USD.token + shares.remaining)) + {:description "Withdraw all remaining shares."}) + + (T/trx '(zero? (asset/balance USD.market + *address*)) + {:description "No shares left."}) + + (T/trx '(zero? (asset/balance USD.token + USD.market)) + {:description "No USD tokens left in liquidity pool."}) + + (T/trx '(zero? (balance USD.market)) + {:description "No CVX left in liquidity pool."})))) + + + +(defn suite.missing-market + + ^{:doc {:description "Testing facts about markets which do not exist or have no liquidity."}} + + [] + + (T/group '((T/path.conj 'missing-market) + + (T/trx '(nil? (torus/get-market GBP.token)) + {:description "There is no market for GBP yet."}) + + (T/trx '(nil? (torus/price #4242424242)) + {:description "No price for a market that does not exist."}) + + (T/trx '(nil? (torus/price GBP.token)) + {:description "No price for a market that does not exist, even if token exists."})))) + + +;;; + + +(defn suite.main + + ^{:doc {:description "Main suite gathering all other suites."}} + + [] + + (T/group '((T/path.conj 'torus.exchange) + (suite.api) + (suite.deployed-currencies) + (suite.initial-token-market) + (suite.missing-market)))) + + +;;;;;;;;;; + + +(suite.main) +(T/print "torus.exchange") diff --git a/convex-core/src/test/java/convex/actors/ActorsTest.java b/convex-core/src/test/java/convex/actors/ActorsTest.java new file mode 100644 index 000000000..d9fc62895 --- /dev/null +++ b/convex-core/src/test/java/convex/actors/ActorsTest.java @@ -0,0 +1,256 @@ +package convex.actors; + +import static convex.core.lang.TestState.eval; +import static convex.core.lang.TestState.evalB; +import static convex.core.lang.TestState.evalL; +import static convex.core.lang.TestState.step; +import static convex.test.Assertions.assertArityError; +import static convex.test.Assertions.assertAssertError; +import static convex.test.Assertions.assertCVMEquals; +import static convex.test.Assertions.assertCastError; +import static convex.test.Assertions.assertFundsError; +import static convex.test.Assertions.assertStateError; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import convex.core.data.Address; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.init.InitTest; +import convex.core.lang.Context; +import convex.core.lang.Core; +import convex.core.lang.Symbols; +import convex.core.lang.TestState; +import convex.core.util.Utils; + +public class ActorsTest { + + @Test public void testDeployAndCall() { + Context ctx=TestState.step("(def caddr (deploy '(let [n 10] (defn getter ^{:callable? true} [] n) (defn hidden [] nil) (defn plus ^{:callable? true} [x] (+ n x)))))"); + + assertEquals(Address.class,ctx.getResult().getClass()); + + assertEquals(10L,evalL(ctx,"(call caddr (getter))")); + assertEquals(14L,evalL(ctx,"(call caddr (plus 4))")); + + assertFalse(evalB(ctx,"(callable? caddr 'foo)")); + assertTrue(evalB(ctx,"(callable? caddr 'getter)")); + + assertStateError(step(ctx,"(call caddr (bad-symbol 2))")); + assertStateError(step(ctx,"(call caddr (hidden 2))")); + assertArityError(step(ctx,"(call caddr (getter 2))")); + assertArityError(step(ctx,"(call caddr (plus))")); + assertFundsError(step(ctx,"(call caddr 1000000000000000000 (plus))")); + assertCastError(step(ctx,"(call caddr (plus :foo))")); + } + + @Test public void testSimpleDeploys() { + assertTrue(evalB("(address? (deploy 1))")); + } + + @Test public void testDeployFailures() { + assertArityError(step("(deploy)")); + assertArityError(step("(deploy 1 2)")); + + assertArityError(step("(deploy '(if))")); + } + + @Test public void testUserAsActor() { + Context ctx=step("(do (defn foo ^{:callable? true} [] *caller*) (defn bar [] nil) (def z 1))"); + assertEquals(InitTest.HERO,eval(ctx,"(call *address* (foo))")); + assertStateError(step(ctx,"(call *address* (non-existent-function))")); + assertStateError(step(ctx,"(call *address* (bar))")); + assertStateError(step(ctx,"(call *address* (z))")); + } + + @Test public void testNotActor() { + assertFalse(evalB("(actor? *address*)")); + assertFalse(evalB("(callable? *address* 'foo)")); + assertStateError(TestState.step("(call *address* (not-a-function))")); + } + + @Test public void testMinimalContract() { + Context ctx=TestState.step("(def caddr (deploy '(do)))"); + Address a=(Address) ctx.getResult(); + assertNotNull(a); + + assertFalse(evalB(ctx,"(callable? caddr 'foo)")); + + assertEquals(Core.COUNT,ctx.lookup(Symbols.COUNT).getValue()); + assertNull(ctx.getAccountStatus(a).getEnvironmentValue(Symbols.FOO)); + } + + @Test public void testTokenContract() throws IOException { + String VILLAIN=InitTest.VILLAIN.toHexString(); + String HERO=InitTest.HERO.toHexString(); + + // setup address for this scene + Context ctx=TestState.step("(do (def HERO (address \""+HERO+"\")) (def VILLAIN (address \""+VILLAIN+"\")))"); + + // Technique of constructing a contract using a String + String contractString=Utils.readResourceAsString("contracts/token.con"); + ctx=TestState.step(ctx,"(def my-token (deploy ("+contractString+" 101 1000 HERO)))"); // contract initialisation args + + assertEquals(1000L,evalL(ctx,"(call my-token (balance *address*))")); + assertEquals(0L,evalL(ctx,"(call my-token (balance VILLAIN))")); + ctx=TestState.step(ctx,"(call my-token (transfer VILLAIN 10))"); + ctx=TestState.step(ctx,"(call my-token (transfer HERO 100))"); // should have no effect + final Context fctx=ctx; // save context for later tests + + assertEquals(990L,evalL(fctx,"(call my-token (balance *address*))")); + assertEquals(10L,evalL(fctx,"(call my-token (balance VILLAIN))")); + + assertEquals(1000L,evalL(fctx,"(call my-token (total-supply))")); + + assertTrue(evalB(fctx,"(actor? my-token)")); + assertFalse(evalB(fctx,"(actor? HERO)")); + assertFalse(evalB(fctx,"(actor? :foo)")); + + // some tests for contract safety + assertAssertError(TestState.step(fctx,"(call my-token (transfer VILLAIN 1000))")); + assertAssertError(TestState.step(fctx,"(call my-token (transfer VILLAIN -1))")); + assertAssertError(TestState.step(fctx,"(call my-token (transfer nil 10))")); + assertStateError(TestState.step(fctx,"(call my-token (bad-function))")); + } + + + @Test public void testHelloContract() throws IOException { + Context ctx=TestState.step("(do )"); + + // Technique for deploying contract with a quoted form + String contractString=Utils.readResourceAsString("contracts/hello.con"); + ctx=TestState.step(ctx,"(def hello (deploy (quote "+contractString+")))"); + + ctx=TestState.step(ctx,"(call hello (greet \"Nikki\"))"); + assertEquals("Hello Nikki",ctx.getResult().toString()); + + ctx=TestState.step(ctx,"(call hello (greet \"Nikki\"))"); + assertEquals("Welcome back Nikki",ctx.getResult().toString()); + + ctx=TestState.step(ctx,"(call hello (greet \"Alice\"))"); + assertEquals("Hello Alice",ctx.getResult().toString()); + + } + + @Test public void testFundingContract() throws IOException { + Context ctx=TestState.step("(do )"); + + Address addr=ctx.getAddress(); + + String contractString=Utils.readResourceAsString("contracts/funding.con"); + ctx=TestState.step(ctx,"(def funcon (deploy '"+contractString+"))"); + assertFalse(ctx.isExceptional()); + Address caddr=(Address) ctx.getResult(); + long initialBalance=ctx.getBalance(addr); + + { + // just test return of the correct *offer* value + ctx=TestState.step(ctx,"(call funcon 1234 (echo-offer))"); + assertCVMEquals(1234,ctx.getResult()); + assertEquals(initialBalance,ctx.getBalance(addr)); + assertEquals(TestState.TOTAL_FUNDS,ctx.getState().computeTotalFunds()); + } + + { + // test accepting half of funds + final Context rctx=TestState.step(ctx,"(call funcon 1000 (accept-quarter))"); + assertCVMEquals(250,rctx.getResult()); + assertEquals(250,rctx.getBalance(caddr)); + + assertEquals(initialBalance-250,rctx.getBalance(addr)); + assertEquals(TestState.TOTAL_FUNDS,rctx.getState().computeTotalFunds()); + } + + { + // test accepting all funds + final Context rctx=TestState.step(ctx,"(call funcon 1237 (accept-all))"); + assertCVMEquals(1237,rctx.getResult()); + assertEquals(1237,rctx.getBalance(caddr)); + + assertEquals(initialBalance-1237,rctx.getBalance(addr)); + assertEquals(TestState.TOTAL_FUNDS,rctx.getState().computeTotalFunds()); + } + + { + // test accepting zero funds + final Context rctx=TestState.step(ctx,"(call funcon 1237 (accept-zero))"); + assertCVMEquals(0,rctx.getResult()); + assertEquals(0,rctx.getBalance(caddr)); + + assertEquals(initialBalance,rctx.getBalance(addr)); + assertEquals(TestState.TOTAL_FUNDS,rctx.getState().computeTotalFunds()); + } + + { + // test contract that accepts funds then rolls back + final Context rctx=TestState.step(ctx,"(call funcon 1237 (accept-rollback))"); + assertEquals(Keywords.FOO,rctx.getResult()); + assertEquals(0,rctx.getBalance(caddr)); + assertEquals(0,rctx.getOffer()); + + assertEquals(initialBalance,rctx.getBalance(addr)); + assertEquals(TestState.TOTAL_FUNDS,rctx.getState().computeTotalFunds()); + } + + { + // test contract that accepts funds repeatedly + final Context rctx=TestState.step(ctx,"(call funcon 1337 (accept-repeat))"); + assertCVMEquals(0,rctx.getResult()); // final offer echoed back + assertEquals(1337,rctx.getBalance(caddr)); + + assertEquals(initialBalance-1337,rctx.getBalance(addr)); + assertEquals(TestState.TOTAL_FUNDS,rctx.getState().computeTotalFunds()); + } + + { + // test contract that forwards funds to self + final Context rctx=TestState.step(ctx,"(call funcon 1337 (accept-forward))"); + assertCVMEquals(1337,rctx.getResult()); // result of forward to accept-all + assertEquals(1337,rctx.getBalance(caddr)); + + assertEquals(initialBalance-1337,rctx.getBalance(addr)); + assertEquals(TestState.TOTAL_FUNDS,rctx.getState().computeTotalFunds()); + } + + // test *offer* restored after send + assertEquals(0,evalL(ctx,"(do (call funcon 1237 (accept-zero)) *offer*)")); + + // test *offer* in contract with no send + assertEquals(0,evalL(ctx,"(call funcon (echo-offer))")); + } + + + + @Test public void testExceptionContract() throws IOException { + Context ctx=TestState.step("(do )"); + + String contractString=Utils.readResourceAsString("contracts/exceptional.con"); + ctx=TestState.step(ctx,"(def ex (deploy '"+contractString+"))"); + + ctx=TestState.step(ctx,"(call ex (halt-fn \"Jenny\"))"); + assertEquals("Jenny",ctx.getResult().toString()); + + // calling this will break the fragile definition, but then rollback to restore it + ctx=TestState.step(ctx,"(call ex (rollback-fn \"Alice\"))"); + assertEquals("Alice",ctx.getResult().toString()); + + ctx=TestState.step(ctx,"(call ex (get-fragile))"); + assertEquals(Keyword.create("ok"),ctx.getResult()); + + // Calling this should break the fragile definition permanently + ctx=TestState.step(ctx,"(call ex (break-fn \"Lana\"))"); + assertEquals("Lana",ctx.getResult().toString()); + + ctx=TestState.step(ctx,"(call ex (get-fragile))"); + assertEquals(Keyword.create("broken"),ctx.getResult()); + + } + +} diff --git a/convex-core/src/test/java/convex/actors/PredictionMarketTest.java b/convex-core/src/test/java/convex/actors/PredictionMarketTest.java new file mode 100644 index 000000000..45178215c --- /dev/null +++ b/convex-core/src/test/java/convex/actors/PredictionMarketTest.java @@ -0,0 +1,168 @@ +package convex.actors; + +import static convex.test.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.data.Maps; +import convex.core.lang.ACVMTest; +import convex.core.lang.Context; +import convex.core.lang.RT; +import convex.core.lang.TestState; +import convex.core.util.Utils; + +public class PredictionMarketTest extends ACVMTest { + + protected PredictionMarketTest() { + super(TestState.STATE); + } + + @SuppressWarnings("rawtypes") + private T evalCall(Context ctx,Address addr, long offer, String name, Object... args) { + Context rctx=doCall(ctx,addr, offer, name, args); + return RT.jvm(rctx.getResult()); + } + + @SuppressWarnings("unchecked") + private Context doCall(Context ctx,Address addr, long offer, String name, Object... args) { + int n=args.length; + ACell[] cvmArgs=new ACell[n]; + for (int i=0; i rctx=ctx.actorCall(addr, offer, name, cvmArgs); + return (Context) rctx; + } + + @SuppressWarnings("rawtypes") + @Test + public void testPredictionContract() throws IOException { + String contractString = Utils.readResourceAsString("lab/prediction-market.cvx"); + + // Run code to initialise actor with [oracle oracle-key outcomes] + Context ctx = context(); + ctx=step(contractString); + assertNotError(ctx); + ctx=step(ctx,"(deploy (build-prediction-market *address* :bar #{true,false}))"); + assertNotError(ctx); + + Address addr = (Address) ctx.getResult(); + assertNotNull(addr); + ctx = step(ctx, "(def caddr " + addr + ")"); + assertFalse(ctx.isExceptional()); + + // tests of bonding curve function with empty stakes + assertEquals(0.0, (double)evalCall(ctx,addr, 0L, "bond", Maps.empty()), 0.01); + + // bonding curve point with one staked outcome + assertEquals(10.0, evalCall(ctx,addr, 0L, "bond", Maps.of(true, 10L)), 0.01); + + // two staked outcomes + assertEquals(5.0, evalCall(ctx,addr, 0L, "bond", Maps.of(true, 3L, false, 4L)), 0.01); + + long initalBal=ctx.getBalance(HERO); + { // stake on, stake off..... + // first we stake on the 'true' outcome + Context rctx1 = doCall(ctx,addr, 10L, "stake", true, 10L); + assertCVMEquals(10L, rctx1.getResult()); + assertEquals(10L, initalBal- rctx1.getBalance(HERO)); + assertEquals(1.0, evalD(rctx1, "(call caddr (price true))")); // should be exact price 100% + assertEquals(0.0, evalD(rctx1, "(call caddr (price false))")); // should be exact price 0% + + // stake on other outcome. Note that we offer too much funds, but this won't be + // accepted so no issue. + Context rctx2 = doCall(rctx1,addr, 10L, "stake", false, 10L); + assertCVMEquals(4L, rctx2.getResult()); + assertEquals(14L, initalBal - rctx2.getBalance(HERO)); + assertEquals(TestState.TOTAL_FUNDS, rctx2.getState().computeTotalFunds()); + + // halve stakes + Context rctx3 = doCall(rctx2,addr, 10L, "stake", false, 5L); + rctx3 = doCall(rctx3,addr, 10L, "stake", true, 5L); + assertEquals(7L, initalBal - rctx3.getBalance(HERO)); + assertEquals(0.5, evalD(rctx3, "(call caddr (price true))"), 0.1); // approx price given rounding + + // zero one stake + Context rctx4 = doCall(rctx3,addr, 10L, "stake", false, 0L); + assertCVMEquals(-2L, rctx4.getResult()); // refund of 2 + assertEquals(5L, initalBal - rctx4.getBalance(HERO)); + + // Exit market + Context rctx5 =doCall(rctx4,addr, 10L, "stake", true, 0L); + assertCVMEquals(-5L, rctx5.getResult()); // refund of 5 + assertEquals(0L, initalBal - rctx5.getBalance(HERO)); + assertEquals(TestState.TOTAL_FUNDS, rctx2.getState().computeTotalFunds()); + } + + { // underfunded stake request + Context rctx1 = doCall(ctx,addr, 5L, "stake", true, 10L); + assertStateError(rctx1); // TODO: what is right error type? + assertEquals(0L, initalBal - rctx1.getBalance(HERO)); + } + + { // negative stake request + Context rctx1 = doCall(ctx,addr, 5L, "stake", true, -10L); + assertAssertError(rctx1); // TODO: what is right error type? + assertEquals(0L, initalBal - rctx1.getBalance(HERO)); + } + } + + @Test + public void testPayouts() throws IOException { + // setup address for this little play + Context ctx = step("(do (def HERO " + HERO + ") (def VILLAIN " +VILLAIN + ") )"); + + ctx = step("(import convex.trusted-oracle :as oaddr)"); + + // call to create oracle with key :bar and current address (HERO) trusted + ctx = step(ctx, "(oaddr/register :bar {:trust #{*address*}})"); + + // deploy a prediction market using the oracle + String contractString = Utils.readResourceAsString("lab/prediction-market.cvx"); + ctx=step(ctx,"(deploy ("+contractString+" oaddr :bar #{true,false}))"); + Address pmaddr = (Address) ctx.getResult(); + ctx = step(ctx, "(def pmaddr " + pmaddr + ")"); + ctx = stepAs(VILLAIN, ctx, "(def pmaddr "+pmaddr+")"); + + // initial state checks + assertEquals(false,evalB(ctx, "(call pmaddr (finalized?))")); + assertEquals(0L, evalL(ctx, "(balance pmaddr)")); + + { // Act 1. Two players stake. our Villain wins this time.... + Context c = ctx; + c = step(c, "(call pmaddr 5000 (stake true 4000))"); + c = stepAs(VILLAIN, c, "(call pmaddr 5000 (stake false 3000))"); + assertEquals(5000L, c.getBalance(pmaddr)); + + assertFalse(evalB(c, "(call pmaddr (finalized?))")); + assertEquals(0.64, evalD(c, "(call pmaddr (price true))"), 0.0001); // 64% chance on true. Looks a good bet + assertNull(eval(c, "(call pmaddr (payout))")); + + // But alas, our hero is thwarted... + c = step(c, "(oaddr/provide :bar false)"); + assertCVMEquals(Boolean.FALSE, c.getResult()); + + // collect payouts + c = step(c, "(call pmaddr (payout))"); + assertCVMEquals(0L, c.getResult()); + assertEquals(HERO_BALANCE - 4000, c.getBalance(HERO)); + + c = stepAs(VILLAIN, c, "(call pmaddr (payout))"); + assertCVMEquals(5000L, c.getResult()); + assertEquals(VILLAIN_BALANCE + 4000, c.getBalance(VILLAIN)); + + assertEquals(0L, c.getBalance(pmaddr)); + assertEquals(TestState.TOTAL_FUNDS, c.getState().computeTotalFunds()); + } + } + +} diff --git a/convex-core/src/test/java/convex/actors/RegistryTest.java b/convex-core/src/test/java/convex/actors/RegistryTest.java new file mode 100644 index 000000000..00005ac48 --- /dev/null +++ b/convex-core/src/test/java/convex/actors/RegistryTest.java @@ -0,0 +1,111 @@ +package convex.actors; + +import static convex.test.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import convex.core.data.ACell; +import convex.core.data.AHashMap; +import convex.core.data.Address; +import convex.core.data.Keyword; +import convex.core.data.Maps; +import convex.core.data.Symbol; +import convex.core.init.Init; +import convex.core.init.InitTest; +import convex.core.lang.ACVMTest; +import convex.core.lang.Context; +import convex.test.Samples; + +public class RegistryTest extends ACVMTest { + + protected RegistryTest() throws IOException { + super(InitTest.BASE); + } + + static final Address REG = Init.REGISTRY_ADDRESS; + + Context INITIAL_CONTEXT=context(); + + @Test + public void testRegistryContract() throws IOException { + // TODO: think about whether we want this in initial state + // String contractString=Utils.readResourceAsString("contracts/registry.con"); + // Object + // cfn=CoreTest.INITIAL_CONTEXT.eval(Reader.read(contractString)).getResult(); + // Context ctx=CoreTest.INITIAL_CONTEXT.deployContract(cfn); + // Address addr=(Address) ctx.getResult(); + + AHashMap ddo = Maps.of(Keyword.create("name"), "Bob"); + Context ctx = INITIAL_CONTEXT.actorCall(REG, 0, Symbol.create("register"), ddo); + assertEquals(ddo, ctx.actorCall(REG, 0, "lookup", ctx.getAddress()).getResult()); + } + + @Test + public void testRegistryCNS() throws IOException { + Context ctx=INITIAL_CONTEXT.fork(); + + assertEquals(REG,eval(ctx,"(call *registry* (cns-resolve 'convex.registry))")); + } + + @Test + public void testRegistryCNSUpdate() throws IOException { + Context ctx=INITIAL_CONTEXT.fork(); + + assertNull(eval(ctx,"(call *registry* (cns-resolve 'convex.test.foo))")); + + // Real Address we want for CNS mapping + final Address badAddr=Samples.BAD_ADDRESS; + + ctx=step(ctx,"(call *registry* (cns-update 'convex.test.foo "+badAddr+"))"); + assertNobodyError(ctx); + + final Address realAddr=Address.create(1); // Init address, FWIW + ctx=step(ctx,"(call *registry* (cns-update 'convex.test.foo "+realAddr+"))"); + assertNotError(ctx); + + assertEquals(realAddr,eval(ctx,"(call *registry* (cns-resolve 'convex.test.foo))")); + + { // Check VILLAIN can't steal CNS mapping + Context c=ctx.forkWithAddress(VILLAIN); + + // VILLAIN shouldn't be able to use update on existing CNS mapping + assertTrustError(step(c,"(call *registry* (cns-update 'convex.test.foo *address*))")); + + // original mapping should be held + assertEquals(realAddr,eval(c,"(call *registry* (cns-resolve 'convex.test.foo))")); + } + + { // Check Transfer of control to VILLAIN + Context c=step(ctx,"(call *registry* (cns-control 'convex.test.foo "+VILLAIN+"))"); + + // HERO shouldn't be able to use update or control any more + assertTrustError(step(c,"(call *registry* (cns-update 'convex.test.foo *address*))")); + assertTrustError(step(c,"(call *registry* (cns-control 'convex.test.foo *address*))")); + + // Switch to VILLAIN + c=c.forkWithAddress(VILLAIN); + + // Change mapping + c=step(c,"(call *registry* (cns-update 'convex.test.foo *address*))"); + assertNotError(c); + assertEquals(VILLAIN,eval(c,"(call *registry* (cns-resolve 'convex.test.foo))")); + } + + { // Check VILLAIN can create new mapping + // TODO probably shouldn't be free-for-all? + + Context c=ctx.forkWithAddress(VILLAIN); + + // VILLAIN shouldn't be able to use update on existing CNS mapping + c=step(c,"(call *registry* (cns-update 'convex.villain *address*))"); + assertNotError(c); + + // original mapping should be held + assertEquals(VILLAIN,eval(c,"(call *registry* (cns-resolve 'convex.villain))")); + } + } +} diff --git a/convex-core/src/test/java/convex/actors/TorusTest.java b/convex-core/src/test/java/convex/actors/TorusTest.java new file mode 100644 index 000000000..26a04ef9b --- /dev/null +++ b/convex-core/src/test/java/convex/actors/TorusTest.java @@ -0,0 +1,251 @@ +package convex.actors; + +import static convex.test.Assertions.assertError; +import static convex.test.Assertions.assertNotError; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import convex.core.data.Address; +import convex.core.data.prim.CVMDouble; +import convex.core.init.InitTest; +import convex.core.lang.ACVMTest; +import convex.core.lang.Context; +import convex.core.lang.RT; +import convex.core.util.Utils; +import convex.lib.FungibleTest; + +public class TorusTest extends ACVMTest { + Context INITIAL=context(); + + protected TorusTest() { + super(InitTest.STATE); + + try { + Context ctx=INITIAL; + ctx=step(ctx,"(import convex.fungible :as fun)"); + + ctx=step(ctx,"(import convex.asset :as asset)"); + + // Deploy currencies for testing (10m each, 2 decimal places) + ctx=step(ctx,"(def USD (deploy (fun/build-token {:supply 1000000000})))"); + USD=(Address) ctx.getResult(); + //System.out.println("USD deployed Address = "+USD); + ctx=step(ctx,"(def GBP (deploy (fun/build-token {:supply 1000000000})))"); + GBP=(Address) ctx.getResult(); + + // Deploy Torus actor itself + ctx=ctx.withJuice(INITIAL_JUICE); + + ctx= step(ctx,"(def TORUS (import torus.exchange :as torus))"); + TORUS=(Address)ctx.getResult(); + assertNotNull(ctx.getAccountStatus(TORUS)); + //System.out.println("Torus deployed Address = "+TORUS); + + // Deploy USD market. No market for GBP yet! + ctx= step(ctx,"(call TORUS (create-market USD))"); + USD_MARKET=(Address)ctx.getResult(); + INITIAL= ctx.withResult(TORUS).withJuice(INITIAL_JUICE); + } catch (Throwable e) { + e.printStackTrace(); + throw Utils.sneakyThrow(e); + } + } + + Address USD = null; + Address GBP = null; + Address TORUS = null; + Address USD_MARKET = null; + + static { + + } + + @Test public void testMissingMarket() { + Context ctx=INITIAL.fork(); + + assertNull(eval(ctx,"(torus/get-market GBP)")); + + // price should be null for missing markets, regardless of whether or not a token exists + assertNull(eval(ctx,"(torus/price GBP)")); + assertNull(eval(ctx,"(torus/price #789798789)")); + + } + + @Test public void testDeployedCurrencies() { + Context ctx=INITIAL.fork(); // Initial test context + ctx=step(ctx,"(import torus.exchange :as torus)"); + ctx= step(ctx,"(def GBP (import currency.GBP :as GBP))"); + ctx= step(ctx,"(def USD (import currency.USD :as USD))"); + assertNotNull(ctx.getResult()); + ctx= step(ctx,"(torus/price GBP USD)"); + assertTrue(ctx.getResult() instanceof CVMDouble); + + ctx= step(ctx,"(torus/price GBP USD)"); + + } + + + @Test public void testTorusAPI() { + Context ctx=INITIAL.fork(); + + // Deploy GBP market. + ctx= step(ctx,"(def GBPM (call TORUS (create-market GBP)))"); + Address GBP_MARKET=(Address)ctx.getResult(); + assertNotNull(GBP_MARKET); + + // Check we can access the USD market + ctx= step(ctx,"(def USDM (torus/get-market USD))"); + assertEquals(USD_MARKET,ctx.getResult()); + + // Prices should be null with no markets + assertNull(eval(ctx,"(torus/price USD)")); + assertNull(eval(ctx,"(torus/price GBP)")); + + // ============================================================ + // FIRST TEST: Initial deposit of $100k USD liquidity, £50k GBP liquidity + // Deposit some liquidity $100,000 for 1000 Gold = $100 price = 100000 CVX / US Cent + ctx=step(ctx,"(torus/add-liquidity USD 10000000 1000000000000)"); + assertEquals(1.0,Math.sqrt(10000000.0*1000000000000.0)/(long)RT.jvm(ctx.getResult()),0.00001); + ctx=step(ctx,"(torus/add-liquidity GBP 5000000 1000000000000)"); + assertEquals(1.0,Math.sqrt(5000000.0*1000000000000.0)/(long)RT.jvm(ctx.getResult()),0.00001); + + // ============================================================ + // SECOND TEST: Check prices + assertEquals(100000.0,evalD(ctx,"(torus/price USD)")); + assertEquals(200000.0,evalD(ctx,"(torus/price GBP)")); + + assertEquals(1.0,evalD(ctx,"(torus/price GBP GBP)")); + assertEquals(0.5,evalD(ctx,"(torus/price USD GBP)")); + assertEquals(2.0,evalD(ctx,"(torus/price GBP USD)")); + + // ============================================================ + // THIRD TEST: Check marginal buy trades for $1 / £1 + assertEquals(101,evalL(ctx,"(torus/buy GBP 100 GBP)")); + assertEquals(101,evalL(ctx,"(torus/buy-quote GBP 100 GBP)")); + assertEquals(51,evalL(ctx,"(torus/buy-quote USD 100 GBP)")); + assertEquals(51,evalL(ctx,"(torus/buy USD 100 GBP)")); + assertEquals(201,evalL(ctx,"(torus/buy GBP 100 USD)")); + assertEquals(201,evalL(ctx,"(torus/buy-quote GBP 100 USD)")); + + // ============================================================ + // FOURTH TEST: Check marginal sell trades for $1 / £1 + assertEquals(99,evalL(ctx,"(torus/sell GBP 100 GBP)")); + assertEquals(99,evalL(ctx,"(torus/sell-quote GBP 100 GBP)")); + assertEquals(49,evalL(ctx,"(torus/sell USD 100 GBP)")); + assertEquals(49,evalL(ctx,"(torus/sell-quote USD 100 GBP)")); + assertEquals(199,evalL(ctx,"(torus/sell GBP 100 USD)")); + assertEquals(199,evalL(ctx,"(torus/sell-quote GBP 100 USD)")); + + // Trades too big + assertError(step(ctx,"(torus/buy USD "+Long.MAX_VALUE+")")); + assertError(step(ctx,"(torus/buy USD 10000000)")); + + // Too expensive for pool + assertError(step(ctx,"(torus/buy USD 9999999)")); + } + + @Test public void testInitialTokenMarket() { + Context ctx=INITIAL.fork(); + + // Check we can access the USD market + ctx= step(ctx,"(def USDM (torus/get-market USD))"); + assertEquals(USD_MARKET,ctx.getResult()); + + // should be no price for initial market with zero liquidity + assertNull(eval(ctx,"(call USDM (price))")); + + // Offer tokens to market ($200k) + ctx= step(ctx,"(asset/offer USDM [USD 20000000])"); + assertEquals(20000000L,evalL(ctx,"(asset/get-offer USD *address* USDM)")); + + // ============================================================ + // FIRST TEST: Initial deposit of $100k USD liquidity + // Deposit some liquidity $100,000 for 1000 Gold = $100 price = 100000 CVX / US Cent + ctx= step(ctx,"(call USDM 1000000000000 (add-liquidity 10000000))"); + final long INITIAL_SHARES=RT.jvm(ctx.getResult()); + + assertEquals(10000000L,evalL(ctx,"(asset/balance USD USDM)")); + assertEquals(1000000000000L,evalL(ctx,"(balance USDM)")); + assertEquals(INITIAL_SHARES,evalL(ctx,"(asset/balance USDM *address*)")); // Initial pool shares, accessible as a fungible asset balance + + // Should have consumed half the full offer of tokens + assertEquals(10000000L,evalL(ctx,"(asset/get-offer USD *address* USDM)")); + + // price should be 100000 CVX / US Cent + assertEquals(100000.0,evalD(ctx,"(call USDM (price))")); + + // ============================================================ + // SECOND TEST: Initial deposit of $100k USD liquidity + // Deposit more liquidity $100,000 for 1000 Gold - previous token offer should cover this + ctx= step(ctx,"(call USDM 1000000000000 (add-liquidity 10000000))"); + final long NEW_SHARES=RT.jvm(ctx.getResult()); + assertEquals(20000000L,evalL(ctx,"(asset/balance USD USDM)")); + + // Check new pool shares, accessible as a fungible asset balance + assertEquals(INITIAL_SHARES+NEW_SHARES,evalL(ctx,"(asset/balance USDM *address*)")); + + // Price should be unchanged + assertEquals(100000.0,evalD(ctx,"(call USDM (price))")); + + // should have consumed remaining token offer + assertEquals(0L,evalL(ctx,"(asset/get-offer USD *address* USDM)")); + + // ============================================================ + // THIRD TEST - withdraw half of liquidity + long balanceBeforeWithdrawal=ctx.getBalance(); + ctx= step(ctx,"(call USDM (withdraw-liquidity "+NEW_SHARES+"))"); + assertEquals(RT.cvm(NEW_SHARES),ctx.getResult()); + + assertEquals(INITIAL_SHARES,evalL(ctx,"(asset/balance USDM *address*)")); + assertEquals(10000000L,evalL(ctx,"(asset/balance USD USDM)")); + assertEquals(990000000L,evalL(ctx,"(asset/balance USD *address*)")); + assertTrue(ctx.getBalance()>balanceBeforeWithdrawal); + assertEquals(1000000000000L,evalL(ctx,"(balance USDM)")); // Convex balance back to start + + // Generic fungible test on shares + FungibleTest.doFungibleTests(ctx,USD_MARKET,ctx.getAddress()); + + // ============================================================ + // FORTH TEST - buy half of all tokens ($50k) + ctx= step(ctx,"(call USDM *balance* (buy-tokens 5000000))"); + long paidConvex=RT.jvm(ctx.getResult()); + assertTrue(paidConvex>1000000000000L); // should cost more than pool Convex balance after fee + assertTrue(paidConvex<1100000000000L,"Paid:" +paidConvex); // but less than 10% fee + assertEquals(5000000L,evalL(ctx,"(asset/balance USD USDM)")); + assertEquals(995000000L,evalL(ctx,"(asset/balance USD *address*)")); + assertEquals(INITIAL_SHARES,evalL(ctx,"(asset/balance USDM *address*)")); + + // ============================================================ + // FIFTH TEST - sell back tokens ($50k) + ctx= step(ctx,"(asset/offer USDM [USD 5000000])"); + ctx= step(ctx,"(call USDM (sell-tokens 5000000))"); + long gainedConvex=RT.jvm(ctx.getResult()); + assertTrue(gainedConvex>900000000000L); // should gain most of money back + assertTrue(gainedConvex0); + ctx=step(ctx,"(torus/withdraw-liquidity USD "+shares+")"); + assertNotError(ctx); + assertEquals(0L,evalL(ctx,"(asset/balance USDM *address*)")); // should have no shares left + assertEquals(0L,evalL(ctx,"(asset/balance USD USDM)")); // should be no USD left in liquidity pool + assertEquals(0L,evalL(ctx,"(balance USDM)")); // should be no CVX left in liquidity pool + } + + @Test public void testSetup() { + assertNotNull(TORUS); + assertNotNull(USD); + assertNotNull(GBP); + assertNotNull(USD_MARKET); + } + +} diff --git a/convex-core/src/test/java/convex/comms/GenTestFormat.java b/convex-core/src/test/java/convex/comms/GenTestFormat.java new file mode 100644 index 000000000..4c8aab370 --- /dev/null +++ b/convex-core/src/test/java/convex/comms/GenTestFormat.java @@ -0,0 +1,59 @@ +package convex.comms; + +import static org.junit.Assert.assertEquals; + +import org.junit.runner.RunWith; + +import com.pholser.junit.quickcheck.From; +import com.pholser.junit.quickcheck.Property; +import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; + +import convex.core.data.ACell; +import convex.core.data.AString; +import convex.core.data.Blob; +import convex.core.data.Format; +import convex.core.data.FuzzTestFormat; +import convex.core.data.Ref; +import convex.core.data.Strings; +import convex.core.exceptions.BadFormatException; +import convex.core.lang.RT; +import convex.test.generators.PrimitiveGen; +import convex.test.generators.ValueGen; + +@RunWith(JUnitQuickcheck.class) +public class GenTestFormat { + @Property + public void messageRoundTrip(String str) throws BadFormatException { + AString s=Strings.create(str); + Blob b = Format.encodedBlob(s); + AString s2 = Format.read(b); + assertEquals(s, s2); + assertEquals(b, Format.encodedBlob(s2)); + + FuzzTestFormat.doMutationTest(b); + } + + @Property + public void primitiveRoundTrip(@From(PrimitiveGen.class) ACell prim) throws BadFormatException { + Blob b = Format.encodedBlob(prim); + ACell o = Format.read(b); + assertEquals(prim, o); + assertEquals(b, Format.encodedBlob(o)); + + FuzzTestFormat.doMutationTest(b); + } + + @Property + public void dataRoundTrip(@From(ValueGen.class) ACell value) throws BadFormatException { + Ref pref = ACell.createPersisted(value); // ensure persisted + Blob b = Format.encodedBlob(value); + ACell o = Format.read(b); + + assertEquals(RT.getType(value), RT.getType(o)); + assertEquals(value, o); + assertEquals(b, Format.encodedBlob(o)); + assertEquals(pref.getValue(), o); + + FuzzTestFormat.doMutationTest(b); + } +} diff --git a/convex-core/src/test/java/convex/comms/VLCEncodingTest.java b/convex-core/src/test/java/convex/comms/VLCEncodingTest.java new file mode 100644 index 000000000..b833b3fa3 --- /dev/null +++ b/convex-core/src/test/java/convex/comms/VLCEncodingTest.java @@ -0,0 +1,142 @@ +package convex.comms; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.ByteBuffer; + +import org.junit.jupiter.api.Test; + +import convex.core.data.Blob; +import convex.core.data.Format; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.lang.RT; + +public class VLCEncodingTest { + + @Test + public void testMessageLength() throws BadFormatException { + ByteBuffer bb = Blob.fromHex("8048").getByteBuffer(); + assertEquals(0, bb.position()); + int len = Format.peekMessageLength(bb); + assertEquals(72, len); + assertEquals(2, Format.getVLCLength(len)); + } + + /** + * Test the assumption that MAX_MESSAGE_LENGTH is the largest length that can be + * VLC encoded in 2 bytes + * @throws BadFormatException + */ + @Test + public void testVLCLength() throws BadFormatException { + assertEquals(2, Format.getVLCLength(Format.LIMIT_ENCODING_LENGTH)); + assertEquals(3, Format.getVLCLength(Format.LIMIT_ENCODING_LENGTH + 1)); + + ByteBuffer bb = Blob.fromHex("BF7F").getByteBuffer(); + assertEquals(0, bb.position()); + int len = Format.peekMessageLength(bb); + assertEquals(Format.LIMIT_ENCODING_LENGTH, len); + assertEquals(2, Format.getVLCLength(len)); + } + + @Test + public void testLongVLC() { + // note 09 as tag for long + assertEquals("0900", Format.encodedString(0L)); + assertEquals("0901", Format.encodedString(1L)); + assertEquals("097f", Format.encodedString(-1L)); + + assertEquals("093f", Format.encodedString(63L)); // 6 lowest bits set + assertEquals("098040", Format.encodedString(64L)); // first overflow to 2 bytes VLC + assertEquals("098100", Format.encodedString(128L)); // first carry of positive bit + + assertEquals("0941", Format.encodedString(-63L)); + assertEquals("0940", Format.encodedString(-64L)); // sign bit only in 1 byte + assertEquals("09ff3f", Format.encodedString(-65L)); // sign overflow to 2 bytes VLC + assertEquals("09ff00", Format.encodedString(-128L)); + assertEquals("09fe7f", Format.encodedString(-129L)); // first negative carry + + assertEquals("0980ffffffffffffffff7f", Format.encodedString(Long.MAX_VALUE)); + assertEquals("09ff808080808080808000", Format.encodedString(Long.MIN_VALUE)); + } + + +// TODO: Currently not allowing BigInteger as valid data object. May reconsider. +// @Test public void testBigIntegerVLC() { +// // note 0a as tag for short +// assertEquals("0a00",Format.encodedString(BigInteger.valueOf(0))); +// assertEquals("0a01",Format.encodedString(BigInteger.valueOf(1))); +// assertEquals("0a7f",Format.encodedString(BigInteger.valueOf(-1))); +// +// assertEquals("0a3f",Format.encodedString(BigInteger.valueOf(63))); // 6 lowest bits set +// assertEquals("0a8040",Format.encodedString(BigInteger.valueOf(64))); // first overflow to 2 bytes VLC +// assertEquals("0a8100",Format.encodedString(BigInteger.valueOf(128))); // first carry of positive bit +// +// assertEquals("0a41",Format.encodedString(BigInteger.valueOf(-63))); +// assertEquals("0a40",Format.encodedString(BigInteger.valueOf(-64))); // sign bit only in 1 byte +// assertEquals("0aff3f",Format.encodedString(BigInteger.valueOf(-65))); // sign overflow to 2 bytes VLC +// assertEquals("0aff00",Format.encodedString(BigInteger.valueOf(-128))); +// assertEquals("0afe7f",Format.encodedString(BigInteger.valueOf(-129))); // first negative carry +// +// assertEquals("0a80ffffffffffffffff7f",Format.encodedString(BigInteger.valueOf(Long.MAX_VALUE))); +// assertEquals("0aff808080808080808000",Format.encodedString(BigInteger.valueOf(Long.MIN_VALUE))); +// +// BigInteger b1=BigInteger.valueOf(Long.MAX_VALUE).multiply(BigInteger.valueOf(128)); +// assertEquals("0a80ffffffffffffffffff00",Format.encodedString(b1)); +// +// BigInteger b2=BigInteger.valueOf(Long.MIN_VALUE).multiply(BigInteger.valueOf(128)); +// assertEquals("0aff80808080808080808000",Format.encodedString(b2)); +// } + + @Test + public void testLongVLCBadFormat() { + // too long encodings + assertThrows(BadFormatException.class, () -> Format.read(Blob.fromHex("098000"))); + assertThrows(BadFormatException.class, () -> Format.read(Blob.fromHex("09FF7F"))); + } + + @Test + public void testVLCSignExtend() { + assertEquals(0L, Format.vlcSignExtend((byte) 0x00)); + assertEquals(-64L, Format.vlcSignExtend((byte) 0x40)); + assertEquals(-1L, Format.vlcSignExtend((byte) 0xFF)); + assertEquals(63, Format.vlcSignExtend((byte) 0x3F)); + assertEquals(0L, Format.vlcSignExtend((byte) 0x80)); + } + +// @Test public void testBigIntegerVLCRegression() throws BadFormatException { +// BigInteger b1=new BigInteger("-10826789006513807832719466915686947597958886414817953"); +// String b1s=b1.toString(16); +// String encodedString=Format.encodedString(b1); +// BigInteger b2=Format.read(Blob.fromHex(encodedString)); +// String b2s=b2.toString(16); +// assertEquals(b1s,b2s); +// assertEquals(b1,b2); +// } + +// @Test public void testLongVLCRegression() throws BadFormatException { +// long longVal=-221195466131295L; +// BigInteger b1=BigInteger.valueOf(longVal); +// String encodedString=Format.encodedString(b1); +// String longEncodedString=Format.encodedString(longVal); +// assertEquals(longEncodedString.substring(2),encodedString.substring(2)); // should be equal after tag +// BigInteger b2=Format.read(Blob.fromHex(encodedString)); +// assertEquals(b1,b2); +// } + + @Test + public void testLongVLCRegression2() throws BadFormatException { + CVMLong b = CVMLong.create(1496216L); + Blob blob = Format.encodedBlob(b); + assertEquals(b, Format.read(blob)); + } + + @Test + public void testLongVLCRegression() throws BadFormatException { + CVMLong b = RT.cvm(1234578); + Blob blob = Format.encodedBlob(b); + assertEquals(b, Format.read(blob)); + } +} diff --git a/convex-core/src/test/java/convex/comms/VLCParamTest.java b/convex-core/src/test/java/convex/comms/VLCParamTest.java new file mode 100644 index 000000000..8e22a37d1 --- /dev/null +++ b/convex-core/src/test/java/convex/comms/VLCParamTest.java @@ -0,0 +1,56 @@ +package convex.comms; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import convex.core.data.ACell; +import convex.core.data.Blob; +import convex.core.data.Format; +import convex.core.data.FuzzTestFormat; +import convex.core.data.Tag; +import convex.core.data.prim.CVMByte; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.lang.RT; + +@RunWith(Parameterized.class) +public class VLCParamTest { + private ACell value; + + public VLCParamTest(Object value) { + // create using CVM-coerced values + this.value = RT.cvm(value); + } + + @Parameterized.Parameters(name = "{0}") + public static Collection dataExamples() { + return Arrays.asList(new Object[][] { { 0L }, { 63L }, { 64L }, { -63L }, { -64L }, { -65L }, { 1234L }, + { 1234578 }, { -1234578 }, { CVMByte.create(1) }, { CVMByte.create(255) }, { Long.MAX_VALUE }, { Long.MIN_VALUE }, + { Integer.MAX_VALUE }, { Integer.MIN_VALUE }, +// { BigInteger.valueOf(Long.MAX_VALUE).multiply(BigInteger.TEN) }, +// { BigInteger.valueOf(Long.MIN_VALUE).multiply(BigInteger.TEN) }, + + }); + } + + @Test + public void testRoundTrip() throws BadFormatException { + Blob b = Format.encodedBlob(value); + ACell v2 = Format.read(b); + assertEquals(value, v2); + + if (value instanceof CVMLong) { + CVMLong cl=(CVMLong) value; + assertEquals(Tag.LONG, b.byteAt(0)); // check correct tag + assertEquals(1 + Format.getVLCLength(cl.longValue()), b.count()); // check length after tag + } + + FuzzTestFormat.doMutationTest(b); + } +} diff --git a/convex-core/src/test/java/convex/core/BeliefMergeTest.java b/convex-core/src/test/java/convex/core/BeliefMergeTest.java new file mode 100644 index 000000000..d0796f978 --- /dev/null +++ b/convex-core/src/test/java/convex/core/BeliefMergeTest.java @@ -0,0 +1,497 @@ +package convex.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Instant; +import java.util.HashSet; +import java.util.Random; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +import convex.core.crypto.Ed25519KeyPair; +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.core.data.BlobMap; +import convex.core.data.BlobMaps; +import convex.core.data.PeerStatus; +import convex.core.data.RecordTest; +import convex.core.data.SignedData; +import convex.core.data.Vectors; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.BadSignatureException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.Juice; +import convex.core.lang.RT; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Transfer; +import convex.core.util.Text; +import convex.core.util.Utils; + +@Execution(value = ExecutionMode.CONCURRENT) +public class BeliefMergeTest { + + public static final int NUM_PEERS = 9; + public static final int NUM_INITIAL_TRANS = 10; + public static final int ROUNDS = 20; + + public static final Ed25519KeyPair[] KEY_PAIRS = new Ed25519KeyPair[NUM_PEERS]; + public static final Address[] ADDRESSES = new Address[NUM_PEERS]; + public static final AccountKey[] KEYS = new AccountKey[NUM_PEERS]; + public static final State INITIAL_STATE; + private static final long TEST_TIMESTAMP = Instant.parse("1977-11-13T00:30:00Z").toEpochMilli(); + private static final long TOTAL_VALUE; + + static { + // long seed=new Random().nextLong(); + long seed = 2654733563337952L; + // System.out.println("Generating with seed: "+seed); + AVector accounts = Vectors.empty(); + BlobMap peers = BlobMaps.empty(); + for (int i = 0; i < NUM_PEERS; i++) { + Ed25519KeyPair kp = Ed25519KeyPair.createSeeded(seed + i * 17777); + AccountKey key = kp.getAccountKey(); + // TODO numeric addresses + Address address=Address.create(i); + KEY_PAIRS[i] = kp; + KEYS[i] = key; + ADDRESSES[i] = address; + AccountStatus accStatus = AccountStatus.create((i + 1) * 1000000,key); + PeerStatus peerStatus = PeerStatus.create(address,(i + 1) * 100000); + accounts = accounts.conj(accStatus); + peers = peers.assoc(key, peerStatus); + } + + AVector globals = Constants.INITIAL_GLOBALS; + globals = globals.assoc(State.GLOBAL_JUICE_PRICE, RT.cvm(1L)); // cheap juice for simplicity. USe CVM long + INITIAL_STATE = State.create(accounts, peers, globals, BlobMaps.empty()); + TOTAL_VALUE = INITIAL_STATE.computeTotalFunds(); + } + + private Peer initialPeerState(int i) { + return Peer.create(KEY_PAIRS[i], INITIAL_STATE); + } + + private Peer[] initialBeliefs() { + int n = NUM_PEERS; + Peer[] result = new Peer[n]; + for (int i = 0; i < n; i++) { + result[i] = initialPeerState(i); + } + return result; + } + + public Peer[] shareBeliefs(Peer[] initial) throws BadSignatureException, InvalidDataException { + int n = initial.length; + Peer[] result = new Peer[n]; + + // extract beliefs to share + Belief[] sharedBeliefs = new Belief[n]; + for (int j = 0; j < n; j++) + sharedBeliefs[j] = initial[j].getBelief(); + + for (int i = 0; i < n; i++) { + Peer ps = initial[i]; + result[i] = ps.mergeBeliefs(sharedBeliefs); // belief merge step + } + return result; + } + + public Peer[] shareGossip(Peer[] initial, int numGossips, int round) + throws BadSignatureException, InvalidDataException { + Random r = new Random(107701 + round * 1337); + int n = initial.length; + Peer[] result = new Peer[n]; + + Belief[] sharedBeliefs = new Belief[n]; + for (int j = 0; j < n; j++) + sharedBeliefs[j] = initial[j].getBelief(); + + for (int i = 0; i < n; i++) { + Peer ps = initial[i]; + Belief[] sources = new Belief[numGossips]; + for (int j = 0; j < numGossips; j++) { + sources[j] = sharedBeliefs[r.nextInt(n)]; + } + result[i] = ps.mergeBeliefs(sources); // belief merge step + } + return result; + } + + @SuppressWarnings("unchecked") + private Peer[] proposeTransactions(Peer[] initial, int peerIndex, ATransaction... transactions) + throws BadSignatureException { + Peer[] result = initial.clone(); + Peer ps = initial[peerIndex]; // current per under consideration + + // create a block of transactions + int tcount = transactions.length; + SignedData[] signedTransactions = (SignedData[]) new SignedData[tcount]; + for (int ix = 0; ix < tcount; ix++) { + signedTransactions[ix] = initial[peerIndex].sign(transactions[ix]); + } + long newTimeStamp = ps.getTimeStamp() + peerIndex + 100; + Block block = Block.of(newTimeStamp, KEYS[peerIndex], signedTransactions); + + ps = ps.proposeBlock(block); + result[peerIndex] = ps; + return result; + } + + @Test + public void testBasicMerge() throws BadSignatureException, InvalidDataException { + Peer b0 = initialPeerState(0); + Peer b1 = initialPeerState(1); + assertNotEquals(b0, b1); // should not be equal - no knowledge of other peer chains yet + + Peer bm0 = b0.mergeBeliefs(b1.getBelief()); + assertTrue(b0.getPeerOrder() == bm0.getPeerOrder()); + + // propose a new block by peer 1, after 200ms + long newTimestamp1 = TEST_TIMESTAMP + 200; + b1 = b1.updateTimestamp(b1.getTimeStamp() + 200); + assertEquals(0, b1.getPeerOrder().getBlocks().size()); + Peer b1a = b1.proposeBlock(Block.of(newTimestamp1,KEYS[1])); // empty block, just with timestamp + assertEquals(1, b1a.getPeerOrder().getBlocks().size()); + + // merge updated belief, new proposed block should be included + Peer bm2 = b0.mergeBeliefs(b1a.getBelief()); + assertEquals(b1a.getPeerOrder().getBlocks(), bm2.getPeerOrder().getBlocks()); + } + + /** + * This test creates a set of peers, and a single transaction sending tokens + * from the first peers to the last peer Each round of peers updates is + * gossipped simultaneously and the results checked at each stage To validate + * correct propagation of the new block across the network + * @throws BadSignatureException + * @throws InvalidDataException + * @throws BadFormatException + */ + @Test + public void testSingleBlockConsensus() throws BadSignatureException, InvalidDataException, BadFormatException { + boolean ANALYSIS = false; + Peer[] bs0 = initialBeliefs(); + assertNotEquals(bs0[0].getBelief(), bs0[1].getBelief()); // only have own beliefs + validateBeliefs(bs0); + + Peer[] bs1 = shareBeliefs(bs0); // sync all beliefs + assertTrue(allBeliefsEqual(bs1)); // should share beliefs + + Peer[] bs2 = shareBeliefs(bs1); // sync again, should be idempotent + assertEquals(bs1[0].getBelief(), bs2[0].getBelief()); // belief should not change for peer 0 + assertTrue(allBeliefsEqual(bs2)); // beliefs across peers should be equal + + int PROPOSER = 0; + int RECEIVER = NUM_PEERS - 1; + Address PADDRESS = ADDRESSES[PROPOSER]; + Address RADDRESS = ADDRESSES[RECEIVER]; + AccountKey PKEY = KEYS[PROPOSER]; + AccountKey RKEY = KEYS[RECEIVER]; + long INITIAL_BALANCE_PROPOSER = INITIAL_STATE.getBalance(PADDRESS); + long INITIAL_BALANCE_RECEIVER = INITIAL_STATE.getBalance(RADDRESS); + long TRANSFER_AMOUNT = 100; + long TJUICE=Juice.TRANSFER; + + ATransaction trans = Transfer.create(PADDRESS, 1, RADDRESS, TRANSFER_AMOUNT); // note 1 = first sequence number required + Peer[] bs3 = proposeTransactions(bs2, PROPOSER, trans); + if (ANALYSIS) printAnalysis(bs3, "Make proposal"); + assertEquals(1, bs3[PROPOSER].getOrder(PKEY).getBlockCount()); + assertEquals(0, bs3[RECEIVER].getOrder(PKEY).getBlockCount()); + + // New block should win vote for all peers, but not achieve enough support + // for proposed consensus yet + Peer[] bs4 = shareBeliefs(bs3); + if (ANALYSIS) printAnalysis(bs4, "Share 1st round, each peer should adopt proposed block"); + assertEquals(1, bs4[PROPOSER].getOrder(PKEY).getBlockCount()); + assertEquals(1, bs4[RECEIVER].getOrder(RKEY).getBlockCount()); + assertEquals(0, bs4[PROPOSER].getOrder(RKEY).getBlockCount()); // proposer can't see block in receiver's + // chain yet + + // all peers should propose new consensus, but not confirmed yet + assertEquals(0, bs4[PROPOSER].getOrder(PKEY).getProposalPoint()); + assertEquals(0, bs4[RECEIVER].getOrder(RKEY).getProposalPoint()); + Peer[] bs5 = shareBeliefs(bs4); + if (ANALYSIS) printAnalysis(bs5, + "Share 2nd round: each peer should propose consensus after seeing majority for new block"); + assertEquals(1, bs5[PROPOSER].getOrder(PKEY).getProposalPoint()); + assertEquals(1, bs5[RECEIVER].getOrder(RKEY).getProposalPoint()); + + // all peers should now agree on consensus, but don't know each other's + // consensus yet + assertEquals(0, bs5[PROPOSER].getOrder(PKEY).getConsensusPoint()); + assertEquals(0, bs5[RECEIVER].getOrder(RKEY).getConsensusPoint()); + assertEquals(0, bs5[PROPOSER].getOrder(RKEY).getConsensusPoint()); + Peer[] bs6 = shareBeliefs(bs5); + if (ANALYSIS) printAnalysis(bs6, + "Share 3nd round: each peer should confirm consensus after seeing proposals from others"); + assertEquals(1, bs6[PROPOSER].getOrder(PKEY).getConsensusPoint()); + assertEquals(1, bs6[RECEIVER].getOrder(RKEY).getConsensusPoint()); + assertEquals(0, bs6[PROPOSER].getOrder(RKEY).getConsensusPoint()); + + Peer[] bs7 = shareBeliefs(bs6); + if (ANALYSIS) printAnalysis(bs7, "Share 4th round: should reach full consensus, confirmations shared"); + assertEquals(1, bs7[PROPOSER].getOrder(RKEY).getConsensusPoint()); // proposer now sees receivers consensus + + // final state checks + assertTrue(allBeliefsEqual(bs7)); // beliefs across peers should be equal + State finalState = bs7[0].getConsensusState(); + assertEquals(INITIAL_BALANCE_PROPOSER-TRANSFER_AMOUNT-TJUICE, finalState.getBalance(PADDRESS)); + assertEquals(INITIAL_BALANCE_RECEIVER+TRANSFER_AMOUNT, finalState.getBalance(RADDRESS)); + + // matter cannot be created or destroyed.... + assertEquals(TOTAL_VALUE, finalState.computeTotalFunds()); + } + + /** + * This test creates a set of peers, and one transaction for each peer Each + * round of peers updates is gossipped simultaneously and the results checked at + * each stage To validate correct propagation of the new block across the + * network + * @throws BadSignatureException + * @throws InvalidDataException + * @throws BadFormatException + */ + @Test + public void testMultiBlockConsensus() throws BadSignatureException, InvalidDataException, BadFormatException { + boolean ANALYSIS = false; + Peer[] bs0 = initialBeliefs(); + assertFalse(allBeliefsEqual(bs0)); // only have own beliefs + validateBeliefs(bs0); + + Peer[] bs1 = shareBeliefs(bs0); // sync all beliefs + assertTrue(allBeliefsEqual(bs1)); // should see other beliefs + + Peer[] bs2 = shareBeliefs(bs1); // sync again, should be idempotent + assertEquals(bs1[0].getBelief(), bs2[0].getBelief()); // belief should not change + assertTrue(allBeliefsEqual(bs2)); // beliefs across peers should be equal + + int PROPOSER = 0; + int RECEIVER = NUM_PEERS - 1; + Address PADDRESS = ADDRESSES[PROPOSER]; + Address RADDRESS = ADDRESSES[RECEIVER]; + AccountKey PKEY = KEYS[PROPOSER]; + AccountKey RKEY = KEYS[RECEIVER]; + Long INITIAL_BALANCE_PROPOSER = INITIAL_STATE.getBalance(PADDRESS); + Long INITIAL_BALANCE_RECEIVER = INITIAL_STATE.getBalance(RADDRESS); + long TJUICE=Juice.TRANSFER; + + Peer[] bs3 = bs2; + for (int i = 0; i < NUM_PEERS; i++) { + long TRANSFER_AMOUNT = 100L; + ATransaction trans = Transfer.create(ADDRESSES[i],1, ADDRESSES[NUM_PEERS - 1 - i], TRANSFER_AMOUNT); // note 1 = first + // sequence number + // required + bs3 = proposeTransactions(bs3, i, trans); + } + if (ANALYSIS) printAnalysis(bs3, "Make proposals"); + assertEquals(1, bs3[0].getOrder(PKEY).getBlockCount()); + assertEquals(1, bs3[RECEIVER].getOrder(RKEY).getBlockCount()); + assertEquals(0, bs3[RECEIVER].getOrder(PKEY).getBlockCount()); + + // New block should win vote for all peers, but not achieve enough support + // for proposed consensus yet + Peer[] bs4 = shareBeliefs(bs3); + if (ANALYSIS) + printAnalysis(bs4, "Share 1st round, each peer should see others chains, vote for same plus new blocks"); + assertEquals(NUM_PEERS, bs4[PROPOSER].getOrder(PKEY).getBlockCount()); + assertEquals(NUM_PEERS, bs4[RECEIVER].getOrder(RKEY).getBlockCount()); + assertEquals(1, bs4[PROPOSER].getOrder(RKEY).getBlockCount()); // proposer can only see 1st block from + // receiver + assertEquals(0, bs4[PROPOSER].getOrder(PKEY).getProposalPoint()); + assertEquals(0, bs4[RECEIVER].getOrder(RKEY).getProposalPoint()); + + // Next round + Peer[] bs5 = shareBeliefs(bs4); + if (ANALYSIS) printAnalysis(bs5, "Share 2nd round: "); + + // all peers should now agree on consensus + Peer[] bs6 = shareBeliefs(bs5); + if (ANALYSIS) printAnalysis(bs6, "Share 3nd round: "); + + Peer[] bs7 = shareBeliefs(bs6); + if (ANALYSIS) printAnalysis(bs7, "Share 4th round: should reach full consensus?"); + assertEquals(NUM_PEERS, bs7[PROPOSER].getOrder(RKEY).getConsensusPoint()); // proposer now sees receivers + // consensus + + // final state checks + assertTrue(allBeliefsEqual(bs7)); + State finalState = bs7[0].getConsensusState(); + // should have 1 transaction each + assertEquals(1L, finalState.getAccount(PADDRESS).getSequence()); + assertEquals(1L, finalState.getAccount(RADDRESS).getSequence()); + assertEquals(INITIAL_BALANCE_PROPOSER-TJUICE, finalState.getBalance(PADDRESS)); + assertEquals(INITIAL_BALANCE_RECEIVER-TJUICE, finalState.getBalance(RADDRESS)); + + // law of conservation of gil + assertEquals(TOTAL_VALUE, finalState.computeTotalFunds()); + } + + private boolean allBeliefsEqual(Peer[] pss) { + int n = pss.length; + for (int i = 0; i < n - 1; i++) { + if (!Utils.equals(pss[i].getBelief(), pss[i + 1].getBelief())) return false; + } + return true; + } + + /** + * This test creates a set of peers, and one transaction for each peer Each + * round of peers updates is gossipped partially To validate correct propagation + * of the new block across the network + * @throws BadSignatureException + * @throws InvalidDataException + * @throws BadFormatException + */ + @Test + public void testGossipConsensus() throws BadSignatureException, InvalidDataException, BadFormatException { + boolean ANALYSIS = false; + int GOSSIP_NUM = 4; + + Peer[] bs0 = initialBeliefs(); + if (ANALYSIS) printAnalysis(bs0, "Initial beliefs"); + assertFalse(allBeliefsEqual(bs0)); // only have own beliefs + validateBeliefs(bs0); + + Peer[] bs1 = shareBeliefs(bs0); // sync all beliefs + assertTrue(allBeliefsEqual(bs1)); // should see other beliefs + + Peer[] bs2 = shareBeliefs(bs1); // sync again, should be idempotent + assertEquals(bs1[0].getBelief(), bs2[0].getBelief()); // belief should not change + assertTrue(allBeliefsEqual(bs2)); // beliefs across peers should be equal + if (ANALYSIS) printAnalysis(bs2, "Shared beliefs"); + + int PROPOSER = 0; + int RECEIVER = NUM_PEERS - 1; + Address PADDRESS = ADDRESSES[PROPOSER]; + Address RADDRESS = ADDRESSES[RECEIVER]; + AccountKey PKEY = KEYS[PROPOSER]; + AccountKey RKEY = KEYS[RECEIVER]; + long INITIAL_BALANCE_PROPOSER = INITIAL_STATE.getBalance(PADDRESS); + long INITIAL_BALANCE_RECEIVER = INITIAL_STATE.getBalance(RADDRESS); + long TJUICE=Juice.TRANSFER*NUM_INITIAL_TRANS; + + + Peer[] bs3 = bs2; + for (int i = 0; i < NUM_PEERS; i++) { + // propose initial transactions + for (int j = 1; j <= NUM_INITIAL_TRANS; j++) { + long TRANSFER_AMOUNT = 100L; + ATransaction trans = Transfer.create(ADDRESSES[i],j, ADDRESSES[NUM_PEERS - 1 - i], TRANSFER_AMOUNT); // note 1 = + // first + // sequence + // number + // required + bs3 = proposeTransactions(bs3, i, trans); + } + } + if (ANALYSIS) printAnalysis(bs3, "Make proposals"); + assertEquals(NUM_INITIAL_TRANS, bs3[0].getOrder(PKEY).getBlockCount()); + assertEquals(NUM_INITIAL_TRANS, bs3[RECEIVER].getOrder(RKEY).getBlockCount()); + assertEquals(0, bs3[RECEIVER].getOrder(PKEY).getBlockCount()); + + Peer[] bs4 = bs3; + for (int i = 1; i < ROUNDS; i++) { + bs4 = shareGossip(bs4, GOSSIP_NUM, i); + if (ANALYSIS) printAnalysis(bs4, "Share round: " + i); + } + bs4 = shareGossip(bs4, GOSSIP_NUM, ROUNDS); + if (ANALYSIS) printAnalysis(bs4, "Share round: " + ROUNDS); + + // final state checks + assertEquals(NUM_PEERS * NUM_INITIAL_TRANS, bs4[PROPOSER].getOrder(RKEY).getConsensusPoint()); // proposer + // now sees + // receivers + // consensus + assertTrue(allBeliefsEqual(bs4)); + + Order finalChain = bs4[0].getOrder(PKEY); + AVector finalBlocks = finalChain.getBlocks(); + assertEquals(NUM_PEERS * NUM_INITIAL_TRANS, new HashSet<>(finalBlocks).size()); + + State finalState = bs4[0].getConsensusState(); + AVector accounts = finalState.getAccounts(); + if (ANALYSIS) printAccounts(accounts); + + // should have correct number of transactions each + for (int i = 0; i < NUM_PEERS; i++) { + assertEquals(NUM_INITIAL_TRANS, accounts.get(ADDRESSES[i].longValue()).getSequence()); + } + // should have equal balance + assertEquals(INITIAL_BALANCE_PROPOSER-TJUICE, finalState.getBalance(PADDRESS)); + assertEquals(INITIAL_BALANCE_RECEIVER-TJUICE, finalState.getBalance(RADDRESS)); + + // 100% of value still exists + assertEquals(TOTAL_VALUE, finalState.computeTotalFunds()); + + RecordTest.doRecordTests(bs4[0].getBelief()); + RecordTest.doRecordTests(finalState); + } + + private void printAccounts(AVector accounts) { + System.out.println("===== Accounts ====="); + for (int i = 0; i < NUM_PEERS; i++) { + Address address = ADDRESSES[i]; + AccountStatus as = accounts.get(address); + System.out.println(peerString(i) + " " + as); + } + } + + private void validateBeliefs(Peer[] pss) throws InvalidDataException, BadFormatException { + for (Peer ps : pss) { + ps.getBelief().validate(); + } + } + + private static void printAnalysis(Peer[] beliefs, String msg) throws BadSignatureException { + System.out.println("===== " + msg + " ====="); + for (int i = 0; i < NUM_PEERS; i++) { + if ((i >= 5) && (i < NUM_PEERS - 5)) { + System.out.println(".... (" + (NUM_PEERS - 10) + " peers skipped)"); + i = NUM_PEERS - 6; + continue; + } + Peer ps = beliefs[i]; + Belief b = beliefs[i].getBelief(); + + Order c = b.getOrder(KEYS[i]); + int agreedPeers = 0; + String mat = ""; + for (int j = 0; j < NUM_PEERS; j++) { + if ((j >= 5) && (j < NUM_PEERS - 5)) { + mat += " ...."; + j = NUM_PEERS - 6; + continue; + } + Order jc = b.getOrder(KEYS[j]); + mat += " " + ((jc == null) ? "----" : jc.getHash().toHexString(4)); + if (c.equals(jc)) agreedPeers++; + } + + long clen = c.getBlockCount(); + String blockRep = ""; + for (int ix = 0; ix < clen; ix++) { + blockRep += " " + c.getBlock(ix).getHash().toHexString(2); + } + + long pp = c.getProposalPoint(); + long cp = c.getConsensusPoint(); + System.out.println(peerString(i) + " clen:" + Text.leftPad(clen, 3) + " prop:" + Text.leftPad(pp, 3) + + " cons:" + Text.leftPad(cp, 3) + " state:" + ps.getConsensusState().getHash().toHexString(4) + + " hash: " + c.getHash().toHexString(4) + " mat:" + mat + " agreed: " + agreedPeers + "/" + + b.getOrders().count() + " blks:" + blockRep); + } + } + + private static String peerString(int i) { + return "Peer: " + Text.rightPad(i, 4) + " [" + ADDRESSES[i].toHexString(4) + "]"; + } + +} diff --git a/convex-core/src/test/java/convex/core/BeliefVotingTest.java b/convex-core/src/test/java/convex/core/BeliefVotingTest.java new file mode 100644 index 000000000..11cc2137c --- /dev/null +++ b/convex-core/src/test/java/convex/core/BeliefVotingTest.java @@ -0,0 +1,16 @@ +package convex.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import convex.core.data.Maps; + +public class BeliefVotingTest { + + @Test + public void testComputeVote() { + assertEquals(100.0, Belief.computeVote(Maps.hashMapOf(1, 50.0, 0, 50.0)), 0.000001); + assertEquals(0.0, Belief.computeVote(Maps.hashMapOf()), 0.000001); + } +} diff --git a/convex-core/src/test/java/convex/core/MessageSizeTest.java b/convex-core/src/test/java/convex/core/MessageSizeTest.java new file mode 100644 index 000000000..adb50e591 --- /dev/null +++ b/convex-core/src/test/java/convex/core/MessageSizeTest.java @@ -0,0 +1,32 @@ +package convex.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import convex.core.data.Format; +import convex.core.data.StringShort; +import convex.test.Samples; + +public class MessageSizeTest { + + @Test public void testMaxEmbedded() { + assertEquals(Format.MAX_EMBEDDED_LENGTH,Samples.MAX_EMBEDDED_BLOB.getEncoding().count()); + assertTrue(Samples.MAX_EMBEDDED_BLOB.isEmbedded()); + + assertEquals(Format.MAX_EMBEDDED_LENGTH+1,Samples.NON_EMBEDDED_BLOB.getEncoding().count()); + assertFalse(Samples.NON_EMBEDDED_BLOB.isEmbedded()); + } + + @Test public void testEmbeddedStrings() { + assertTrue(Format.MAX_EMBEDDED_LENGTH>=Samples.MAX_EMBEDDED_STRING.getEncoding().count()); + assertEquals(StringShort.MAX_EMBEDDED_STRING_LENGTH,Samples.MAX_EMBEDDED_STRING.length()); + assertTrue(Samples.MAX_EMBEDDED_STRING.isEmbedded()); + + assertTrue(Format.MAX_EMBEDDED_LENGTHr)); + + RecordTest.doRecordTests(r1); + + } + + @Test + public void testResultCreation() { + Result r1=Result.create(CVMLong.create(0L),RT.cvm(1L),null); + + RecordTest.doRecordTests(r1); + } + +} diff --git a/convex-core/src/test/java/convex/core/StakingTest.java b/convex-core/src/test/java/convex/core/StakingTest.java new file mode 100644 index 000000000..31f336778 --- /dev/null +++ b/convex-core/src/test/java/convex/core/StakingTest.java @@ -0,0 +1,71 @@ +package convex.core; + +import static convex.test.Assertions.assertArgumentError; +import static convex.test.Assertions.assertFundsError; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import convex.core.data.ACell; +import convex.core.data.PeerStatus; +import convex.core.init.InitTest; +import convex.core.lang.ACVMTest; +import convex.core.lang.Context; + +public class StakingTest extends ACVMTest { + + protected StakingTest() { + super(InitTest.STATE); + } + + @Test + public void testDelegatedStaking() { + + } + + @Test + public void testStake() { + Context ctx0 =context(); + + Context ctx1 = ctx0.setDelegatedStake(InitTest.FIRST_PEER_KEY, 1000); + PeerStatus ps1 = ctx1.getState().getPeer(InitTest.FIRST_PEER_KEY); + assertEquals(1000L, ps1.getDelegatedStake()); + + // round tripping this should return to initial state precisely + // since we are not consuming any juice here, or adjusting anything other than + // stake positions + Context ctx2 = ctx1.setDelegatedStake(InitTest.FIRST_PEER_KEY, 0); + assertEquals(ctx0.getState(), ctx2.getState()); + + // test putting entire balance on stake + Context ctx3 = step(ctx0, "(stake " + InitTest.FIRST_PEER_KEY + " *balance*)"); + assertEquals(0L, ctx3.getBalance(InitTest.HERO)); + assertEquals(HERO_BALANCE, ctx3.getState().getPeer(InitTest.FIRST_PEER_KEY).getDelegatedStake(InitTest.HERO)); + + // test putting too much balance + assertFundsError(step(ctx0, "(stake " + InitTest.FIRST_PEER_KEY + " (inc *balance*))")); + } + + @Test + public void testStakeReturns() { + Context ctx0 = context(); + assertEquals(1000L, evalL(ctx0, "(stake " + InitTest.FIRST_PEER_KEY + " 1000)")); + } + + @Test + public void testBadStake() { + Context ctx0 = context(); + + // TODO: new test since HERO is now a peer manager + // not a peer, should be state error + //assertStateError(ctx0.setDelegatedStake(InitTest.HERO_KEYPAIR.getAccountKey(), 1000)); + + // bad arguments, out of range + assertArgumentError(ctx0.setDelegatedStake(InitTest.FIRST_PEER_KEY, -1)); + assertArgumentError(ctx0.setDelegatedStake(InitTest.FIRST_PEER_KEY, Long.MAX_VALUE)); + + // insufficient funds for stake + assertFundsError(ctx0.setDelegatedStake(InitTest.FIRST_PEER_KEY, Constants.MAX_SUPPLY)); + assertFundsError(ctx0.setDelegatedStake(InitTest.FIRST_PEER_KEY, ctx0.getBalance(InitTest.HERO) + 1)); + } +} diff --git a/convex-core/src/test/java/convex/core/StateTest.java b/convex-core/src/test/java/convex/core/StateTest.java new file mode 100644 index 000000000..b9f211624 --- /dev/null +++ b/convex-core/src/test/java/convex/core/StateTest.java @@ -0,0 +1,72 @@ +package convex.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.AccountStatus; +import convex.core.data.Blob; +import convex.core.data.Format; +import convex.core.data.RecordTest; +import convex.core.data.Ref; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.init.InitTest; + +/** + * Tests for the State data structure + */ +public class StateTest { + State INIT_STATE=InitTest.createState(); + + @Test + public void testEmptyState() { + State s = State.EMPTY; + AVector accts = s.getAccounts(); + assertEquals(0, accts.count()); + + RecordTest.doRecordTests(s); + } + + @Test + public void testInitialState() throws InvalidDataException { + State s = INIT_STATE; + assertSame(s, s.withAccounts(s.getAccounts())); + assertSame(s, s.withPeers(s.getPeers())); + + s.validate(); + + RecordTest.doRecordTests(s); + } + + @Test + public void testRoundTrip() throws BadFormatException { + State s = INIT_STATE; + // TODO: fix this + // s=s.store(Keywords.STATE); + // assertEquals(1,s.getStore().size()); + + assertEquals(0,s.getRef().getStatus()); + + Ref rs = ACell.createPersisted(s); + assertEquals(Ref.PERSISTED, rs.getStatus()); + + // Initial ref should now have persisted status + assertTrue(s.getRef().isPersisted()); + + Blob b = Format.encodedBlob(s); + State s2 = Format.read(b); + assertEquals(s, s2); + + AccountStatus as=s2.getAccount(InitTest.HERO); + assertNotNull(as); + + RecordTest.doRecordTests(s2); + RecordTest.doRecordTests(as); + } +} diff --git a/convex-core/src/test/java/convex/core/StateTransitionsTest.java b/convex-core/src/test/java/convex/core/StateTransitionsTest.java new file mode 100644 index 000000000..1f67caaed --- /dev/null +++ b/convex-core/src/test/java/convex/core/StateTransitionsTest.java @@ -0,0 +1,317 @@ +package convex.core; + +import static convex.test.Assertions.assertCVMEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.Test; + +import convex.core.crypto.AKeyPair; +import convex.core.crypto.Ed25519KeyPair; +import convex.core.data.ABlob; +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.core.data.BlobMap; +import convex.core.data.SignedData; +import convex.core.data.Vectors; +import convex.core.exceptions.BadSignatureException; +import convex.core.init.InitTest; +import convex.core.lang.Juice; +import convex.core.lang.Reader; +import convex.core.lang.TestState; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Invoke; +import convex.core.transactions.Transfer; +import convex.core.util.Utils; + +/** + * Tests for State transition scenarios + */ +public class StateTransitionsTest { + + final AKeyPair KEYPAIR_A = Ed25519KeyPair.createSeeded(1001); + final AKeyPair KEYPAIR_B = Ed25519KeyPair.createSeeded(1002); + final AKeyPair KEYPAIR_C = Ed25519KeyPair.createSeeded(1003); + final AKeyPair KEYPAIR_NIKI = Ed25519KeyPair.createSeeded(1004); + final AKeyPair KEYPAIR_ROBB = Ed25519KeyPair.createSeeded(1005); + + final AKeyPair KEYPAIR_PEER = Ed25519KeyPair.createSeeded(1006); + final AccountKey FIRST_PEER_KEY=KEYPAIR_PEER.getAccountKey(); + + final Address ADDRESS_A = Address.create(0); // initial account + final Address ADDRESS_B = Address.create(1); // initial account + final Address ADDRESS_ROBB = Address.create(2); // initial account + final Address ADDRESS_C = Address.create(3); + + final Address ADDRESS_NIKI = Address.create(4); + + @Test + public void testAccountTransfers() throws BadSignatureException { + AVector accounts = Vectors.of( + AccountStatus.create(10000L,KEYPAIR_A.getAccountKey()), + AccountStatus.create(1000L,KEYPAIR_B.getAccountKey()), + AccountStatus.create(Constants.MAX_SUPPLY - 10000 - 1000,KEYPAIR_ROBB.getAccountKey()) + // No account for C yet + ); + State s = State.EMPTY.withAccounts(accounts); // don't need any peers for these tests + assertEquals(Constants.MAX_SUPPLY, s.computeTotalFunds()); + + assertEquals(10000, s.getBalance(ADDRESS_A)); + assertEquals(1000, s.getBalance(ADDRESS_B)); + assertNull(s.getBalance(ADDRESS_C)); + + long TCOST = Juice.TRANSFER * s.getJuicePrice().longValue(); + + { // transfer from existing to existing account A->B + Transfer t1 = Transfer.create(ADDRESS_A,1, ADDRESS_B, 50); + SignedData st = KEYPAIR_A.signData(t1); + long nowTS = Utils.getCurrentTimestamp(); + Block b = Block.of(nowTS,FIRST_PEER_KEY, st); + BlockResult br = s.applyBlock(b); + AVector results = br.getResults(); + assertEquals(1, results.count()); + assertNull(br.getErrorCode(0),br.getResult(0).toString()); // should be null for successful transfer transaction + State s2 = br.getState(); + assertEquals(9950 - TCOST, s2.getBalance(ADDRESS_A)); + assertEquals(1050, s2.getBalance(ADDRESS_B)); + assertCVMEquals(nowTS, s2.getTimeStamp()); + } + + { // transfer from existing to non-existing account A -> C + Transfer t1 = Transfer.create(ADDRESS_A,1, ADDRESS_C, 50); + SignedData st = KEYPAIR_A.signData(t1); + Block b = Block.of(System.currentTimeMillis(),FIRST_PEER_KEY, st); + State s2 = s.applyBlock(b).getState(); + + // no transfer should have happened, although cost should have been paid + assertEquals(10000 - TCOST, s2.getBalance(ADDRESS_A)); + assertNull(s2.getBalance(ADDRESS_C)); + } + + { // transfer from a non-existent address + Transfer t1 = Transfer.create(ADDRESS_C,1, ADDRESS_B, 50); + SignedData st = KEYPAIR_C.signData(t1); + Block b = Block.of(System.currentTimeMillis(),FIRST_PEER_KEY, st); + BlockResult br=s.applyBlock(b); + assertEquals(ErrorCodes.NOBODY, br.getResult(0).getErrorCode()); + + } + + { // transfer from existing to new account A -> C + // First create new account C + State s0=s.putAccount(ADDRESS_C, AccountStatus.create(0L,KEYPAIR_C.getAccountKey())); + + Transfer t1 = Transfer.create(ADDRESS_A,1, ADDRESS_C, 50); + SignedData st = KEYPAIR_A.signData(t1); + Block b = Block.of(System.currentTimeMillis(),FIRST_PEER_KEY, st); + State s2 = s0.applyBlock(b).getState(); + + // Transfer should have happened + assertEquals(9950 - TCOST, s2.getBalance(ADDRESS_A)); + assertEquals(50, s2.getBalance(ADDRESS_C)); + } + + { // two transfers in sequence, both from A -> C + // First create new account C + State s0=s.putAccount(ADDRESS_C, AccountStatus.create(0L,KEYPAIR_C.getAccountKey())); + + Transfer t1 = Transfer.create(ADDRESS_A,1, ADDRESS_C, 150); + SignedData st1 = KEYPAIR_A.signData(t1); + Transfer t2 = Transfer.create(ADDRESS_A,2, ADDRESS_C, 150); + SignedData st2 = KEYPAIR_A.signData(t2); + Block b = Block.of(System.currentTimeMillis(),FIRST_PEER_KEY, st1, st2); + + BlockResult br = s0.applyBlock(b); + State s2 = br.getState(); + assertEquals(9700 - TCOST * 2, s2.getBalance(ADDRESS_A)); + assertEquals(1000, s2.getBalance(ADDRESS_B)); + assertEquals(300, s2.getBalance(ADDRESS_C)); + } + + { // two transfers in sequence, 2 different accounts A B --> new account C + // First create new account C + State s0=s.putAccount(ADDRESS_C, AccountStatus.create(0L,KEYPAIR_C.getAccountKey())); + + Transfer t1 = Transfer.create(ADDRESS_A,1, ADDRESS_C, 50); + SignedData st1 = KEYPAIR_A.signData(t1); + Transfer t2 = Transfer.create(ADDRESS_B,1, ADDRESS_C, 50); + SignedData st2 = KEYPAIR_B.signData(t2); + Block b = Block.of(System.currentTimeMillis(),FIRST_PEER_KEY, st1, st2); + + BlockResult br = s0.applyBlock(b); + State s2 = br.getState(); + assertEquals(9950 - TCOST, s2.getBalance(ADDRESS_A)); + assertEquals(950 - TCOST, s2.getBalance(ADDRESS_B)); + assertEquals(100, s2.getBalance(ADDRESS_C)); + + AVector results = br.getResults(); + assertEquals(2, results.count()); + assertCVMEquals(50L,br.getResult(0).getValue()); // result for successful transfer + assertEquals(Constants.MAX_SUPPLY, br.getState().computeTotalFunds()); + } + + { // transfer with an incorrect sequence number + Transfer t1 = Transfer.create(ADDRESS_A,2, ADDRESS_C, 50); + SignedData st = KEYPAIR_A.signData(t1); + Block b = Block.of(System.currentTimeMillis(),FIRST_PEER_KEY, st); + BlockResult br = s.applyBlock(b); + AVector results = br.getResults(); + assertEquals(1, results.count()); + assertEquals(ErrorCodes.SEQUENCE, br.getResult(0).getErrorCode()); + } + + { // transfer amount greater than current balance + Transfer t1 = Transfer.create(ADDRESS_A,1, ADDRESS_C, 50000); + SignedData st = KEYPAIR_A.signData(t1); + Block b = Block.of(System.currentTimeMillis(),FIRST_PEER_KEY, st); + BlockResult br = s.applyBlock(b); + assertEquals(ErrorCodes.FUNDS, br.getResult(0).getErrorCode()); + + State newState = br.getState(); + assertEquals(Constants.MAX_SUPPLY, newState.computeTotalFunds()); + } + + + + { // sending money to NIKI, a new account + // Two new Accounts + State s0=s.putAccount(ADDRESS_C, AccountStatus.create(0L,KEYPAIR_C.getAccountKey())); + s0=s0.putAccount(ADDRESS_NIKI, AccountStatus.create(0L,KEYPAIR_NIKI.getAccountKey())); + + // System.out.println(ADDRESS_NIKI); + // System.out.println("Niki has "+s.getBalance(ADDRESS_NIKI).getValue()); + + long AMT = 500; + // System.out.println("Tansferring "+AMT+" to Niki"); + + Transfer t1 = Transfer.create(ADDRESS_A,1, ADDRESS_NIKI, AMT); + SignedData st = KEYPAIR_A.signData(t1); + Block b = Block.of(System.currentTimeMillis(),FIRST_PEER_KEY, st); + BlockResult br = s0.applyBlock(b); + // System.out.println("Transfer complete...."); + + State newState = br.getState(); + assertEquals(AMT, newState.getBalance(ADDRESS_NIKI)); + // System.out.println("Niki has "+newState.getBalance(ADDRESS_NIKI).getValue()); + + } + + } + + @Test + public void testDeploys() throws BadSignatureException { + State s = TestState.STATE; + ATransaction t1 = Invoke.create(InitTest.HERO,1,Reader.read("(def my-lib-address (deploy '(defn foo [x] x)))")); + AKeyPair kp = InitTest.HERO_KEYPAIR; + Block b1 = Block.of(s.getTimeStamp().longValue(),FIRST_PEER_KEY, kp.signData(t1)); + BlockResult br=s.applyBlock(b1); + assertFalse(br.isError(0),br.getResult(0).toString()); + + s = br.getState(); + + } + + @Test public void testManyDeploysMemoryRegression() throws BadSignatureException { + State s=TestState.STATE; + long lastSize=s.getMemorySize(); + assertTrue(lastSize>0); + + for (int i=1; i<=100; i++) { // i is sequence number + ATransaction trans=Invoke.create(InitTest.HERO, i, Reader.read("(def storage-example\r\n" + + " (deploy '(do (def stored-data nil)\r\n" + + " (defn get ^{:callable? true} [] stored-data)\r\n" + + " (defn set ^{:callable? true} [x] (def stored-data x)))))\r\n")); + AKeyPair kp = InitTest.HERO_KEYPAIR; + Block b=Block.of(s.getTimeStamp().longValue(),FIRST_PEER_KEY,kp.signData(trans)); + BlockResult br=s.applyBlock(b); + Result r=br.getResult(0); + assertFalse(r.isError(),r.toString()); + assertTrue(r.getValue() instanceof Address); + State newState=br.getState(); + + long size=newState.getMemorySize(); + if (size<=lastSize) { + fail("[i="+i+"] Original size: "+lastSize+" -> new size: "+size); + } + lastSize=size; + s=newState; + } + } + + @Test + public void testMemoryAccounting() throws BadSignatureException { + State s = TestState.STATE; + AKeyPair kp = InitTest.HERO_KEYPAIR; + + long initialMem=s.getMemorySize(); + + ATransaction t1 = Invoke.create(InitTest.HERO,1,Reader.read("(def a 1)")); + Block b1 = Block.of(s.getTimeStamp().longValue(),FIRST_PEER_KEY, kp.signData(t1)); + BlockResult br=s.applyBlock(b1); + + // should not be an error + assertNull(br.getErrorCode(0),br.getResult(0).toString()); + + s = br.getState(); + + // should have increased memory size for account + long newMem=s.getMemorySize(); + assertTrue(initialMem> sched2 = s.getSchedule(); + assertEquals(1L, sched2.count()); + // no change to target balance yet + assertEquals(BAL2 + 10000000, s.getBalance(TARGET)); + + // advance 999ms + ATransaction t3 = Invoke.create(InitTest.HERO,3, Reader.read("1")); + Block b3 = Block.of(s.getTimeStamp().longValue() + 999,FIRST_PEER_KEY, kp.signData(t3)); + BlockResult br3 = s.applyBlock(b3); + assertNull(br3.getErrorCode(0)); + s = br3.getState(); + // no change to target balance yet + assertEquals(BAL2 + 10000000, s.getBalance(TARGET)); + + // advance 1ms to trigger scheduled transfer + ATransaction t4 = Invoke.create(InitTest.HERO,4, Reader.read("1")); + Block b4 = Block.of(s.getTimeStamp().longValue() + 1,FIRST_PEER_KEY, kp.signData(t4)); + BlockResult br4 = s.applyBlock(b4); + assertNull(br4.getErrorCode(0)); + s = br4.getState(); + // no change to target balance yet + assertEquals(BAL2 + 20000000, s.getBalance(TARGET)); + + } + +} diff --git a/convex-core/src/test/java/convex/core/TransactionTest.java b/convex-core/src/test/java/convex/core/TransactionTest.java new file mode 100644 index 000000000..cf99864a4 --- /dev/null +++ b/convex-core/src/test/java/convex/core/TransactionTest.java @@ -0,0 +1,49 @@ +package convex.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.Test; + +import convex.core.data.Address; +import convex.core.init.InitTest; +import convex.core.lang.ACVMTest; +import convex.core.lang.Context; +import convex.core.lang.Juice; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Transfer; + +/** + * Tests for Transactions, especially when applied in isolation to a State + */ +public class TransactionTest extends ACVMTest { + + protected TransactionTest() { + super(InitTest.STATE); + } + + Address HERO=InitTest.HERO; + Address VILLAIN=InitTest.VILLAIN; + long JP=Constants.INITIAL_JUICE_PRICE; + + protected State state() { + return context().getState(); + } + + protected State apply(ATransaction t) { + State s=state(); + Context ctx= s.applyTransaction(t); + assertFalse(ctx.isExceptional()); + return ctx.getState(); + } + + @Test + public void testTransfer() { + Transfer t1=Transfer.create(HERO, 1, VILLAIN, 1000); + State s=apply(t1); + long expectedFees=Juice.TRANSFER*JP; + assertEquals(1000+expectedFees,state().getAccount(HERO).getBalance()-s.getAccount(HERO).getBalance()); + assertEquals(expectedFees,s.getGlobalFees().longValue()); + } + +} diff --git a/convex-core/src/test/java/convex/core/crypto/AccountKeyTest.java b/convex-core/src/test/java/convex/core/crypto/AccountKeyTest.java new file mode 100644 index 000000000..16cfef971 --- /dev/null +++ b/convex-core/src/test/java/convex/core/crypto/AccountKeyTest.java @@ -0,0 +1,20 @@ +package convex.core.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import convex.core.data.AccountKey; +import convex.core.data.Blob; +import convex.core.data.Format; +import convex.core.exceptions.BadFormatException; + +public class AccountKeyTest { + @Test public void testEncoding() throws BadFormatException { + AccountKey ak=AccountKey.dummy("1234"); + Blob b=ak.getEncoding(); + AccountKey ak2=AccountKey.create(Format.read(b)); + + assertEquals(ak,ak2); + } +} diff --git a/convex-core/src/test/java/convex/core/crypto/Ed25519Test.java b/convex-core/src/test/java/convex/core/crypto/Ed25519Test.java new file mode 100644 index 000000000..dec907955 --- /dev/null +++ b/convex-core/src/test/java/convex/core/crypto/Ed25519Test.java @@ -0,0 +1,201 @@ +package convex.core.crypto; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.util.Base64; + +import org.junit.jupiter.api.Test; + +import convex.core.data.ACell; +import convex.core.data.AccountKey; +import convex.core.data.Blob; +import convex.core.data.SignedData; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.RT; + +public class Ed25519Test { + + @Test + public void testKeyGen() { + AKeyPair kp1=Ed25519KeyPair.generate(); + AKeyPair kp2=Ed25519KeyPair.generate(); + assertNotEquals(kp1,kp2); + } + + @Test + public void testPublicKeyBytes() { + Ed25519KeyPair kp1=Ed25519KeyPair.generate(); + byte[] publicBytes=kp1.getPublicKeyBytes(); + byte[] addressBytes=kp1.getAccountKey().getBytes(); + assertArrayEquals(publicBytes,addressBytes); + } + + @Test + public void testKeyRebuilding() { + Ed25519KeyPair kp1=Ed25519KeyPair.generate(); + Ed25519KeyPair kp2=Ed25519KeyPair.create(kp1.getSeed()); + assertEquals(kp1,kp2); + assertEquals(kp1.getAccountKey(),kp2.getAccountKey()); + + ACell data=RT.cvm(1L); + + // TODO: figure out why encodings are different + //assertEquals(kp1.getEncodedPrivateKey(),kp2.getEncodedPrivateKey()); + assertEquals(kp1.signData(data),kp2.signData(data)); + } + + @Test + public void testPrivateKeyBytes() { + Ed25519KeyPair kp1=Ed25519KeyPair.generate(); + PrivateKey priv=kp1.getPrivate(); + PublicKey pub=kp1.getPublic(); + AccountKey address=kp1.getAccountKey(); + + ACell data=RT.cvm(1L); + SignedData sd1=kp1.signData(data); + assertTrue(sd1.checkSignature()); + + byte[] privateKeyBytes=kp1.getPrivate().getEncoded(); + + + Ed25519KeyPair kp2=Ed25519KeyPair.create(pub,priv); + assertEquals(address,kp2.getAccountKey()); + assertArrayEquals(privateKeyBytes,kp2.getPrivate().getEncoded()); + + SignedData sd2=kp2.signData(data); + assertTrue(sd2.checkSignature()); + + Blob pkb=Ed25519KeyPair.extractPrivateKey(priv); + AKeyPair kp3=Ed25519KeyPair.create(address, pkb); + + assertEquals(sd2,kp3.signData(data)); + } + + @Test + public void testCreateFromPrivateKey() { + Ed25519KeyPair kp1=Ed25519KeyPair.generate(); + PrivateKey priv=kp1.getPrivate(); + // PublicKey pub=kp1.getPublic(); + + Ed25519KeyPair kp2 = Ed25519KeyPair.create(priv); + assertTrue(kp1.equals(kp2)); + } + + @Test + public void testSeededKeyGen() { + AKeyPair kp1=Ed25519KeyPair.createSeeded(1337); + AKeyPair kp2=Ed25519KeyPair.createSeeded(1337); + AKeyPair kp3=Ed25519KeyPair.createSeeded(13378); + assertTrue(kp1.equals(kp2)); + assertFalse(kp2.equals(kp3)); + } + + @Test + public void testSigFromHex() throws BadFormatException, InvalidDataException { + String s="cafebabe000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + ASignature s1=ASignature.fromHex(s); + s1.validate(); + + assertEquals(s,s1.toHexString()); + + assertThrows(IllegalArgumentException.class,()->ASignature.fromHex("00")); + } + + @Test + public void testAccountKeyRoundTrip() { + // Address should round trip to a Ed25519 public key and back again + AccountKey a=AccountKey.fromHex("0123456701234567012345670123456701234567012345670123456701234567"); + PublicKey pk=Ed25519KeyPair.publicKeyFromBytes(a.getBytes()); + AccountKey b=Ed25519KeyPair.extractAccountKey(pk); + assertEquals(a,b); + } + + /** + * Example test values from: https://stackoverflow.com/questions/53921655/rebuild-of-ed25519-keys-with-bouncy-castle-java + * @throws NoSuchAlgorithmException + * @throws IOException + * @throws InvalidKeySpecException + * @throws InvalidKeyException + * @throws SignatureException + */ + @Test + public void testExample() throws NoSuchAlgorithmException, IOException, InvalidKeySpecException, InvalidKeyException, SignatureException { + + byte [] msg = "eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc".getBytes(StandardCharsets.UTF_8); + + byte[] privateKeyBytes = Base64.getUrlDecoder().decode("nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A"); + byte[] publicKeyBytes = Base64.getUrlDecoder().decode("11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"); + assertEquals(32,privateKeyBytes.length); + assertEquals(32,publicKeyBytes.length); + + PublicKey publicKey=Ed25519KeyPair.publicKeyFromBytes(publicKeyBytes); + PrivateKey privateKey=Ed25519KeyPair.privateKeyFromBytes(privateKeyBytes); + + // Sign + Signature signer = Signature.getInstance("EdDSA"); + signer.initSign(privateKey); + signer.update(msg, 0, msg.length); + byte[] signature = signer.sign(); + + String sigText=Base64.getUrlEncoder().encodeToString(signature).replace("=", ""); + assertEquals("hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg",sigText); + + // Verify + Signature verifier = Signature.getInstance("EdDSA"); + verifier.initVerify(publicKey); + verifier.update(msg); + assertTrue(verifier.verify(signature)); + + // Bad verify - wrong signature + verifier.initVerify(publicKey); + verifier.update(msg); + assertFalse(verifier.verify(new byte[64])); + } + + @Test + public void testRFC8032() { + // From RFC8032 7.1 + { // Empty message + Blob seed=Blob.fromHex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"); + AccountKey pk=AccountKey.fromHex("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"); + Blob msg=Blob.EMPTY; + Blob esig=Blob.fromHex("e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"); + doSigTests(seed,pk,msg,esig); + } + } + + private void doSigTests(Blob seed, AccountKey pk, Blob msg, Blob expectedSig) { + ASignature sig=ASignature.fromBlob(expectedSig); + assertTrue(sig.verify(Blob.EMPTY, pk)); + + byte [] sodiumPK=new byte[32]; + byte [] sodiumSK=new byte[64]; + Providers.SODIUM_SIGN.cryptoSignSeedKeypair(sodiumPK, sodiumSK, seed.getBytes()); + + assertEquals(pk,Blob.wrap(sodiumPK)); + + byte [] sodiumSig=new byte[64]; + // ABlob ssk=Blob.wrap(sodiumPK).append(Blob.wrap(sodiumSK)); + Providers.SODIUM_SIGN.cryptoSignDetached(sodiumSig, msg.getBytes(), (int)msg.count(), sodiumSK); + + // TODO: figure out how to get LazySodium to replicate test vectors + assertEquals(sig.getSignatureBlob(),Blob.wrap(sodiumSig)); + + } + +} diff --git a/convex-core/src/test/java/convex/core/crypto/HashTest.java b/convex-core/src/test/java/convex/core/crypto/HashTest.java new file mode 100644 index 000000000..b3d8aa4c3 --- /dev/null +++ b/convex-core/src/test/java/convex/core/crypto/HashTest.java @@ -0,0 +1,102 @@ +package convex.core.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.bouncycastle.util.Arrays; +import org.junit.jupiter.api.Test; + +import convex.core.data.Blob; +import convex.core.data.Format; +import convex.core.data.Hash; +import convex.core.data.Ref; +import convex.core.data.Strings; +import convex.core.data.Tag; +import convex.core.util.Utils; + +/** + * Tests for hashing functionality + */ +public class HashTest { + public static final String GENESIS_HEADER = "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c"; + + @Test + void testBasicSHA256() { + // empty bytes + Hash h1 = Hashing.sha256(Utils.EMPTY_BYTES); + assertEquals("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", h1.toHexString()); + + // 32 empty bytes + Hash h1_32 = Hashing.sha256(new byte[32]); + assertEquals("66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925", h1_32.toHexString()); + + // Hash from https://www.freeformatter.com/sha256-generator.html + Hash h2 = Hashing.sha256("Hello"); + assertEquals("185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969", h2.toHexString()); + + // Hash from https://passwordsgenerator.net/sha256-hash-generator/ + Hash h3 = Hashing.sha256("Boo"); + assertEquals("BF66F3E41E470B7D073DB8C5FB82E737962D63080EDBCC4F9EF3C3CE735472EA", + h3.toHexString().toUpperCase()); + } + + @Test + void testBuiltinHashes() { + Hash h1 = Hashing.sha3(Utils.EMPTY_BYTES); + assertEquals("a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", h1.toHexString()); + assertEquals(Hash.EMPTY_HASH, h1); + + } + + @Test + void testNullHash() { + // hash of single zero byte (CVM null encoding), tested against multiple online calculators + assertEquals("5d53469f20fef4f8eab52b88044ede69c77a6a68a60728609fc4a65ff531e7d0", Hash.NULL_HASH.toHexString()); + + // different ways of getting the same result, should all correspond + assertSame(Hash.NULL_HASH, Hash.compute(null)); + assertSame(Hash.NULL_HASH, Ref.get(null).getHash()); + } + + @Test + void testDataLength() { + assertEquals(34, Hash.NULL_HASH.getEncodingLength()); + } + + @Test + void testExtractHash() { + Hash h = Hash.compute(Strings.create("foo")); + Blob b = Format.encodedBlob(h); + byte[] bs = b.getBytes(); + Hash h2 = Hash.wrap(bs, 2); // all bytes except the initial tag byte and count + assertEquals(h, h2); + } + + @Test + void testBitcoinGenesis() { + // Bitcoin genesis block header + // 0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c + // Should hash to: + // 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26 + // after double hashing and interpretation in little-endian format + byte[] genesisHeader = Utils.hexToBytes(GENESIS_HEADER); + assertEquals(80, genesisHeader.length); + // genesisHeader=Arrays.reverse(genesisHeader); + Hash h1 = Hashing.sha256(genesisHeader); + assertEquals("af42031e805ff493a07341e2f74ff58149d22ab9ba19f61343e2c86c71c5d66d", h1.toHexString()); + Hash h2 = h1.computeHash(Hashing.getSHA256Digest()); + assertEquals("6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000", h2.toHexString()); + // reversed bytes for little-endian format + assertEquals("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + Utils.toHexString(Arrays.reverse(h2.getBytes()))); + } + + @Test + void testEquality() { + Hash h = Hash.NULL_HASH; + assertEquals(h, Hashing.sha3(new byte[] { Tag.NULL })); + assertEquals(h, h.toBlob()); + + assertEquals(0, h.compareTo(h.toBlob())); + } +} diff --git a/convex-core/src/test/java/convex/core/crypto/MnemonicTest.java b/convex-core/src/test/java/convex/core/crypto/MnemonicTest.java new file mode 100644 index 000000000..508210eb2 --- /dev/null +++ b/convex-core/src/test/java/convex/core/crypto/MnemonicTest.java @@ -0,0 +1,43 @@ +package convex.core.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Random; + +import org.junit.jupiter.api.Test; + +import convex.core.data.Blob; + +public class MnemonicTest { + @Test + public void testZero() { + byte[] bs = new byte[16]; + assertEquals("a a a a a a a a a a a a", Mnemonic.encode(bs)); + Arrays.fill(bs, (byte) -1); + assertEquals("yoke yoke yoke yoke yoke yoke yoke yoke yoke yoke yoke yoke", Mnemonic.encode(bs)); + } + + @Test + public void testRoundTrips() { + Random r = new Random(78976976); + for (int i = 0; i < 100; i++) { + int n = r.nextInt(100) + 1; + byte[] bs = Blob.createRandom(r, n).getBytes(); + + // round trip byte array + String m = Mnemonic.encode(bs); + byte[] bs2 = Mnemonic.decode(m, 8 * n); + assertTrue(Arrays.equals(bs, bs2)); + assertEquals(n, bs2.length); + } + } + + @Test + public void testRandom() { + String mnem = Mnemonic.createSecureRandom(); + byte[] bs = Mnemonic.decode(mnem, 128); + assertEquals(16, bs.length); + } +} diff --git a/convex-core/src/test/java/convex/core/crypto/PBETest.java b/convex-core/src/test/java/convex/core/crypto/PBETest.java new file mode 100644 index 000000000..cb5ec473d --- /dev/null +++ b/convex-core/src/test/java/convex/core/crypto/PBETest.java @@ -0,0 +1,16 @@ +package convex.core.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; + +public class PBETest { + + @Test + public void testKeyDerivation() { + byte[] bs = PBE.deriveKey(new char[0], new byte[1], 128); + assertEquals(16, bs.length); + assertEquals("a352cdf92312599de774874ad9f3fcc5", Hex.toHexString(bs)); + } +} diff --git a/convex-core/src/test/java/convex/core/crypto/PEMToolsTest.java b/convex-core/src/test/java/convex/core/crypto/PEMToolsTest.java new file mode 100644 index 000000000..5bdba2fd1 --- /dev/null +++ b/convex-core/src/test/java/convex/core/crypto/PEMToolsTest.java @@ -0,0 +1,56 @@ +package convex.core.crypto; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.security.PrivateKey; +import java.security.SecureRandom; + +import org.junit.jupiter.api.Test; + +import convex.core.data.AString; +import convex.core.data.Strings; +import convex.core.util.Utils; + +public class PEMToolsTest { + + String generateRandomHex(int size) { + SecureRandom random = new SecureRandom(); + byte password[] = new byte[size]; + random.nextBytes(password); + return Utils.toHexString(password); + } + + @Test + public void testPEMPrivateKey() { + AKeyPair keyPair = Ed25519KeyPair.generate(); + + String testPassword = generateRandomHex(32); + String pemText = null; + try { + pemText = PEMTools.encryptPrivateKeyToPEM(keyPair.getPrivate(), testPassword.toCharArray()); + } catch (Error e) { + throw e; + } + + assertTrue(pemText != null); + PrivateKey privateKey = null; + try { + privateKey = PEMTools.decryptPrivateKeyFromPEM(pemText, testPassword.toCharArray()); + } catch (Error e) { + throw e; + } + + AKeyPair importKeyPair = Ed25519KeyPair.create(privateKey); + AString data = Strings.create(generateRandomHex(1024)); + ASignature leftSignature = keyPair.sign(data.getHash()); + ASignature rightSignature = importKeyPair.sign(data.getHash()); + assertTrue(leftSignature.equals(rightSignature)); + + + // TODO: fix equality testing + // Blob key1 = keyPair.getEncodedPrivateKey(); + // Blob key2 = importKeyPair.getEncodedPrivateKey(); + //assertEquals(key1,key2); + //(keyPair,importKeyPair); + } +} diff --git a/convex-core/src/test/java/convex/core/crypto/PFXTest.java b/convex-core/src/test/java/convex/core/crypto/PFXTest.java new file mode 100644 index 000000000..fc18e12dd --- /dev/null +++ b/convex-core/src/test/java/convex/core/crypto/PFXTest.java @@ -0,0 +1,41 @@ +package convex.core.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.File; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; + +import org.junit.jupiter.api.Test; + +import convex.core.init.InitTest; +import convex.core.lang.RT; + +public class PFXTest { + + @Test public void testNewStore() throws IOException, GeneralSecurityException { + File f=File.createTempFile("temp-keystore", "pfx"); + + PFXTools.createStore(f, "test"); + + // check password is being applied + assertThrows(IOException.class,()->PFXTools.loadStore(f,"foobar")); + + // don't throw, no integrity checking on null? + //assertThrows(IOException.class,()->PFXUtils.loadStore(f,null)); + + KeyStore ks=PFXTools.loadStore(f, "test"); + AKeyPair kp=InitTest.HERO_KEYPAIR; + PFXTools.setKeyPair(ks, kp, "thehero"); + PFXTools.saveStore(ks, f, "test"); + + String alias=InitTest.HERO_KEYPAIR.getAccountKey().toHexString(); + KeyStore ks2=PFXTools.loadStore(f, "test"); + assertEquals(alias,ks2.aliases().asIterator().next()); + + AKeyPair kp2=PFXTools.getKeyPair(ks2,alias, "thehero"); + assertEquals(kp.signData(RT.cvm(1L)).getEncoding(),kp2.signData(RT.cvm(1L)).getEncoding()); + } +} diff --git a/convex-core/src/test/java/convex/core/crypto/ParamTestHash.java b/convex-core/src/test/java/convex/core/crypto/ParamTestHash.java new file mode 100644 index 000000000..f2770e489 --- /dev/null +++ b/convex-core/src/test/java/convex/core/crypto/ParamTestHash.java @@ -0,0 +1,47 @@ +package convex.core.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import convex.core.data.ABlob; +import convex.core.data.Blob; +import convex.core.data.Hash; +import convex.core.util.Utils; + +@RunWith(Parameterized.class) +public class ParamTestHash { + private Hash hash; + + public ParamTestHash(String label, Hash data) { + this.hash = data; + } + + @Parameterized.Parameters(name = "{index}: {0}") + public static Collection dataExamples() { + return Arrays.asList(new Object[][] { + { "Empty bytes", Hashing.sha256(Utils.EMPTY_BYTES) }, + { "Short string data", Hashing.sha256("Hello World") }, + { "Length 2 strict sublist of byte data", Hashing.sha256(new byte[] { 1, 2, 3, 4 }) }, + { "Bitcoin genesis header block", Blob.fromHex(HashTest.GENESIS_HEADER).computeHash(Hashing.getSHA256Digest()) } }); + } + + @Test + public void testHexRoundTrip() { + String hex = hash.toHexString(); + Hash d2 = Hash.fromHex(hex); + assertEquals(hash, d2); + assertEquals(hash.hashCode(), d2.hashCode()); + } + + @Test + public void testSlice() { + ABlob d = hash.slice(0, hash.count()); + assertEquals(hash.toBlob(), d); + } +} diff --git a/convex-core/src/test/java/convex/core/crypto/SignKeyPairTest.java b/convex-core/src/test/java/convex/core/crypto/SignKeyPairTest.java new file mode 100644 index 000000000..bd21a1d05 --- /dev/null +++ b/convex-core/src/test/java/convex/core/crypto/SignKeyPairTest.java @@ -0,0 +1,20 @@ +package convex.core.crypto; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.Test; + +public class SignKeyPairTest { + + @Test + public void testSeeded() { + AKeyPair kp1 = AKeyPair.createSeeded(13); + AKeyPair kp2 = AKeyPair.createSeeded(13); + assertTrue(kp1.equals(kp2)); + AKeyPair kp3 = AKeyPair.createSeeded(1337); + assertFalse(kp1.equals(kp3) ); + } + + +} diff --git a/convex-core/src/test/java/convex/core/crypto/SymmetricTest.java b/convex-core/src/test/java/convex/core/crypto/SymmetricTest.java new file mode 100644 index 000000000..71c994dfb --- /dev/null +++ b/convex-core/src/test/java/convex/core/crypto/SymmetricTest.java @@ -0,0 +1,41 @@ +package convex.core.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.util.Arrays; + +import javax.crypto.SecretKey; + +import org.junit.jupiter.api.Test; + +public class SymmetricTest { + @Test + public void testRoundTrip() { + String plainText = "Hello World!!!"; + + SecretKey key1 = Symmetric.createSecretKey(); + byte[] message = Symmetric.encrypt(key1, plainText); + String decrypted = Symmetric.decryptString(key1, message); + + assertEquals(plainText, decrypted); + + SecretKey key2 = Symmetric.createSecretKey(); + byte[] message2 = Symmetric.encrypt(key2, plainText); + assertFalse(Arrays.equals(message, message2)); + + } + + @Test + public void testSecretKeyVariance() { + assertNotEquals(Symmetric.createSecretKey(), Symmetric.createSecretKey()); + } + + @Test + public void testEncoded() { + SecretKey k = Symmetric.createSecretKey(); + byte[] encoded = k.getEncoded(); + assertEquals(16, encoded.length); + } +} diff --git a/convex-core/src/test/java/convex/core/crypto/WalletTest.java b/convex-core/src/test/java/convex/core/crypto/WalletTest.java new file mode 100644 index 000000000..b471944e7 --- /dev/null +++ b/convex-core/src/test/java/convex/core/crypto/WalletTest.java @@ -0,0 +1,17 @@ +package convex.core.crypto; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.File; + +import org.junit.jupiter.api.Test; + +public class WalletTest { + + @Test + public void testTempStore() { + String password="OmarSharif"; + File file=Wallet.createTempStore(password); + assertNotNull(file); + } +} diff --git a/convex-core/src/test/java/convex/core/data/AccountKeyTest.java b/convex-core/src/test/java/convex/core/data/AccountKeyTest.java new file mode 100644 index 000000000..329f81c67 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/AccountKeyTest.java @@ -0,0 +1,46 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Random; + +import org.junit.jupiter.api.Test; + +public class AccountKeyTest { + + @Test + public void testChecksumRoundTrip() { + Random r = new Random(1585875); + for (int i = 0; i < 10; i++) { + Blob ba = Blob.createRandom(r, AccountKey.LENGTH); + AccountKey a = AccountKey.wrap(ba.getBytes()); + + String s = a.toChecksumHex(); + assertEquals(a, AccountKey.fromHex(s)); + assertEquals(a, AccountKey.fromChecksumHex(s)); + + String sl = s.toLowerCase(); + + assertEquals(a, AccountKey.fromHex(sl)); + assertThrows(IllegalArgumentException.class, () -> AccountKey.fromChecksumHex(sl)); + } + } + + + @Test + public void testEquality() { + String aString = Blob.createRandom(new Random(), AccountKey.LENGTH).toHexString(); + AccountKey a = AccountKey.fromHex(aString); + assertEquals(a, AccountKey.fromHex(aString)); + + // AccountKey should not be equal to Blob with same byte content + Blob b = a.toBlob(); + assertEquals(a, b); + + // AccountKey has comparison equality with Blob + assertEquals(0, a.compareTo(b)); + + BlobsTest.doBlobTests(a); + } +} diff --git a/convex-core/src/test/java/convex/core/data/AddressTest.java b/convex-core/src/test/java/convex/core/data/AddressTest.java new file mode 100644 index 000000000..3715d3775 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/AddressTest.java @@ -0,0 +1,33 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class AddressTest { + + @Test + public void testAddress1() { + Address a1=Address.create(1); + assertEquals("#1",a1.toString()); + String hex="0000000000000001"; + assertEquals(hex,a1.toHexString()); + + assertEquals(a1,Address.fromHex(hex)); + assertTrue(a1.compareTo(Blob.fromHex(hex))==0); + } + + @Test + public void testAddress2() { + Address a1=Address.create(13); + assertEquals("#13",a1.toString()); + } + + @Test + public void testParse() { + assertEquals("#1",Address.parse("#1").toString()); + assertEquals("#2",Address.parse("2").toString()); + assertEquals("#16",Address.parse("0x0000000000000010").toString()); + } +} diff --git a/convex-core/src/test/java/convex/core/data/BlobMapsTest.java b/convex-core/src/test/java/convex/core/data/BlobMapsTest.java new file mode 100644 index 000000000..5e70f8768 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/BlobMapsTest.java @@ -0,0 +1,297 @@ +package convex.core.data; + +import static convex.test.Assertions.assertCVMEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import convex.core.data.prim.CVMLong; +import convex.core.data.type.Types; +import convex.core.exceptions.InvalidDataException; +import convex.core.init.InitTest; +import convex.core.lang.RT; +import convex.test.Samples; + +public class BlobMapsTest { + + @Test + public void testEmpty() throws InvalidDataException { + BlobMap m = BlobMaps.empty(); + + assertFalse(m.containsKey(Blob.EMPTY)); + assertFalse(m.containsKey(null)); + assertFalse(m.containsValue(RT.cvm(1L))); + assertFalse(m.containsValue(null)); + + assertEquals(0L, m.count()); + assertSame(m, m.dissoc(Blob.fromHex("cafe"))); + assertSame(m, m.dissoc(Blob.fromHex(""))); + + // checks vs regular map + assertFalse(m.equals(Maps.empty())); + assertFalse(Maps.empty().equals(m)); + + doBlobMapTests(m); + } + + @Test + public void testBadAssoc() throws InvalidDataException { + BlobMap m =BlobMaps.create(InitTest.HERO, RT.cvm(1L)); + m=m.assoc(InitTest.VILLAIN, RT.cvm(2L)); + assertEquals(2L,m.count()); + + assertNull(m.assoc(null, null)); + } + + + @Test + public void testAssoc() throws InvalidDataException { + Blob k1 = Blob.fromHex("cafe"); + Blob k2 = Blob.fromHex("cafebabe"); + Blob k3 = Blob.fromHex("ccca"); + BlobMap m = BlobMaps.create(k1, RT.cvm(17L)); + + doBlobMapTests(m); + + assertTrue(m.containsKey(k1)); + assertTrue(m.containsValue(RT.cvm(17L))); + assertFalse(m.containsKey(k2)); + assertFalse(m.containsKey(Blob.EMPTY)); + assertFalse(m.containsKey(null)); + + // add second entry + m = m.assoc(k2, RT.cvm(23L)); + assertEquals(2L, m.count()); + MapEntry e2 = m.entryAt(1); + assertSame(k2, e2.getKey()); + assertEquals(RT.cvm(23L), e2.getValue()); + + doBlobMapTests(m); + + // add third entry + m = m.assoc(k3, RT.cvm(34L)); + assertNotNull(m.toString()); + assertEquals(3L, m.count()); + MapEntry e3 = m.entryAt(2); + assertEquals(e3, m.getEntry(k3)); + assertEquals(RT.cvm(34L), e3.getValue()); + + doBlobMapTests(m); + + assertEquals(Vectors.of(17L,23L,34L),m.values()); + } + + @Test + public void testGet() throws InvalidDataException { + Blob k1 = Blob.fromHex("cafe"); + BlobMap m = BlobMaps.of(k1, 17L); + assertNull(m.get(Samples.MAX_EMBEDDED_STRING)); // needs a blob. String counts as non-existent key + assertCVMEquals(17L,m.get(k1)); + + assertNull(m.get((Object)null)); // Null counts as non-existent key when used as an Object arg + } + + + @Test + public void testBlobMapConstruction() throws InvalidDataException { + BlobMap m = BlobMaps.empty(); + for (int i = 0; i < 100; i++) { + Long l = (long) Integer.hashCode(i); + CVMLong cl = RT.cvm(l); + LongBlob lb = LongBlob.create(l); + m = m.assoc(lb, cl); + assertEquals(cl, m.get(lb)); + } + assertEquals(100L, m.count()); + m.validate(); + + doBlobMapTests(m); + + for (int i = 0; i < 100; i++) { + Long l = (long) Integer.hashCode(i); + LongBlob lb = LongBlob.create(l); + m = m.dissoc(lb); + assertFalse(m.containsKey(lb), "Index: " + lb.toHexString()); + } + assertSame(BlobMaps.empty(), m); + } + + @Test + public void testBlobMapRandomConstruction() throws InvalidDataException { + BlobMap m = BlobMaps.empty(); + for (int i = 0; i < 100; i++) { + Long l = (Long.MAX_VALUE / 91 * i * 18); + CVMLong cl=RT.cvm(l); + LongBlob lb = LongBlob.create(l); + m = m.assoc(lb, cl); + assertEquals(cl, m.get(lb)); + } + assertEquals(100L, m.count()); + m.validate(); + + doBlobMapTests(m); + + for (int i = 0; i < 100; i++) { + Long l = (Long.MAX_VALUE / 91 * i * 18); + LongBlob lb = LongBlob.create(l); + m = m.dissoc(lb); + assertFalse(m.containsKey(lb), "Index: " + lb.toHexString()); + } + assertSame(BlobMaps.empty(), m); + } + + @Test + public void testIdentity() { + Blob bb = Blob.fromHex("000000000000cafe"); + LongBlob bl = LongBlob.create(0xcafe); + Address ba=Address.create(0xcafe); + assertNotEquals(BlobMap.create(bb, bl), BlobMap.create(ba,bl)); // different entry key types + assertEquals(BlobMap.create(bb, bl), BlobMap.create(bl,bl)); // same entry key types + } + + @Test + public void testSingleEntry() throws InvalidDataException { + Blob k = Blob.fromHex("cafe"); + Blob k2 = Blob.fromHex("cafebabe"); + CVMLong val=RT.cvm(177777L); + BlobMap m = BlobMaps.create(k, val); + assertEquals(1L, m.count()); + + assertEquals(val, m.get(k)); + + assertNull(m.get(Blob.EMPTY)); + assertNull(m.get(k2)); + + assertSame(BlobMaps.empty(), m.dissoc(k)); + assertSame(m, m.dissoc(k2)); // long key miss + assertSame(m, m.dissoc(k.slice(0, 1))); // short prefix key miss + assertSame(m, m.dissoc(Blob.fromHex("caef"))); // partial prefix key miss + + MapEntry me = m.entryAt(0); + assertEquals(k, me.getKey()); + assertEquals(val, me.getValue()); + + doBlobMapTests(m); + } + + @Test + public void testPrefixEntryTwo() throws InvalidDataException { + Blob k1 = Blob.fromHex("cafe"); + Blob k2 = Blob.fromHex("cafebabe"); + BlobMap m = BlobMaps.of(k1, 17L, k2, 23L); + BlobMap m1 = BlobMaps.of(k1, 17L); + BlobMap m2 = BlobMaps.of(k2, 23L); + assertSame(m, m.dissoc(k1.slice(0, 1))); + assertEquals(m1, m.dissoc(k2)); + assertEquals(m2, m.dissoc(k1)); + + doBlobMapTests(m); + } + + @Test + public void testInitialPeersBlobMap() { + BlobMap bm = InitTest.STATE.getPeers(); + doBlobMapTests(bm); + + BlobMap fm =bm.filterValues(ps -> ps==bm.get(InitTest.FIRST_PEER_KEY)); + assertEquals(1L,fm.count()); + } + + @Test + public void testPrefixEntryThree() throws InvalidDataException { + Blob k1 = Blob.fromHex("cafe"); + Blob k2 = Blob.fromHex("cafebabe"); + Blob k3 = Blob.fromHex("cafefeed"); + BlobMap m = BlobMaps.of(k1, 17L, k2, 23L, k3, 47L); + m.validate(); + assertEquals(2L, m.dissoc(k1).count()); + + assertSame(m, m.assocEntry(m.getEntry(k1))); + assertEquals(m, m.assoc(k1, RT.cvm(17L))); + assertNotEquals(m, m.assoc(k1, RT.cvm(27L))); + + assertEquals(m, BlobMaps.of(k2, 23L, k3, 47L).assoc(k1, RT.cvm(17L))); + + Blob k0 = Blob.fromHex("ca"); + BlobMap m4 = m.assoc(k0, RT.cvm(7L)); + m4.validate(); + BlobMap m4b = BlobMaps.of(k0, 7L, k1, 17L, k2, 23L, k3, 47L); + assertEquals(m4, m4b); + doBlobMapTests(m4); + + doBlobMapTests(m); + } + + @Test + public void testDissocEntries() throws InvalidDataException { + BlobMap m = Samples.INT_BLOBMAP_7; + long n=m.count(); + + for (int i=0; i me=m.entryAt(i); + BlobMap dm= (BlobMap)m.dissoc(me.getKey()); + dm.validate(); + assertEquals(n-1,dm.count()); + BlobMap m2=dm.assocEntry(me); + assertEquals(m,m2); + } + } + + @Test + public void testDissocAll() throws InvalidDataException { + BlobMap m=BlobMaps.empty(); + long n=100; + + for (long i=0; i m = Samples.INT_BLOBMAP_7; + + assertSame(m, m.removeLeadingEntries(0)); + assertSame(BlobMaps.empty(), m.removeLeadingEntries(7)); + } + + @Test + public void testSmallIntBlobMap() { + BlobMap m = Samples.INT_BLOBMAP_7; + + for (int i = 0; i < 7; i++) { + MapEntry me = m.entryAt(i); + assertEquals(i, me.getValue().longValue()); + assertEquals(me, m.getEntry(me.getKey())); + } + doBlobMapTests(m); + } + + private void doBlobMapTests(BlobMap m) { + long n = m.count(); + + if (n >= 2) { + MapEntry e1 = m.entryAt(0); + MapEntry e2 = m.entryAt(n - 1); + assertTrue(e1.getKey().compareTo(e2.getKey()) < 0); + } + + assertEquals(Types.BLOBMAP,m.getType()); + + CollectionsTest.doMapTests(m); + } +} diff --git a/convex-core/src/test/java/convex/core/data/BlobsTest.java b/convex-core/src/test/java/convex/core/data/BlobsTest.java new file mode 100644 index 000000000..f208f1695 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/BlobsTest.java @@ -0,0 +1,239 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Random; + +import org.junit.jupiter.api.Test; + +import convex.core.data.prim.CVMByte; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.test.Samples; + +public class BlobsTest { + @Test + public void testCompare() { + assertTrue(Blob.fromHex("01").compareTo(Blob.fromHex("FF")) < 0); + assertTrue(Blob.fromHex("40").compareTo(Blob.fromHex("30")) > 0); + assertTrue(Blob.fromHex("0102").compareTo(Blob.fromHex("0201")) < 0); + + assertTrue(Blob.fromHex("01").compareTo(Blob.fromHex("01")) == 0); + assertTrue(Blob.fromHex("").compareTo(Blob.fromHex("")) == 0); + } + + @Test + public void testNullHash() { + AArrayBlob d = Blob.create(new byte[] { Tag.NULL }); + assertTrue(d.getContentHash().equals(Hash.NULL_HASH)); + } + + @Test + public void testHexEquals() { + assertTrue(Blob.fromHex("0123").hexEquals(Blob.fromHex("0123"), 0, 4)); + assertTrue(Blob.fromHex("0125").hexEquals(Blob.fromHex("5123"), 1, 2)); + assertTrue(Blob.fromHex("012345").hexEquals(Blob.fromHex("a123"), 2, 2)); + + // zero length ranges + assertTrue(Blob.fromHex("0123").hexEquals(Blob.fromHex("4567"), 1, 0)); + assertTrue(Blob.fromHex("0123").hexEquals(Blob.fromHex("4567"), 0, 0)); + assertTrue(Blob.fromHex("0123").hexEquals(Blob.fromHex("4567"), 4, 0)); + } + + @Test + public void testHexMatchLength() { + assertEquals(4,Blob.fromHex("0123").hexMatchLength(Blob.fromHex("0123"), 0, 4)); + assertEquals(3,Blob.fromHex("0123").hexMatchLength(Blob.fromHex("012f"), 0, 4)); + assertEquals(3,Blob.fromHex("ffff0123").hexMatchLength(Blob.fromHex("ffff012f"), 4, 4)); + + assertEquals(0,Blob.fromHex("ffff0123").hexMatchLength(Blob.fromHex("ffff012f"), 3, 0)); + } + + + @Test + public void testFromHex() { + // bad length for blob + assertNull(Blob.fromHex("2")); + assertNull(Blob.fromHex("zz")); + } + + @Test + public void testBlobTreeConstruction() { + Random r = new Random(); + int clen = Blob.CHUNK_LENGTH; + int hclen = clen / 2; + Blob a = Blob.createRandom(r, clen); + Blob b = Blob.createRandom(r, hclen); + BlobTree bt = BlobTree.create(a, b); + + assertEquals(clen + hclen, bt.count()); + assertEquals(a, bt.getChunk(0)); + assertEquals(b, bt.getChunk(1)); + + doBlobTests(bt); + } + + @Test + public void testToLong() { + assertEquals(255L,Blob.fromHex("ff").toLong()); + assertEquals(-1L,Blob.fromHex("ffffffffffffffff").toLong()); + } + + @Test + public void testLongBlob() { + LongBlob b = LongBlob.create("cafebabedeadbeef"); + Blob bb = Blob.fromHex("cafebabedeadbeef"); + + assertEquals(b.longValue(),bb.longValue()); + + assertEquals(10, b.getHexDigit(1)); // 'a' + + for (int i = 0; i < 8; i++) { + assertEquals(b.byteAt(i), bb.byteAt(i)); + } + + for (int i = 0; i < 16; i++) { + assertEquals(b.getHexDigit(i), bb.getHexDigit(i)); + } + + assertTrue(bb.hexEquals(b)); + assertTrue(b.hexEquals(bb, 3, 10)); + + assertEquals(16, b.commonHexPrefixLength(bb)); + assertEquals(10, b.commonHexPrefixLength(bb.slice(0, 5))); + assertEquals(8, bb.commonHexPrefixLength(b.slice(0, 4))); + + // Longblobs considered as Blob type + assertEquals(bb, b); + assertEquals(b, bb); + + assertEquals(bb, b.toBlob()); + assertEquals(bb.hashCode(), b.hashCode()); + + doBlobTests(b); + + } + + @Test + public void testEmptyBlob() { + ABlob blob = Blob.EMPTY; + assertEquals(0L,blob.toLong()); + assertSame(blob,blob.getChunk(0)); + assertSame(blob,blob.slice(0,0)); + + doBlobTests(Blob.EMPTY); + } + + @Test + public void testBlobSlice() { + ABlob blob = Blob.fromHex("cafebabedeadbeef").slice(2,4); + assertEquals(8,blob.hexLength()); + doBlobTests(blob); + } + + @Test + public void testFullBlob() { + ABlob fb2 = Samples.FULL_BLOB.append(Samples.FULL_BLOB); + assertEquals(Blob.EMPTY, Samples.FULL_BLOB.getChunk(1)); + assertEquals(Samples.FULL_BLOB, fb2.getChunk(1)); + + assertTrue(Samples.FULL_BLOB.hexEquals(Samples.FULL_BLOB)); + + doBlobTests(Samples.FULL_BLOB); + } + + @Test + public void testEncodingSize() { + int el=(int) Samples.FULL_BLOB.getEncoding().count(); + assertEquals(Blobs.MAX_ENCODING_LENGTH,el); + assertEquals(Blob.MAX_ENCODING_LENGTH,el); + + assertTrue(Samples.MAX_EMBEDDED_BLOB.isEmbedded()); + assertFalse(Samples.FULL_BLOB.isEmbedded()); + } + + @Test + public void testBigBlob() throws InvalidDataException, BadFormatException { + BlobTree bb = Samples.BIG_BLOB_TREE; + long len = bb.count(); + + assertEquals(Samples.BIG_BLOB_LENGTH, len); + + assertSame(bb, bb.slice(0)); + assertSame(bb, bb.slice(0, len)); + + Blob firstChunk = bb.getChunk(0); + assertEquals(Blob.CHUNK_LENGTH, firstChunk.count()); + assertEquals(bb.byteAt(0), firstChunk.byteAt(0)); + + Blob blob = bb.toBlob(); + assertEquals(bb.count(), blob.count()); + assertEquals(bb.byteAt(len - 1), blob.byteAt(len - 1)); + assertEquals(bb.byteAt(0), blob.byteAt(0)); + assertEquals(bb.getChunk(1), blob.getChunk(1)); + + assertEquals(len * 2, bb.commonHexPrefixLength(bb)); + + bb.validate(); + + Ref rb = ACell.createPersisted(bb); + BlobTree bbb = Format.read(bb.getEncoding()); + bbb.validate(); + assertEquals(bb, bbb); + assertEquals(bb, rb.getValue()); + assertEquals(bb.count(), bb.hexMatchLength(bbb, 0, len)); + + doBlobTests(bb); + } + + @Test + public void testBlobTreeOutOfRange() { + assertThrows(IndexOutOfBoundsException.class, () -> Samples.BIG_BLOB_TREE.byteAt(Samples.BIG_BLOB_LENGTH)); + assertThrows(IndexOutOfBoundsException.class, () -> Samples.BIG_BLOB_TREE.slice(-1)); + assertThrows(IndexOutOfBoundsException.class, () -> Samples.BIG_BLOB_TREE.slice(1, Samples.BIG_BLOB_LENGTH)); + } + + @Test + public void testBlobFormat() throws BadFormatException { + byte[] bf = new byte[] { Tag.BLOB, 0 }; + Blob b = Format.read(Blob.wrap(bf)); + assertEquals(0, b.count()); + assertNotEquals(b.getHash(), Hash.EMPTY_HASH); + + doBlobTests(b); + } + + /** + * Generic tests for an arbitrary ABlob instance + * @param a Any blob to test, might not be canonical + */ + public static void doBlobTests(ABlob a) { + long n = a.count(); + assertTrue(n >= 0L); + ABlob canonical=a.toCanonical(); + + // copy of the Blob data + ABlob b=Blob.wrap(a.getBytes()).toCanonical(); + + if (a.isRegularBlob()) { + assertEquals(a.count(),b.count()); + assertEquals(canonical,b); + } + + if (n>0) { + assertEquals(n*2,a.commonHexPrefixLength(b)); + + assertEquals(a.slice(n/2,n/2),b.slice(n/2, n/2)); + + assertEquals(a.get(n-1),CVMByte.create(a.byteAt(n-1))); + } + + ObjectsTest.doAnyValueTests(canonical); + } +} diff --git a/convex-core/src/test/java/convex/core/data/BlocksTest.java b/convex-core/src/test/java/convex/core/data/BlocksTest.java new file mode 100644 index 000000000..a28402ce9 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/BlocksTest.java @@ -0,0 +1,49 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import convex.core.Block; +import convex.core.crypto.AKeyPair; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.BadSignatureException; +import convex.core.init.InitTest; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Transfer; + +public class BlocksTest { + + @Test + public void testEquality() throws BadFormatException { + long ts = System.currentTimeMillis(); + Block b1 = Block.create(ts, InitTest.FIRST_PEER_KEY,Vectors.empty()); + Block b2 = Block.create(ts, InitTest.FIRST_PEER_KEY,Vectors.empty()); + + assertEquals(b1, b2); + assertEquals(b1.hashCode(), b2.hashCode()); + assertEquals(b1.getHash(), b2.getHash()); + assertEquals(b1.getEncoding(), b2.getEncoding()); + assertEquals(b1, Format.read(b2.getEncoding())); + + assertEquals(0,b1.getTransactions().count()); + + RecordTest.doRecordTests(b1); + } + + @Test + public void testTransactions() throws BadSignatureException { + AKeyPair kp = InitTest.HERO_KEYPAIR; + + ATransaction t = Transfer.create(InitTest.HERO,0, InitTest.VILLAIN, 1000); + SignedData st = kp.signData(t); + + long ts = System.currentTimeMillis(); + Block b = Block.create(ts, InitTest.FIRST_PEER_KEY,Vectors.of(st)); + assertEquals(1, b.length()); + assertEquals(t, b.getTransactions().get(0).getValue()); + + RecordTest.doRecordTests(b); + + } +} diff --git a/convex-core/src/test/java/convex/core/data/CollectionsTest.java b/convex-core/src/test/java/convex/core/data/CollectionsTest.java new file mode 100644 index 000000000..07098b700 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/CollectionsTest.java @@ -0,0 +1,138 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Iterator; +import java.util.ListIterator; +import java.util.Map; + +import convex.core.lang.RT; + +/** + * Tests for general collection types + */ +public class CollectionsTest { + + /** + * Generic tests for any sequence + * @param a Any Sequence Value + */ + public static void doSequenceTests(ASequence a) { + long n = a.count(); + + if (n > 0) { + T last = a.get(n - 1); + T first = a.get(0); + assertEquals(n - 1, a.longLastIndexOf(last)); + assertEquals(0L, a.longIndexOf(first)); + + assertSame(a, a.assoc(0, first)); + assertSame(a, a.assoc(n - 1, last)); + } + + // Out of range assocs should return null + assertNull( a.assoc(-2, null)); + assertNull( a.assoc(n + 2, null)); + + ListIterator it = a.listIterator(); + assertThrows(UnsupportedOperationException.class, () -> it.set(null)); + assertThrows(UnsupportedOperationException.class, () -> it.add(null)); + + assertThrows(IndexOutOfBoundsException.class, () -> a.listIterator(-1)); + assertThrows(IndexOutOfBoundsException.class, () -> a.listIterator(n + 1)); + + assertThrows(IndexOutOfBoundsException.class, () -> a.getElementRef(-1)); + assertThrows(IndexOutOfBoundsException.class, () -> a.getElementRef(n)); + + doCollectionTests(a); + } + + static final Keyword UNLIKELY_KEYWORD=Keyword.create("this-is-not-likely-to-happen-at-random"); + + /** + * Generic tests for any data structure + * @param a Any Data Structure + */ + public static void doDataStructureTests(ADataStructure a) { + long n = a.count(); + if (n == 0) { + assertSame(a.empty(), a); + } else { + assertFalse(a.isEmpty()); + T first =a.get(0); + assertEquals(first,a.getElementRef(0).getValue()); + } + + assertFalse(RT.bool(a.get(UNLIKELY_KEYWORD))); + assertSame(Keywords.FOO,a.get(UNLIKELY_KEYWORD,Keywords.FOO)); + + assertEquals(n,a.size()); + + ObjectsTest.doAnyValueTests(a); + } + + /** + * Generic tests for any collection + * @param a Any Collection + */ + public static void doCollectionTests(ACollection a) { + Iterator it = a.iterator(); + assertThrows(Throwable.class, () -> it.remove()); + + doDataStructureTests(a); + } + + /** + * Generic tests for any map + * @param a Any Map + */ + public static void doMapTests(AMap a) { + long n = a.count(); + if (n == 0) { + assertThrows(IndexOutOfBoundsException.class, () -> a.entryAt(0)); + } else { + MapEntry me = a.entryAt(n / 2); + assertNotNull(me); + assertSame(a, a.assocEntry(me)); + + K key = me.getKey(); + V value = me.getValue(); + + assertEquals(a.get(key), value); + + // remove and add back entry + AMap da = a.dissoc(key); + assertEquals(n - 1, da.count()); + assertNull(da.getEntry(key)); + assertEquals(a, da.assocEntry(me)); + } + + { // test that entrySet works properly + java.util.Set> es=a.entrySet(); + assertEquals(es.size(),a.size()); + AMap t=a; + for (Map.Entry me: es) { + t=t.dissoc(me.getKey()); + } + assertSame(a.empty(),t); + } + + assertThrows(IndexOutOfBoundsException.class, () -> a.entryAt(-1)); + assertThrows(IndexOutOfBoundsException.class, () -> a.entryAt(n)); + + doDataStructureTests(a); + } + + /** + * Generic tests for any set + * @param a Any Set + */ + public static void doSetTests(ASet a) { + doCollectionTests(a); + } +} diff --git a/convex-core/src/test/java/convex/core/data/EncodingTest.java b/convex-core/src/test/java/convex/core/data/EncodingTest.java new file mode 100644 index 000000000..38a8b3a4e --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/EncodingTest.java @@ -0,0 +1,223 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +import org.junit.jupiter.api.Test; + +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.lang.RT; +import convex.test.Samples; + +public class EncodingTest { + + @Test public void testVLCLongLength() throws BadFormatException, BufferUnderflowException { + ByteBuffer bb=ByteBuffer.allocate(100); + bb.put(Tag.LONG); + Format.writeVLCLong(bb, Long.MAX_VALUE); + + // must be max long length plus tag + assertEquals(Format.MAX_VLC_LONG_LENGTH+1,bb.position()); + + bb.flip(); + Blob b=Blob.fromByteBuffer(bb); + + CVMLong max=RT.cvm(Long.MAX_VALUE); + + assertEquals(max,Format.read(b)); + + assertEquals(max.getEncoding(),b); +; } + +// @Test public void testBigIntegerRegression() throws BadFormatException { +// BigInteger expected=BigInteger.valueOf(-4223); +// assertEquals(expected,Format.read("0adf01")); +// +// assertThrows(BadFormatException.class,()->Format.read("0affdf01")); +// } +// +// @Test public void testBigIntegerRegression2() throws BadFormatException { +// BigInteger b=BigInteger.valueOf(1496216); +// Blob blob=Format.encodedBlob(b); +// assertEquals(b,Format.read(blob)); +// } +// +// @Test public void testBigIntegerRegression3() throws BadFormatException { +// Blob blob=Blob.fromHex("0a801d"); +// assertThrows(BadFormatException.class,()->Format.read(blob)); +// } +// +// @Test public void testBigDecimalRegression() throws BadFormatException { +// Blob blob=Blob.fromHex("0e001d"); +// BigDecimal bd=Format.read(blob); +// assertEquals(BigDecimal.valueOf(29),bd); +// assertEquals(blob,Format.encodedBlob(bd)); +// } + + @Test public void testEmbeddedRegression() throws BadFormatException { + Keyword k=Keyword.create("foo"); + Blob b=Format.encodedBlob(k); + Object o=Format.read(b); + assertEquals(k,o); + assertTrue(Format.isEmbedded(k)); + Ref r=Ref.get(o); + assertTrue(r.isDirect()); + } + +// @Test public void testEmbeddedBigInteger() throws BadFormatException { +// BigInteger one=BigInteger.ONE; +// assertFalse(Format.isEmbedded(one)); +// AVector v=Vectors.of(BigInteger.ONE,BigInteger.TEN); +// assertFalse(v.getRef(0).isEmbedded()); +// Blob b=Format.encodedBlob(v); +// AVector v2=Format.read(b); +// assertEquals(v,v2); +// assertEquals(b,Format.encodedBlob(v2)); +// } + + @Test public void testBadFormats() throws BadFormatException { + // test excess high order bits above the long range + assertEquals(-3717066608267863778L,((CVMLong)Format.read("09ccb594f3d1bde9b21e")).longValue()); + assertThrows(BadFormatException.class,()->{ + Format.read("09b3ccb594f3d1bde9b21e"); + }); + + // test excess high bytes for -1 + assertThrows(BadFormatException.class,()->Format.read("09ffffffffffffffffff7f")); + + // test excess high bytes for negative number + assertEquals(RT.cvm(Long.MIN_VALUE),(CVMLong)Format.read("09ff808080808080808000")); + assertThrows(BadFormatException.class,()->Format.read("09ff80808080808080808000")); + + } + + @Test public void testStringRegression() throws BadFormatException { + StringShort s=StringShort.create("��zI�&$\\ž1�����4�E4�a8�#?$wD(�#"); + Blob b=Format.encodedBlob(s); + StringShort s2=Format.read(b); + assertEquals(s,s2); + } + + @Test public void testListRegression() throws BadFormatException { + MapEntry me=MapEntry.create(Blobs.fromHex("41da2aa427dc50975dd0b077"), RT.cvm(-1449690165L)); + List l=List.reverse(me); + assertEquals(me,l.reverse()); // ensure MapEntry gets converted to canonical vector + + Blob b=Format.encodedBlob(l); + List l2=Format.read(b); + + assertEquals(l,l2); + } + + @Test public void testMalformedStrings() { + // bad examples constructed using info from https://www.w3.org/2001/06/utf-8-wrong/UTF-8-test.html + assertThrows(BadFormatException.class,()->Format.read("300180")); // continuation only + assertThrows(BadFormatException.class,()->Format.read("3001FF")); + } + + @Test public void testCanonical() { + assertTrue(Format.isCanonical(Vectors.empty())); + assertTrue(Format.isCanonical(null)); + assertTrue(Format.isCanonical(RT.cvm(1))); + assertTrue(Format.isCanonical(Blob.create(new byte[1000]))); // should be OK + assertFalse(Blob.create(new byte[10000]).isCanonical()); // too big to be canonical + } + + @Test public void testReadBlobData() throws BadFormatException { + Blob d=Blob.fromHex("cafebabe"); + Blob edData=Format.encodedBlob(d); + AArrayBlob dd=Format.read(edData); + assertEquals(d,dd); + assertSame(edData,dd.getEncoding()); // should re-use encoded data object directly + } + + @Test + public void testBadMessageTooLong() throws BadFormatException { + ACell o=Samples.FOO; + Blob data=Format.encodedBlob(o).append(Blob.fromHex("ff")).toBlob(); + assertThrows(BadFormatException.class,()->Format.read(data)); + } + + @Test + public void testMessageLength() throws BadFormatException { + // empty bytebuffer, therefore no message lengtg + ByteBuffer bb1=Blob.fromHex("").toByteBuffer(); + assertThrows(IndexOutOfBoundsException.class,()->Format.peekMessageLength(bb1)); + + // bad first byte! Needs to carry if 0x40 or more + ByteBuffer bb2=Blob.fromHex("43").toByteBuffer(); + assertThrows(BadFormatException.class,()->Format.peekMessageLength(bb2)); + + // maximum message length + ByteBuffer bb2a=Blob.fromHex("BF7F").toByteBuffer(); + assertEquals(Format.LIMIT_ENCODING_LENGTH,Format.peekMessageLength(bb2a)); + + // overflow message length + Blob overflow=Blob.fromHex("C000"); + ByteBuffer bb2aa=overflow.toByteBuffer(); + assertThrows(BadFormatException.class,()->Format.peekMessageLength(bb2aa)); + + ByteBuffer bb2b=Blob.fromHex("8043").toByteBuffer(); + assertEquals(67,Format.peekMessageLength(bb2b)); + + + ByteBuffer bb3=Blob.fromHex("FFFF").toByteBuffer(); + assertThrows(BadFormatException.class,()->Format.peekMessageLength(bb3)); + } + + @Test + public void testHexDigits() { + byte[] bs=new byte[8]; + + Blob src=Blob.fromHex("cafebabe"); + Format.writeHexDigits(bs, 2, src, 2, 4); + assertEquals(Blobs.fromHex("00000204feba0000"),Blob.wrap(bs)); + + Format.writeHexDigits(bs, 3, src, 0, 3); + assertEquals(Blobs.fromHex("0000020003caf000"),Blob.wrap(bs)); + } + + @Test + public void testWriteRef() { + // TODO: consider whether this is valid + // shouldn't be allowed to write a Ref directly as a top-level message + // ByteBuffer b=ByteBuffer.allocate(10); + // assertThrows(IllegalArgumentException.class,()->Format.write(b, Ref.create("foo"))); + } + + @Test + public void testMaxLengths() { + int ME=Format.MAX_EMBEDDED_LENGTH; + + Blob maxEmbedded=Blob.create(new byte[ME-3]); // Maximum embedded length + Blob notEmbedded=Blob.create(new byte[ME-2]); // Non-embedded length + assertTrue(maxEmbedded.isEmbedded()); + assertFalse(notEmbedded.isEmbedded()); + assertEquals(ME, maxEmbedded.getEncodingLength()); + + // Maps + assertEquals(2+16*ME,MapLeaf.MAX_ENCODING_LENGTH); + assertEquals(4+16*ME,MapTree.MAX_ENCODING_LENGTH); + assertEquals(Maps.MAX_ENCODING_SIZE,MapTree.MAX_ENCODING_LENGTH); + + // Vectors + assertEquals(1+Format.MAX_VLC_LONG_LENGTH+17*ME,VectorLeaf.MAX_ENCODING_SIZE); + + // Blobs + Blob maxBlob=Blob.create(new byte[Blob.CHUNK_LENGTH]); + assertEquals(Blob.MAX_ENCODING_LENGTH,maxBlob.getEncodingLength()); + assertEquals(Blob.MAX_ENCODING_LENGTH,Blobs.MAX_ENCODING_LENGTH); + + // Address + Address maxAddress=Address.create(Long.MAX_VALUE); + assertEquals(1+Format.MAX_VLC_LONG_LENGTH,Address.MAX_ENCODING_LENGTH); + assertEquals(Address.MAX_ENCODING_LENGTH,maxAddress.getEncodingLength()); + } +} diff --git a/convex-core/src/test/java/convex/core/data/FuzzTestFormat.java b/convex-core/src/test/java/convex/core/data/FuzzTestFormat.java new file mode 100644 index 000000000..841d3a17c --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/FuzzTestFormat.java @@ -0,0 +1,97 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.Random; + +import org.junit.jupiter.api.Test; + +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.exceptions.MissingDataException; +import convex.core.lang.RT; +import convex.core.util.Utils; + +/** + * Fuzz testing for data formats. + * + * "Testing leads to failure, and failure leads to understanding." - Burt Rutan + */ +public class FuzzTestFormat { + + private static final int NUM_FUZZ = 3000; + private static Random r = new Random(7855875); + + @Test + public void fuzzTest() { + // create lots of blobs, see what is readable + + for (int i = 0; i < NUM_FUZZ; i++) { + long stime = System.currentTimeMillis(); + r.setSeed(i * 1000); + Blob b = Blob.createRandom(r, 100); + try { + doFuzzTest(b); + } catch (BadFormatException e) { + /* OK */ + } catch (BufferUnderflowException e) { + /* also OK */ + } catch (MissingDataException e) { + /* also OK */ + } + + if (System.currentTimeMillis() > stime + 100) { + System.err.println("Slow fuzz test: " + b); + } + } + } + + private static void doFuzzTest(Blob b) throws BadFormatException { + ByteBuffer bb = b.getByteBuffer(); + + ACell v = Format.read(bb); + + // If we have read the object, check that we can validate as a cell, at minimum + try { + RT.validate(v); + } catch (InvalidDataException e) { + throw new BadFormatException("Validation failed",e); + } + + // if we manage to read the object and it is not a Ref, it must be in canonical + // format! + assertTrue(Format.isCanonical(v),()->"Not canonical: "+Utils.getClassName(v)); + + Blob b2 = Format.encodedBlob(v); + assertEquals(v, Format.read(b2), + () -> "Expected to be able to regenerate value: " + v + " of type " + Utils.getClass(v)); + assertEquals(bb.position(), b2.count(), () -> { + return "Bad length re-reading " + Utils.getClass(v) + ": " + v + " with encoding " + b.toHexString() + + " and re-encoding" + b2.toHexString(); + }); + + // recursive fuzzing on this value + // this is good to test small mutations of + if (r.nextDouble() < 0.8) { + doMutationTest(b2); + } + + } + + public static void doMutationTest(Blob b) { + try { + byte[] bs = b.getBytes(); + bs[r.nextInt(bs.length)] += (byte) r.nextInt(255); + doFuzzTest(Blob.wrap(bs)); + } catch (BadFormatException e) { + /* OK */ + } catch (BufferUnderflowException e) { + /* also OK */ + } catch (MissingDataException e) { + /* also OK */ + } + } +} diff --git a/convex-core/src/test/java/convex/core/data/GenTestAnyValue.java b/convex-core/src/test/java/convex/core/data/GenTestAnyValue.java new file mode 100644 index 000000000..666ce62f7 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/GenTestAnyValue.java @@ -0,0 +1,143 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.runner.RunWith; + +import com.pholser.junit.quickcheck.From; +import com.pholser.junit.quickcheck.Property; +import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; + +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.store.Stores; +import convex.core.util.Utils; +import convex.test.Samples; +import convex.test.generators.ValueGen; + +@RunWith(JUnitQuickcheck.class) +public class GenTestAnyValue { + + @Property + public void printFormats(@From(ValueGen.class) Object o) { + String s=Utils.print(o); + assertNotNull(s); + assertTrue(s.length()>0); + + // TODO: handle all reader cases + //Object o2=Reader.read(s); + // if (o!=null) assertNotNull(o2); + } + + @Property + public void genericTests(@From(ValueGen.class) ACell o) throws InvalidDataException, BadFormatException { + ObjectsTest.doAnyValueTests(o); + } + + @Property + public void testUpdateRefs(@From(ValueGen.class) Object o) { + if (o instanceof ACell) { + ACell rc=(ACell) o; + int n=rc.getRefCount(); + assertThrows(IndexOutOfBoundsException.class,()->rc.getRef(n)); + assertThrows(IndexOutOfBoundsException.class,()->rc.getRef(-1)); + if (n>0 ) { + assertNotNull(rc.getRef(0)); + assertSame(rc,rc.updateRefs(r->r)); + } + } + } + + @Property + public void testFuzzing(@From(ValueGen.class) ACell o) throws InvalidDataException { + Blob b=Format.encodedBlob(o); + FuzzTestFormat.doMutationTest(b); + + if (o instanceof ACell) { + // break all the refs! This should still pass validateCell(), since it woun't change structure. + ACell c=((ACell)o).updateRefs(r->{ + byte[] badBytes=r.getHash().getBytes(); + Utils.writeInt(badBytes, 28,12255); + Hash badHash=Hash.wrap(badBytes); + return Ref.forHash(badHash); + }); + c.validateCell(); + } + } + + @Property + public void validEmbedded(@From(ValueGen.class) ACell o) throws InvalidDataException, BadFormatException { + if (Format.isEmbedded(o)) { + ACell.createPersisted(o); // may have child refs to persist + Blob data=Format.encodedBlob(o); + + ACell o2=Format.read(data); + + // check round trip properties + assertEquals(o,o2); + AArrayBlob data2=Format.encodedBlob(o2); + assertEquals(data,data2); + assertTrue(Format.isEmbedded(o2)); + + // when we persist a ref to an embedded object, should be the object itself + Ref ref=Ref.get(o); + assertEquals(data,ref.getEncoding()); // should encode ref same as value + } else { + // when we persist a ref to non-embedded object, should be a ref type + Ref ref=Ref.get(o); + Blob b=ref.getEncoding(); + assertEquals(Tag.REF,b.byteAt(0)); + assertEquals(Ref.INDIRECT_ENCODING_LENGTH,b.count()); + } + } + + @Property (trials=20) + public void dataRoundTrip(@From(ValueGen.class) ACell o) throws BadFormatException { + Blob data=Format.encodedBlob(o); + + // introduce a small offset to ensure blobs working correctly + data=Samples.ONE_ZERO_BYTE_DATA.append(data).slice(1).toBlob(); + + Ref dataRef=Ref.get(o).persist(); // ensure in store + Hash hash=Hash.compute(o); + assertEquals(dataRef.getHash(),hash); + + // re-read data, should be canonical + ACell o2=Format.read(data); + assertTrue(Format.isCanonical(o2)); + + // equality checks + assertEquals(o,o2); + if (o!=null) assertEquals(o.hashCode(),o2.hashCode()); + assertEquals(hash,Hash.compute(o2)); + + // re-encoding + AArrayBlob data2=Format.encodedBlob(o2); + assertEquals(data,data2); + + // simulate retrieval via hash + Ref dataRef2=Stores.current().refForHash(hash); + if (dataRef2!=null) { + // Have in store + assertEquals(dataRef,dataRef2); + Ref r2=Ref.forHash(hash); + ACell o3=r2.getValue(); + assertEquals(o,o3); + } + } + + @Property + public void setInclusion(@From(ValueGen.class) ACell o) throws BadFormatException, InvalidDataException { + ASet s=Sets.of(o); + s.validate(); + assertEquals(o,s.iterator().next()); + + ASet s2=s.exclude(o); + assertTrue(s2.isEmpty()); + } + +} diff --git a/convex-core/src/test/java/convex/core/data/GenTestBlobs.java b/convex-core/src/test/java/convex/core/data/GenTestBlobs.java new file mode 100644 index 000000000..9aa79fd0e --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/GenTestBlobs.java @@ -0,0 +1,25 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.runner.RunWith; + +import com.pholser.junit.quickcheck.From; +import com.pholser.junit.quickcheck.Property; +import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; + +import convex.core.util.Utils; +import convex.test.generators.BlobGen; + +@RunWith(JUnitQuickcheck.class) +public class GenTestBlobs { + + @Property + public void testToLong(@From(BlobGen.class) ABlob blob) { + long len=blob.count(); + long lv=blob.toLong(); + + int slen=Math.min(8,Utils.checkedInt(len)); + assertEquals(lv,blob.slice(len-slen,slen).toLong()); + } +} diff --git a/convex-core/src/test/java/convex/core/data/GenTestDataStructures.java b/convex-core/src/test/java/convex/core/data/GenTestDataStructures.java new file mode 100644 index 000000000..fe75e6352 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/GenTestDataStructures.java @@ -0,0 +1,72 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collection; + +import org.junit.runner.RunWith; + +import com.pholser.junit.quickcheck.From; +import com.pholser.junit.quickcheck.Property; +import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; + +import convex.core.lang.RT; +import convex.test.generators.DataStructureGen; + +@RunWith(JUnitQuickcheck.class) +public class GenTestDataStructures { + + @SuppressWarnings("rawtypes") + @Property + public void empty(@From(DataStructureGen.class) ADataStructure a) { + long c = a.count(); + + ADataStructure e = a.empty(); + if (c == 0) { + assertSame(a, e); + } + + assertEquals(0, e.count()); + assertTrue(e.isEmpty()); + assertEquals(0, e.size()); + } + + @SuppressWarnings("rawtypes") + @Property + public void testSequence(@From(DataStructureGen.class) ADataStructure a) { + ASequence seq = RT.sequence(a); + assertEquals(seq.count(), a.count()); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Property + public void testJavaInterface(@From(DataStructureGen.class) ADataStructure a) { + if (a instanceof Collection) { + ACollection coll = (ACollection) a; + + assertThrows(UnsupportedOperationException.class, () -> coll.clear()); + assertThrows(UnsupportedOperationException.class, () -> coll.addAll(coll)); + assertThrows(UnsupportedOperationException.class, () -> coll.retainAll(coll)); + assertThrows(UnsupportedOperationException.class, () -> coll.removeAll(coll)); + assertThrows(UnsupportedOperationException.class, () -> coll.remove(null)); + + if (coll instanceof List) { + List list = (List) a; + ASequence seq = (ASequence) list; // must be an ASequence + + assertThrows(UnsupportedOperationException.class, () -> list.set(0, null)); + assertThrows(UnsupportedOperationException.class, () -> list.addAll(0, coll)); + assertThrows(UnsupportedOperationException.class, () -> list.remove(0)); + + int n = list.size(); + if (n > 0) { + assertEquals(list.get(n - 1), RT.nth(seq, n - 1)); + } + } + } + } + +} diff --git a/convex-core/src/test/java/convex/core/data/GenTestMap.java b/convex-core/src/test/java/convex/core/data/GenTestMap.java new file mode 100644 index 000000000..e347cef0e --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/GenTestMap.java @@ -0,0 +1,72 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.runner.RunWith; + +import com.pholser.junit.quickcheck.From; +import com.pholser.junit.quickcheck.Property; +import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; + +import convex.test.generators.MapGen; +import convex.test.generators.PrimitiveGen; + +@RunWith(JUnitQuickcheck.class) +public class GenTestMap { + + @SuppressWarnings({ "rawtypes" }) + @Property + public void primitiveAssoc(@From(MapGen.class) AHashMap m, @From(PrimitiveGen.class) ACell prim) { + long n = m.count(); + long expectedN = (m.containsKey(prim)) ? n : n + 1; + + // add the key + m = m.assoc(prim, prim); + assertSame(prim, m.get(prim)); + assertEquals(expectedN, m.size()); + + // remove the key + m = m.dissoc(prim); + assertNull(m.get(prim)); + assertEquals(expectedN - 1, m.size()); + + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Property + public void mapToIdentity(@From(MapGen.class) AHashMap m) { + AHashMap m2 = m.mapEntries(e -> e); + + // check that the map is unchanged + assertTrue(m2 == m); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Property + public void merging1(@From(MapGen.class) AHashMap m) { + m = m.filterValues(v -> v != null); // don't want null values, to avoid accidental entry removal + + AMap m1 = m.mergeWith(m, (a, b) -> a); + assertEquals(m, m1); + + AMap m2 = m.mergeWith(Maps.empty(), (a, b) -> a); + assertEquals(m, m2); + + AMap m3 = m.mergeWith(Maps.empty(), (a, b) -> b); + assertSame(Maps.empty(), m3); + + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Property + public void merging2(@From(MapGen.class) AHashMap a, + @From(MapGen.class) AHashMap b) { + long[] c = new long[] { 0L }; + a.mergeWith(b, (va, vb) -> ((c[0]++ & 1) == 0L) ? va : vb); + assertTrue(c[0] >= Math.max(a.count(), b.count())); + } + +} diff --git a/convex-core/src/test/java/convex/core/data/GenTestMessages.java b/convex-core/src/test/java/convex/core/data/GenTestMessages.java new file mode 100644 index 000000000..9ff883669 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/GenTestMessages.java @@ -0,0 +1,25 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.ByteBuffer; + +import org.junit.runner.RunWith; + +import com.pholser.junit.quickcheck.Property; +import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; + +import convex.core.exceptions.BadFormatException; + +@RunWith(JUnitQuickcheck.class) +public class GenTestMessages { + + @Property + public void messageLengthVLC(Long a) throws BadFormatException { + ByteBuffer bb = ByteBuffer.allocate(Format.MAX_VLC_LONG_LENGTH); + Format.writeVLCLong(bb, a); + bb.flip(); + assertEquals(bb.remaining(), Format.getVLCLength(a)); + assertEquals(a, Format.readVLCLong(bb)); + } +} diff --git a/convex-core/src/test/java/convex/core/data/GenTestStrings.java b/convex-core/src/test/java/convex/core/data/GenTestStrings.java new file mode 100644 index 000000000..675265629 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/GenTestStrings.java @@ -0,0 +1,19 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.runner.RunWith; + +import com.pholser.junit.quickcheck.From; +import com.pholser.junit.quickcheck.Property; +import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; + +import convex.test.generators.StringGen; + +@RunWith(JUnitQuickcheck.class) +public class GenTestStrings { + @Property + public void testStringProperties(@From(StringGen.class) AString a) { + assertEquals(a,Strings.create(a.toString())); + } +} diff --git a/convex-core/src/test/java/convex/core/data/GenTestVectors.java b/convex-core/src/test/java/convex/core/data/GenTestVectors.java new file mode 100644 index 000000000..fb03977ea --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/GenTestVectors.java @@ -0,0 +1,44 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.runner.RunWith; + +import com.pholser.junit.quickcheck.From; +import com.pholser.junit.quickcheck.Property; +import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; + +import convex.test.generators.VectorGen; + +@RunWith(JUnitQuickcheck.class) +public class GenTestVectors { + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Property + public void testGenericProperties(@From(VectorGen.class) AVector a) { + VectorsTest.doVectorTests(a); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Property + public void testConcatPrefixes(@From(VectorGen.class) AVector a, @From(VectorGen.class) AVector b) { + long al = a.count(); + long bl = b.count(); + + AVector ab = a.concat(b); + VectorsTest.doVectorTests(ab); // useful to test these + + assertEquals(al + bl, ab.count()); + + assertEquals(al, a.commonPrefixLength(a)); + assertTrue(al <= a.commonPrefixLength(ab)); + assertTrue(bl <= b.concat(a).commonPrefixLength(b)); + + long cp = a.commonPrefixLength(b); + assertEquals(cp, b.commonPrefixLength(a)); + if (cp > 0) { + assertEquals(a.get(cp - 1), b.get(cp - 1)); + } + } +} diff --git a/convex-core/src/test/java/convex/core/data/KeywordTest.java b/convex-core/src/test/java/convex/core/data/KeywordTest.java new file mode 100644 index 000000000..5a51e06a5 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/KeywordTest.java @@ -0,0 +1,67 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.ByteBuffer; + +import org.junit.jupiter.api.Test; + +import convex.core.Constants; +import convex.core.exceptions.BadFormatException; +import convex.core.util.Text; +import convex.test.Samples; + +public class KeywordTest { + + @Test + public void testBadKeywords() { + assertNotNull(Keyword.create(Text.whiteSpace(Constants.MAX_NAME_LENGTH))); + + // null return for invalid names + assertNull(Keyword.create(Text.whiteSpace(Constants.MAX_NAME_LENGTH+1))); + assertNull(Keyword.create("")); + assertNull(Keyword.create((String)null)); + + // exception for invalid names using createChecked + assertThrows(IllegalArgumentException.class, () -> Keyword.createChecked(Text.whiteSpace(Constants.MAX_NAME_LENGTH+1))); + assertThrows(IllegalArgumentException.class, () -> Keyword.createChecked("")); + assertThrows(IllegalArgumentException.class, () -> Keyword.createChecked((AString)null)); + assertThrows(IllegalArgumentException.class, () -> Keyword.createChecked((String)null)); + } + + @Test + public void testBadFormat() { + // should fail because this is an empty String + assertThrows(BadFormatException.class, () -> Keyword.read(Blob.fromHex("00").toByteBuffer())); + } + + @Test + public void testRoundTripRegression() throws BadFormatException { + Keyword k=Keyword.create("key17"); + + Blob enc=Blob.fromHex("33056b65793137"); + + assertEquals(enc,k.getEncoding()); + + ByteBuffer bb=enc.getByteBuffer(); + assertEquals(Tag.KEYWORD,bb.get()); + Keyword k2=Keyword.read(bb); + + assertEquals(k,k2); + assertEquals(enc,k.getEncoding()); + } + + @Test + public void testNormalKeyword() { + Keyword k = Keyword.create("foo"); + assertEquals(Samples.FOO, k); + + assertEquals("foo", k.getName().toString()); + assertEquals(":foo", k.toString()); + assertEquals(5, k.getEncoding().length); // tag+length+3 name + + } +} diff --git a/convex-core/src/test/java/convex/core/data/ListsTest.java b/convex-core/src/test/java/convex/core/data/ListsTest.java new file mode 100644 index 000000000..14b18b47b --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/ListsTest.java @@ -0,0 +1,90 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import convex.core.data.prim.CVMLong; +import convex.test.Samples; + +public class ListsTest { + + @Test + public void testEmptyList() { + AList e = Lists.empty(); + assertEquals(0, e.size()); + assertSame(e, Lists.of()); + assertFalse(e.contains(null)); + doListTests(e); + } + + @Test + public void testPrimitiveEquality() { + assertEquals(Lists.of(1L), Lists.of(1L)); + } + + @Test + public void testListToArray() { + assertEquals(3, Lists.of(1, 2, 3).toArray().length); + assertEquals(0, Lists.empty().toArray().length); + } + + @Test public void testDrop() { + assertSame(Lists.empty(), Lists.empty().drop(0)); + assertNull(Lists.empty().drop(1)); + + AList ll=Lists.of(1L, 2L, 3L); + + assertSame(ll,ll.drop(0)); + assertSame(Lists.empty(),ll.drop(3)); + + assertEquals(Lists.of(2L,3L),ll.drop(1)); + assertEquals(Lists.of(3L),ll.drop(2)); + assertNull(ll.drop(5)); + + assertEquals(Lists.of(299),Samples.INT_LIST_300.drop(299)); + assertNull(Samples.INT_LIST_300.drop(400)); + } + + @Test + public void testToString() { + assertEquals("(1 2 3)",Lists.of(1L, 2L, 3L).toString()); + } + + @Test + public void testContainsAll() { + assertTrue(Lists.of(1, 2, 3).containsAll(Sets.of(2, 3))); + assertFalse(Lists.of(1, 2).containsAll(Sets.of(2, 3, 4))); + } + + @Test + public void testGenericListSamples() { + doListTests(Lists.of(1, 2L, Vectors.empty())); + doListTests(Samples.INT_LIST_10); + doListTests(Samples.INT_LIST_300); + } + + /** + * Generic tests for any list + * @param a Any List + */ + public static void doListTests(AList a) { + long n = a.count(); + + if (n == 0) { + assertSame(Lists.empty(), a); + } else { + T first = a.get(0); + assertEquals(first, a.iterator().next()); + } + + assertEquals(a, Lists.of(a.toArray())); + + // call inherited sequence tests + CollectionsTest.doSequenceTests(a); + } +} diff --git a/convex-core/src/test/java/convex/core/data/MapsTest.java b/convex-core/src/test/java/convex/core/data/MapsTest.java new file mode 100644 index 000000000..19b0d5799 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/MapsTest.java @@ -0,0 +1,361 @@ +package convex.core.data; + +import static convex.test.Assertions.assertCVMEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; + +import convex.core.data.prim.CVMBool; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.exceptions.ValidationException; +import convex.core.init.InitTest; +import convex.core.lang.RT; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Transfer; +import convex.core.util.Bits; +import convex.test.Samples; + +/** + * Tests for CVM Map data structures. + */ +public class MapsTest { + + @Test + public void testMapBuilding() throws InvalidDataException, ValidationException { + int SIZE = 1000; + + AMap m = Maps.empty(); + for (long i = 0; i < SIZE; i++) { + CVMLong ci=RT.cvm(i); + assertFalse(m.containsKey(ci)); + m = m.assoc(ci, ci); + // Log.debug(i+ ": "+m); + if ((i < 10) || (i % 23 == 0)) m.validate(); // PERF: only check some steps + assertEquals(i + 1, m.size()); + assertEquals(ci, m.get(ci)); + assertTrue(m.containsKey(ci)); + } + + long C = 1000000; + assertEquals(SIZE * (SIZE - 1) / 2 + C, (long) m.reduceValues((acc, a) -> acc + a.longValue(), C)); + assertEquals(SIZE * (SIZE - 1) + C, (long) m.reduceEntries((acc, e) -> acc + e.getKey().longValue() + e.getValue().longValue(), C)); + + for (long i = 0; i < SIZE; i++) { + CVMLong ci=RT.cvm(i); + assertTrue(m.containsKey(ci)); + m = m.dissoc(ci); + assertEquals(SIZE - i - 1, m.size()); + assertNull(m.get(ci)); + if ((i < 10) || (i % 31 == 0)) m.validate(); // PERF: only check some steps + } + + assertTrue(m.isEmpty()); + } + + @Test + public void testDiabolicalMaps() { + // test that we can at least get hashes without nasty recursion + // shouldn't create maps without hashes in key values + assertNotNull(Samples.DIABOLICAL_MAP_2_10000.getHash()); + assertNotNull(Samples.DIABOLICAL_MAP_30_30.getHash()); + + // TestCollections.doMapTests(Samples.DIABOLICAL_MAP_2_10000); + // TestCollections.doMapTests(Samples.DIABOLICAL_MAP_30_30); + } + + @Test + public void testTreeIndexesForDigits() { + assertEquals(0, Bits.indexForDigit(0, (short) 0x111)); + assertEquals(-1, Bits.indexForDigit(2, (short) 0x111)); + assertEquals(1, Bits.indexForDigit(4, (short) 0x111)); + assertEquals(2, Bits.indexForDigit(8, (short) 0x111)); + assertEquals(-1, Bits.indexForDigit(15, (short) 0x111)); + + assertEquals(0, Bits.indexForDigit(4, (short) 0x010)); + assertEquals(-1, Bits.indexForDigit(3, (short) 0x010)); + assertEquals(-1, Bits.indexForDigit(5, (short) 0x010)); + } + + @Test + public void testTreePositionsForDigits() { + assertEquals(0, Bits.positionForDigit(0, (short) 0x111)); + assertEquals(1, Bits.positionForDigit(2, (short) 0x111)); + assertEquals(1, Bits.positionForDigit(4, (short) 0x111)); + assertEquals(2, Bits.positionForDigit(8, (short) 0x111)); + assertEquals(3, Bits.positionForDigit(15, (short) 0x111)); + + assertEquals(0, Bits.positionForDigit(4, (short) 0x010)); + assertEquals(0, Bits.positionForDigit(3, (short) 0x010)); + assertEquals(1, Bits.positionForDigit(5, (short) 0x010)); + } + + @Test + public void testContains() { + assertTrue(Samples.LONG_MAP_10.containsValue(RT.cvm(3L))); + assertFalse(Samples.LONG_MAP_10.containsValue(RT.cvm(12L))); + assertTrue(Samples.LONG_MAP_100.containsValue(RT.cvm(12L))); + assertFalse(Samples.LONG_MAP_100.containsValue(RT.cvm(100L))); + } + + @Test + public void testContainsRef() { + assertTrue(Samples.LONG_MAP_10.containsKeyRef(Ref.get(RT.cvm(1L)))); + assertFalse(Samples.LONG_MAP_10.containsKeyRef(Ref.get(RT.cvm(12L)))); + assertTrue(Samples.LONG_MAP_100.containsKeyRef(Ref.get(RT.cvm(12L)))); + assertFalse(Samples.LONG_MAP_100.containsKeyRef(Ref.get(RT.cvm(100L)))); + } + + @Test + public void testMapToString() { + AMap m = Maps.empty(); + assertEquals("{}", m.toString()); + m = m.assoc(RT.cvm(1L), RT.cvm(2L)); + assertEquals("{1 2}", m.toString()); + } + + @Test + public void testTruncateHexDigits() throws InvalidDataException, BadFormatException { + AHashMap m = Samples.LONG_MAP_100; + assertEquals(100, m.count()); + AHashMap m1 = m.mapEntries(e -> (e.getKeyHash().getHexDigit(0) < 8) ? e : null); + AHashMap m2 = m.mapEntries(e -> (e.getKeyHash().getHexDigit(0) >= 8) ? e : null); + assertEquals(100, m1.count() + m2.count()); + m1.validate(); + m2.validate(); + // should merge back to original map. Useful also for testing empty children + // merges. + assertEquals(m, m1.mergeDifferences(m2, (a, b) -> (a == null) ? b : a)); + } + + @Test + public void regressionEmbeddedTransfer() throws BadFormatException { + ATransaction trans=Transfer.create(InitTest.HERO,0, InitTest.HERO, 58); + CVMLong key=CVMLong.create(23771L); + AMap m=Maps.create(key,trans); + MapEntry me=m.entryAt(0); + assertEquals(key,me.getKey()); + assertEquals(trans,me.getValue()); + + // transaction should never be embedded + assertEquals(trans.isEmbedded(),me.getValueRef().isEmbedded()); + + Blob b=m.getEncoding(); + AMap m2=Format.read(b); + + assertEquals(m,m2); + + Blob b2=m2.getEncoding(); + assertEquals(b,b2); + } + + @Test + public void testMapToNull() throws InvalidDataException, BadFormatException { + // check that we obtain the singleton instance, using a map to null to remove + // keys + assertSame(Maps.empty(), Samples.LONG_MAP_100.mapEntries(e -> null)); + assertSame(Maps.empty(), Samples.LONG_MAP_10.mapEntries(e -> null)); + } + + @Test + public void testTreeDigitForIndex() { + assertEquals(5, MapTree.digitForIndex(0, (short) 0x020)); + + assertEquals(0, MapTree.digitForIndex(0, (short) 0x111)); + assertEquals(4, MapTree.digitForIndex(1, (short) 0x111)); + assertEquals(8, MapTree.digitForIndex(2, (short) 0x111)); + } + + @Test + public void testBadDigitNegative() { + assertThrows(IllegalArgumentException.class, () -> MapTree.digitForIndex(-1, (short) 0x111)); + assertThrows(IllegalArgumentException.class, () -> MapTree.digitForIndex(3, (short) 0x111)); + } + + @Test + public void testSmallMergeIndentity() { + AHashMap m0 = Maps.empty(); + AHashMap m1 = Maps.of(1, 2, 3, 4); + AHashMap m2 = Maps.of(3, 4, 5, 6); + AHashMap m3 = Maps.of(1, 2, 3, 4, 5, 6); + + assertSame(m0, m1.mergeWith(m3, (a, b) -> null)); + assertSame(m3, m3.mergeWith(m3, (a, b) -> a)); + assertSame(m2, m2.mergeWith(m3, (a, b) -> a)); + assertSame(m2, m2.mergeWith(m1, (a, b) -> a)); + assertSame(m0, m3.mergeWith(m3, (a, b) -> null)); + assertTrue(m3.equals(m2.mergeWith(m3, (a, b) -> b))); + + } + + @Test + public void regressionCreateWithDuplicateEntries() { + MapEntry e = MapEntry.of(1L, 2L); + AMap m = Maps.create(Vectors.of(e, e)); + assertEquals(1, m.size()); + } + + @Test + public void regressionTestMerge() throws InvalidDataException { + AHashMap m = Maps.of(Blob.fromHex("798b809c"), null); + m.validate(); + // this should remove the entry, since mergeWith removes null values + AHashMap m1 = m.mergeWith(m, (a, b) -> a); + assertSame(Maps.empty(), m1); + + CollectionsTest.doMapTests(m); + } + + @Test + public void testDuplicateEntryCreate() { + AMap m = Maps.of(10, 2, 10, 3); + assertEquals(1, m.size()); + assertEquals(RT.cvm(10L), m.entryAt(0).getKey()); + } + + @Test + public void testFilterHex() { + MapLeaf m = Maps.of(1, true, 2, true, 3, true, -1000, true); + assertEquals(4L,m.count()); + + // TODO: selective filter + //assertEquals(Maps.of(3L, true), m.filterHexDigits(0, 64)); // hex digit 0 = 6 only + + assertSame(m, m.filterHexDigits(0, 0xFFFF)); // all digits selected + assertSame(Maps.empty(), m.filterHexDigits(0, 0)); // all digits selected + } + + private static final Predicate EVEN_PRED = a -> { + return (a.longValue() & 1L) == 0L; + }; + + @Test + public void testFilterValues10() { + AHashMap m = Samples.LONG_MAP_10; + AHashMap m2 = m.filterValues(EVEN_PRED); + assertEquals(5, m2.size()); + } + + @Test + public void testFilterValues100() { + AHashMap m = Samples.LONG_MAP_100; + AHashMap m2 = m.filterValues(EVEN_PRED); + assertEquals(50, m2.size()); + + } + + @Test + public void testEmpty() { + AMap m=Maps.empty(); + assertEquals(0L,m.count()); + assertSame(m,Maps.empty()); + + assertEquals(2L,m.getEncoding().count()); + } + + @Test + public void testEquals() { + AMap m = Samples.LONG_MAP_100; + assertNotEquals(m, m.assoc(null, null)); + assertNotEquals(m, m.assoc(RT.cvm(2L), RT.cvm(3L))); + + CollectionsTest.doMapTests(m); + } + + @Test + public void testEqualsKeys() { + assertTrue(Maps.empty().equalsKeys(Maps.of())); + assertFalse(Maps.of(1, 2, 3, 4).equalsKeys(Maps.of(1, 2, 4, 5))); + assertTrue(Maps.of(1, 2, 3, 4).equalsKeys(Maps.of(1, 4, 3, 2))); + } + + @SuppressWarnings("unchecked") + @Test + public void testTreeMapBuilding() { + assertThrows(Throwable.class, () -> MapTree.create(new MapEntry[] { MapEntry.of(1, 2) }, 0)); + } + + @Test + public void testMapEntry() { + AMap m = Maps.of(1L, 2L); + MapEntry me = m.getEntry(RT.cvm(1L)); + assertCVMEquals(1L, me.getKey()); + assertCVMEquals(2L, me.getValue()); + + // out of range assocs + assertNull( me.assoc(2, RT.cvm(3L))); + assertNull( me.assoc(-1, RT.cvm(0L))); + + assertThrows(UnsupportedOperationException.class, () -> me.setValue(RT.cvm(6L))); + + assertEquals(me, me.assoc(0, RT.cvm(1L))); + assertEquals(me, me.assoc(1, RT.cvm(2L))); + + + assertTrue(me.contains(RT.cvm(1L))); + assertTrue(me.contains(RT.cvm(2L))); + assertFalse(me.contains(CVMBool.TRUE)); + assertFalse(me.contains(null)); + + // generic tests for MapEntry treated as a vector + VectorsTest.doVectorTests(me); + } + + @Test + public void testAssocs() { + AMap m = Maps.of(1L, 2L); + assertSame(m, m.assoc(RT.cvm(1L), RT.cvm(2L))); + + CollectionsTest.doMapTests(m); + } + + @Test + public void testConj() { + AMap m = Maps.of(1L, 2L); + AMap me = Maps.of(1L, 2L, 3L, 4L); + assertEquals(m, m.conj(Vectors.of(1L, 2L))); + assertEquals(me, m.conj(Vectors.of(3L, 4L))); + assertEquals(me, m.conj(MapEntry.of(3L, 4L))); + + // failures with conj'ing things that aren't valid map entries + assertNull(m.conj(Vectors.empty())); + assertNull(m.conj(Vectors.of(1L))); + assertNull(m.conj(Vectors.of(1L, 2L, 3L))); + assertNull(m.conj(null)); + + CollectionsTest.doMapTests(me); + + } + + @Test + public void testMergeWith() { + AHashMap m = Maps.of(1L, 1L, 2L, 2L, 3L, 3L, 4L, 4L, 5L, 5L, 6L, 6L, 7L, 7L, 8L, 8L); + AHashMap m2 = m.mergeWith(m, (a, b) -> ((a.longValue() & 1L) == 0L) ? a : null); + assertEquals(4, m2.size()); + + AHashMap bm = Maps.coerce(Samples.LONG_MAP_100); + AHashMap sm = Maps.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + // change values in big map using small map + AHashMap bm2 = bm.mergeWith(sm, (a, b) -> { + return (a == null) ? b : a; + }); + assertEquals(100, bm2.count()); + assertEquals(bm2, sm.mergeWith(bm, (a, b) -> { + return (b == null) ? a : b; + })); + + CollectionsTest.doMapTests(m); + CollectionsTest.doMapTests(m2); + } +} diff --git a/convex-core/src/test/java/convex/core/data/ObjectsTest.java b/convex-core/src/test/java/convex/core/data/ObjectsTest.java new file mode 100644 index 000000000..af2b31012 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/ObjectsTest.java @@ -0,0 +1,183 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import convex.core.Constants; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.store.AStore; +import convex.core.store.MemoryStore; +import convex.core.store.Stores; +import convex.core.util.Utils; + +/** + * Generic test functions for arbitrary Data Objects. + */ +public class ObjectsTest { + + /** + * Generic tests for a Cell + * + * @param a Cell to test + */ + public static void doCellTests(ACell a) { + if (a==null) return; + + assertEquals(a.getEncodingLength(),a.getEncoding().count()); + + try { + a.validateCell(); + // doCellStorageTest(a); // TODO: Maybe fix after we have ACell.toDirect() + } catch (InvalidDataException e) { + throw Utils.sneakyThrow(e); + } + + if (a.getRefCount()>0) { + doRefContainerTests(a); + } + + if (a.isCanonical()) { + // Canonical objects should map to themselves + assertSame(a,a.toCanonical()); + } else { + // non-canonical objects should map to a canonical object + ACell canon=a.toCanonical(); + assertNotSame(canon,a); + assertTrue(canon.isCanonical()); + assertEquals(a,canon); + } + + doCellRefTests(a); + } + + private static void doCellRefTests(ACell a) { + Ref cachedRef=a.cachedRef; + Ref ref=a.getRef(); + if (cachedRef!=null) assertSame(ref,cachedRef); + assertSame(ref,a.getRef()); + + assertEquals(a.isEmbedded(),ref.isEmbedded()); + + Ref refD=ref.toDirect(); + assertTrue(ref.equalsValue(refD)); + assertTrue(refD.equalsValue(ref)); + + } + + @SuppressWarnings("unused") + private static void doCellStorageTest(ACell a) throws InvalidDataException { + + AStore temp=Stores.current(); + try { + // test using a new memory store + MemoryStore ms=new MemoryStore(); + Stores.setCurrent(ms); + + Ref r=a.getRef(); + + Hash hash=r.getHash(); + + assertNull(ms.refForHash(hash)); + + // persist the Ref + ACell.createPersisted(a); + + // retrieve from store + Ref rr=ms.refForHash(hash); + + // should be able to retrieve and validate complete structure + assertNotNull(rr,()->"Failed to retrieve from store with "+Utils.getClassName(a) + " = "+a); + ACell b=rr.getValue(); + b.validate(); + assertEquals(a,b); + } finally { + Stores.setCurrent(temp); + } + } + + /** + * Generic tests for any CVM Value + * + * @param a Value to test + */ + public static void doAnyValueTests(ACell a) { + Hash h=Hash.compute(a); + + boolean embedded=Format.isEmbedded(a); + + Ref r = Ref.get(a).persist(); + assertEquals(h,r.getHash()); + assertEquals(a, r.getValue()); + + Blob encoding = Format.encodedBlob(a); + if (a==null) { + assertEquals(Blob.NULL_ENCODING,encoding); + } else { + assertEquals(a.getTag(),encoding.byteAt(0)); // Correct Tag + assertSame(encoding,a.getEncoding()); // should be same cached encoding + assertEquals(encoding.length,a.getEncodingLength()); + + if (a.isCVMValue()) { + assertNotNull(a.getType()); + } + + } + + + // Any encoding should be less than or equal to the limit + assertTrue(encoding.length <= Format.LIMIT_ENCODING_LENGTH); + + // If length exceeds MAX_EMBEDDED_LENGTH, cannot be an embedded value + if (encoding.length > Format.MAX_EMBEDDED_LENGTH) { + assertFalse(Format.isEmbedded(a),()->"Testing: "+Utils.getClassName(a)+ " = "+Utils.toString(a)); + } + + // tests for memory size + if (a!=null) { + long memorySize=a.getMemorySize(); + long encodingSize=a.getEncodingLength(); + int rc=a.getRefCount(); + long childMem=0; + for (int i=0; i childRef=a.getRef(i); + long cms=childRef.getMemorySize(); + childMem+=cms; + } + if (embedded) { + assertEquals(memorySize,childMem); + } else { + assertEquals(memorySize,encodingSize+childMem+Constants.MEMORY_OVERHEAD); + } + } + + + try { + ACell a2; + a2 = Format.read(encoding); + assertEquals(a, a2); + } catch (BadFormatException e) { + throw new Error("Can't read encoding: " + encoding.toHexString(), e); + } + + doCellTests(a); + } + + /** + * Tests for any value implementing the IRefContainer interface + * + * @param a + */ + private static void doRefContainerTests(ACell a) { + long tcount = Utils.totalRefCount(a); + int rcount = Utils.refCount(a); + + assertTrue(rcount <= tcount); + } + +} diff --git a/convex-core/src/test/java/convex/core/data/ParamTestBlobs.java b/convex-core/src/test/java/convex/core/data/ParamTestBlobs.java new file mode 100644 index 000000000..764fe47b2 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/ParamTestBlobs.java @@ -0,0 +1,72 @@ +package convex.core.data; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Random; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import convex.core.crypto.HashTest; +import convex.test.Samples; + +@RunWith(Parameterized.class) +public class ParamTestBlobs { + private ABlob data; + + public ParamTestBlobs(String label, ABlob data) { + this.data = data; + } + + private static Random rand = new Random(1234); + + @Parameterized.Parameters(name = "{index}: {0}") + public static Collection dataExamples() { + return Arrays.asList(new Object[][] { { "Empty bytes", Blob.wrap(new byte[0]) }, + { "Short hex string CAFEBABE", Blob.fromHex("CAFEBABE") }, + { "Long random BlobTree", BlobTree.create(Blob.createRandom(rand, 10000)) }, + { "Length 2 strict sublist of byte data", Blob.create(new byte[] { 1, 2, 3, 4 }, 1, 2) }, + { "Bitcoin genesis header block", Blob.fromHex(HashTest.GENESIS_HEADER) }, + { "Max size embedded blob", Samples.MAX_EMBEDDED_BLOB }, + { "Full Blob of random data", Samples.FULL_BLOB }, { "Big blob", Samples.BIG_BLOB_TREE } }); + } + + @Test + public void testHexRoundTrip() { + String hex = data.toHexString(); + ABlob d2 = Blobs.fromHex(hex); + assertEquals(data, d2); + assertEquals(data.hashCode(), d2.hashCode()); + } + + @Test + public void testSlice() { + long n=data.count(); + ABlob full = data.slice(0, n); + assertEquals(data, full); + BlobsTest.doBlobTests(full); + + ABlob half = data.slice(n/2,n/2); + BlobsTest.doBlobTests(half); + } + + @Test + public void testCompare() { + long len = data.count(); + assertEquals(0, data.compareTo(data)); + assertEquals(0, data.compareTo(Blob.create(data.getBytes()))); + + assertTrue(data.compareTo(data.append(Samples.ONE_ZERO_BYTE_DATA)) < 0); + + if (len > 0) { + // anything should be "larger" than empty data + assertTrue(data.compareTo(Blob.EMPTY) > 0); + assertTrue(Blob.EMPTY.compareTo(data) < 0); + } + + } +} diff --git a/convex-core/src/test/java/convex/core/data/ParamTestOps.java b/convex-core/src/test/java/convex/core/data/ParamTestOps.java new file mode 100644 index 000000000..4f0e25b46 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/ParamTestOps.java @@ -0,0 +1,79 @@ +package convex.core.data; + +import static convex.test.Assertions.assertCVMEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import convex.core.State; +import convex.core.data.prim.CVMBool; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.exceptions.ValidationException; +import convex.core.init.InitTest; +import convex.core.lang.AOp; +import convex.core.lang.Context; +import convex.core.lang.RT; +import convex.core.lang.TestState; +import convex.core.lang.ops.Cond; +import convex.core.lang.ops.Constant; +import convex.core.lang.ops.Def; +import convex.core.lang.ops.Do; +import convex.core.lang.ops.Invoke; +import convex.core.lang.ops.Lookup; + +@RunWith(Parameterized.class) +public class ParamTestOps { + private AOp op; + private Object expected; + + private static final State INITIAL_STATE = TestState.STATE; + + public ParamTestOps(String label, AOp v, Object expected) { + this.op = v; + this.expected = expected; + } + + @Parameterized.Parameters(name = "{index}: {0}") + public static Collection dataExamples() throws BadFormatException { + return Arrays + .asList(new Object[][] { + { "Constant", Constant.of(1L), RT.cvm(1L) }, + { "Lookup", Do.create(Def.create("foo", Constant.of(13)), + Lookup.create("foo")), RT.cvm(13) }, + { "Def", Def.create("foo", Constant.createString("bar")), Strings.create("bar") }, + { "Vector", Invoke.create("vector", Constant.createString("foo"), Constant.createString("bar")), + Vectors.of(Strings.create("foo"), Strings.create("bar")) }, + + { "Do", Do.create(Constant.createString("foo"), Constant.createString("bar")), Strings.create("bar") }, + { "Cond", + Cond.create(Constant.of(CVMBool.TRUE), Constant.createString("truthy"), + Constant.createString("falsey")), + Strings.create("truthy") }, + { "Def", Def.create("foo", Constant.of(1L)), 1L } }); + } + + @Test + public void testExpectedResult() { + long JUICE = 10000; + Context c = Context.createInitial(INITIAL_STATE, InitTest.HERO, JUICE); + Context c2 = c.execute(op); + + assertCVMEquals(expected, c2.getResult()); + } + + @Test + public void testCanonical() { + assertTrue(op.isCanonical()); + } + + @Test + public void testGeneric() throws InvalidDataException, ValidationException { + ObjectsTest.doAnyValueTests(op); + } +} diff --git a/convex-core/src/test/java/convex/core/data/ParamTestRefs.java b/convex-core/src/test/java/convex/core/data/ParamTestRefs.java new file mode 100644 index 000000000..106cc6d26 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/ParamTestRefs.java @@ -0,0 +1,81 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import convex.core.data.prim.CVMLong; +import convex.core.store.AStore; +import convex.core.store.MemoryStore; +import convex.core.store.Stores; +import etch.EtchStore; + +@RunWith(Parameterized.class) +public class ParamTestRefs { + private AStore store; + + public ParamTestRefs(String label,AStore store) { + this.store = store; + } + + @Parameterized.Parameters(name = "{index}: {0}") + public static Collection dataExamples() { + return Arrays + .asList(new Object[][] { + { "Memory Store", new MemoryStore() }, + { "Temp Etch Store", EtchStore.createTemp() } }); + } + + @Test + public void testStoreUsage() { + AStore temp=Stores.current(); + + try { + Stores.setCurrent(store); + + { // single embedded value + CVMLong n=CVMLong.create(1567565765677L); + Ref r=Ref.get(n); + assertTrue(r.isEmbedded()); + Ref r2=r.persist(); + assertTrue(r.isEmbedded()); + assertSame(n,r2.getValue()); + } + + + { // structure with embedded value + AVector v=Vectors.of(6759578996496L); + Ref> r=v.getRef(); + assertEquals(Ref.UNKNOWN,r.getStatus()); + Ref> r2=r.persist(); + assertEquals(Ref.PERSISTED,r2.getStatus()); + assertEquals(v.getRef(0),r2.getValue().getRef(0)); + } + + { // map with embedded structure + AMap> m=Maps.of(156746748L,Vectors.of(8797987L)); + Ref>> r=m.getRef(); + assertEquals(Ref.UNKNOWN,r.getStatus()); + + Ref>> r2=r.persist(); + + assertEquals(Ref.PERSISTED,r2.getStatus()); + MapEntry> me2=r2.getValue().entryAt(0); + assertTrue(me2.getRef(0).isEmbedded()); + assertEquals(Ref.PERSISTED,me2.getRef(1).getStatus()); + } + + } finally { + Stores.setCurrent(temp); + } + } + + +} diff --git a/convex-core/src/test/java/convex/core/data/ParamTestValues.java b/convex-core/src/test/java/convex/core/data/ParamTestValues.java new file mode 100644 index 000000000..7e65816f5 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/ParamTestValues.java @@ -0,0 +1,69 @@ +package convex.core.data; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import convex.core.data.prim.CVMByte; +import convex.core.data.prim.CVMDouble; +import convex.core.data.prim.CVMLong; +import convex.core.data.type.AType; +import convex.core.data.type.Types; +import convex.core.exceptions.InvalidDataException; +import convex.core.exceptions.ValidationException; +import convex.test.Samples; + +@RunWith(Parameterized.class) +public class ParamTestValues { + private ACell data; + + public ParamTestValues(String label, ACell v) { + this.data = v; + } + + @Parameterized.Parameters(name = "{index}: {0}") + public static Collection dataExamples() { + return Arrays.asList(new Object[][] { + { "Keyword :foo", Samples.FOO }, + { "Empty Vector", Vectors.empty() }, + { "Long", CVMLong.ONE }, + { "Double", CVMDouble.ONE }, + { "Byte", CVMByte.ZERO }, + { "Single value map", Maps.of(7, 8) }, + { "Account status", AccountStatus.create(1000L,Samples.ACCOUNT_KEY) }, + { "Peer status", PeerStatus.create(Address.create(11), 1000L, Maps.create(Keywords.URL,Strings.create("http://www.google.com:18888"))) }, + { "Signed value", SignedData.create(Samples.KEY_PAIR, Strings.create("foo")) }, + { "Length 300 vector", Samples.INT_VECTOR_300 } }); + } + + @Test + public void testCanonical() { + assertTrue(data.isCanonical()); + } + + @Test + public void testType() { + AType t=data.getType(); + assertNotNull(t); + assertTrue(t.check(data)); + assertTrue(Types.ANY.check(data)); + } + + @Test + public void testHexRoundTrip() throws InvalidDataException, ValidationException { + ACell.createPersisted(data); + String hex = data.getEncoding().toHexString(); + Blob d2 = Blob.fromHex(hex); + ACell rec = Format.read(d2); + rec.validate(); + assertEquals(data, rec); + assertEquals(data.getEncoding(), rec.getEncoding()); + } +} diff --git a/convex-core/src/test/java/convex/core/data/ParamTestVector.java b/convex-core/src/test/java/convex/core/data/ParamTestVector.java new file mode 100644 index 000000000..754ce8e9b --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/ParamTestVector.java @@ -0,0 +1,77 @@ +package convex.core.data; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import convex.core.exceptions.BadFormatException; +import convex.test.Samples; + +/** + * Parameterised test class for a bunch of vectors. + * + */ +@RunWith(Parameterized.class) +public class ParamTestVector { + private AVector v; + + public ParamTestVector(String label, AVector v) { + this.v = v; + } + + @Parameterized.Parameters(name = "{index}: {0}") + public static Collection dataExamples() { + return Arrays + .asList(new Object[][] { { "Empty Vector", Vectors.empty() }, { "Single value vector", Vectors.of(7L) }, + { "MapEntry vector", MapEntry.of(1L, 2L) }, { "Nested vector", Vectors.of(Vectors.empty()) }, + { "Vector with Account status", Vectors.of(AccountStatus.create(1000L,Samples.ACCOUNT_KEY)) }, + { "Vector with Peer status", Vectors.of(PeerStatus.create(Address.create(11), 1000L)) }, + { "Length 10 vector", Samples.INT_VECTOR_10 }, { "Length 16 vector", Samples.INT_VECTOR_16 }, + { "Length 23 vector", Samples.INT_VECTOR_23 }, { "Length 32 vector", Samples.INT_VECTOR_32 }, + { "Length 300 vector", Samples.INT_VECTOR_300 }, + { "Length 256 tree vector", Samples.INT_VECTOR_256 } }); + } + + @Test + public void testGenericProperties() { + VectorsTest.doVectorTests(v); + } + + @Test + public void testCanonical() { + assertTrue(v.toCanonical().isCanonical()); + } + + @Test + public void testElements() { + int n = v.size(); + for (int i = 0; i < n; i++) { + Object o = v.get(i); + assertEquals(o, v.slice(i, 1).get(0)); + } + + assertThrows(Throwable.class, () -> v.get(-1)); + assertThrows(Throwable.class, () -> v.get(n)); + } + + @Test + public void testBuffer() throws BadFormatException { + int size = v.size(); + ByteBuffer b = ByteBuffer.allocate(3000); + v.write(b); + b.flip(); + AVector rec = Format.read(b); + assertEquals(0, b.remaining()); + assertEquals(size, rec.size()); + assertEquals(v, rec); + } + +} diff --git a/convex-core/src/test/java/convex/core/data/RecordTest.java b/convex-core/src/test/java/convex/core/data/RecordTest.java new file mode 100644 index 000000000..af86fa9fa --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/RecordTest.java @@ -0,0 +1,90 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import convex.core.Belief; +import convex.core.Result; +import convex.core.data.prim.CVMLong; +import convex.core.init.InitTest; +import convex.core.lang.TestState; +import convex.core.lang.impl.RecordFormat; + +public class RecordTest { + + @Test + public void testBelief() { + Belief b=Belief.createSingleOrder(InitTest.FIRST_PEER_KEYPAIR); + assertEquals(b.getRefCount(),b.getOrders().getRefCount()); + + doRecordTests(b); + + } + + public static void doRecordTests(ARecord r) { + + RecordFormat format=r.getFormat(); + + AVector keys=format.getKeys(); + int n=(int) keys.count(); + + AVector vals=r.values(); + assertEquals(n,vals.size()); + + ACell[] vs=new ACell[n]; // new array to extract values + for (int i=0; i me0=r.getEntry(k); + assertEquals(k,me0.getKey()); + assertEquals(v,me0.getValue()); + + // TODO: consider this invariant? + assertEquals(r.toHashMap(),r.assoc(k, v)); + + // indexed access + assertEquals(v,vals.get(i)); + + // indexed entry-wise access + MapEntry me=r.entryAt(i); + assertEquals(k,me.getKey()); + assertEquals(v,me.getValue()); + } + assertThrows(IndexOutOfBoundsException.class,()->r.entryAt(n)); + assertThrows(IndexOutOfBoundsException.class,()->r.entryAt(-1)); + + int rc=r.getRefCount(); + for (int i=0; ir.getRef(rc)); + + assertSame(r,r.updateAll(r.getValuesArray())); + assertSame(r,r.updateAll(r.values().toCellArray())); + + CollectionsTest.doDataStructureTests(r); + } + + @Test + public void testResult() { + String s="{:id 4,:result #44,:error-code nil,:trace nil}"; + AHashMap m=TestState.eval(s); + assertEquals(4,m.count); + assertEquals("0xa8a8f308df3cb0eab838b64a41c2534ad0a07f126ee1291f686182aa32862ae6",m.getHash().toString()); + + Result r=Result.create(CVMLong.create(4), Address.create(44), null, null); + assertEquals(s,r.toString()); + assertEquals("0x63e45711e949f3e9c026df0ba8d0896e17683955e0059423fa0f1238acdfacd8",r.getHash().toString()); + + assertEquals(m,r.toHashMap()); + + doRecordTests(r); + } +} diff --git a/convex-core/src/test/java/convex/core/data/RefTest.java b/convex-core/src/test/java/convex/core/data/RefTest.java new file mode 100644 index 000000000..daa5ac0cc --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/RefTest.java @@ -0,0 +1,182 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Random; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.InvalidDataException; +import convex.core.exceptions.MissingDataException; +import convex.core.lang.RT; +import convex.core.lang.Symbols; +import convex.core.util.Utils; +import convex.test.Samples; + +public class RefTest { + @Test + public void testMissingData() { + // create a Ref using just a bad hash + Ref ref = Ref.forHash(Samples.BAD_HASH); + + // equals comparison should work + assertEquals(ref, Ref.forHash(Samples.BAD_HASH)); + + // gneric properties of missing Ref + assertEquals(Samples.BAD_HASH, ref.getHash()); + assertEquals(Ref.UNKNOWN, ref.getStatus()); // shouldn't know anything about this Ref yet + assertFalse(ref.isDirect()); + + // we expect a failure here + assertThrows(MissingDataException.class, () -> ref.getValue()); + } + + @Test + public void testRefSet() { + // 10 element refs + assertEquals(10, Ref.accumulateRefSet(Samples.INT_VECTOR_10).size()); + assertEquals(10, Utils.totalRefCount(Samples.INT_VECTOR_10)); + + // 256 element refs, 16 tree branches + assertEquals(272, Ref.accumulateRefSet(Samples.INT_VECTOR_256).size()); + assertEquals(272, Utils.totalRefCount(Samples.INT_VECTOR_256)); + + // 11 = 10 element refs plus one for enclosing ref + assertEquals(11, Ref.accumulateRefSet(Samples.INT_VECTOR_10.getRef()).size()); + } + + @Test + public void testShallowPersist() { + Blob bb = Blob.createRandom(new Random(), 100); // unique blob but embedded + assertTrue(bb.isEmbedded()); + + AVector v = Vectors.of(bb,bb,bb,bb); // vector containing big blob four times. Shouldn't be embedded. + assertFalse(v.isEmbedded()); + + Hash bh = bb.getHash(); + Hash vh = v.getHash(); + + Ref> ref = v.getRef().persistShallow(); + assertEquals(Ref.STORED, ref.getStatus()); + + assertThrows(MissingDataException.class, () -> Ref.forHash(bh).getValue()); + + assertFalse(v.isEmbedded()); + assertEquals(v, Ref.forHash(vh).getValue()); + } + + @Test + public void testEmbedded() { + assertTrue(Ref.get(RT.cvm(1L)).isEmbedded()); // a primitive + assertTrue(Ref.NULL_VALUE.isEmbedded()); // singleton null ref + assertTrue(List.EMPTY_REF.isEmbedded()); // singleton null ref + assertFalse(Blob.create(new byte[Format.MAX_EMBEDDED_LENGTH]).getRef().isEmbedded()); // too big to embed + assertTrue(Samples.LONG_MAP_10.getRef().isEmbedded()); // a ref container + } + + @Test + public void testPersistEmbeddedNull() throws InvalidDataException { + Ref nr = Ref.get(null); + assertSame(Ref.NULL_VALUE, nr); + assertSame(nr, nr.persist()); + nr.validate(); + assertTrue(nr.isEmbedded()); + } + + @Test + public void testPersistEmbeddedLong() { + ACell val=RT.cvm(10001L); + Ref nr = Ref.get(val); + assertSame(nr.getValue(), nr.persist().getValue()); + assertTrue(nr.isEmbedded()); + } + + @Test + public void testGoodData() { + AVector value = Vectors.of(Keywords.FOO, Symbols.FOO); + // a good ref + Ref orig = value.getRef(); + assertEquals(Ref.UNKNOWN, orig.getStatus()); + assertFalse(orig.isPersisted()); + orig = orig.persist(); + assertTrue(orig.isPersisted()); + + // a ref using the same hash + if (!(value.isEmbedded())) { + Ref ref = Ref.forHash(orig.getHash()); + assertEquals(orig, ref); + assertEquals(value, ref.getValue()); + } + } + + @Test + public void testCompare() { + assertEquals(0, Ref.get(RT.cvm(1L)).compareTo(ACell.createPersisted(RT.cvm(1L)))); + assertEquals(1, Ref.get(RT.cvm(1L)).compareTo( + Ref.forHash(Hash.fromHex("0000000000000000000000000000000000000000000000000000000000000000")))); + assertEquals(-1, Ref.get(RT.cvm(1L)).compareTo( + Ref.forHash(Hash.fromHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")))); + } + + @Test + public void testVectorRefCounts() { + AVector v=Vectors.of(1,2,3); + assertEquals(3,v.getRefCount()); + + AVector zv=Vectors.repeat(CVMLong.create(0), 16); + assertEquals(16,zv.getRefCount()); + + // 3 tail elements after prefix ref + AVector zvv=zv.concat(v); + assertEquals(4,zvv.getRefCount()); + + } + + @Test + public void testToString() { + AVector v=Vectors.of(1,2,3,4); + Ref> ref=v.getRef(); + assertNotNull(ref.toString()); + } + + @Test + public void testMissing() { + Hash bad=Hash.fromHex("0000000000000000000000000000000000000000000000000000000000000000"); + Ref ref=Ref.forHash(bad); + assertTrue(ref.isMissing()); + + } + + @Test + public void testDiabolicalDeep() { + Ref a = Samples.DIABOLICAL_MAP_2_10000.getRef(); + // TODO: consider if this should be possible, currently not (stack overflow) + // Ref.accumulateRefSet(a); + assertTrue(a.isEmbedded()); + } + + @Test + public void testDiabolicalWide() { + Ref a = Samples.DIABOLICAL_MAP_30_30.getRef(); + // OK since we manage de-duplication + Set> set = Ref.accumulateRefSet(a); + assertEquals(31 + 30 * 16, set.size()); // 16 refs at each level after de-duping + assertFalse(a.isEmbedded()); + } + + @Test + public void testNullRef() { + Ref nullRef = Ref.get(null); + assertNotNull(nullRef); + assertSame(nullRef.getHash(), Hash.NULL_HASH); + assertTrue(nullRef.isEmbedded()); + assertFalse(nullRef.isMissing()); + } +} diff --git a/convex-core/src/test/java/convex/core/data/SetsTest.java b/convex-core/src/test/java/convex/core/data/SetsTest.java new file mode 100644 index 000000000..ffe935bff --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/SetsTest.java @@ -0,0 +1,209 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import convex.core.data.prim.CVMByte; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.RT; +import convex.test.Samples; + +public class SetsTest { + + @Test + public void testEmptySet() { + ASet e = Sets.empty(); + assertEquals(0, e.size()); + assertFalse(e.contains(null)); + } + + @Test + public void testIncludeExclude() { + ASet s = Sets.empty(); + assertEquals("#{}", s.toString()); + s = s.include(RT.cvm(1L)); + assertEquals("#{1}", s.toString()); + s = s.include(RT.cvm(1L)); + assertEquals("#{1}", s.toString()); + s = s.include(RT.cvm(2L)); + assertEquals("#{1,2}", s.toString()); + s = s.exclude(RT.cvm(1L)); + assertEquals("#{2}", s.toString()); + s = s.exclude(RT.cvm(2L)); + assertTrue(s.isEmpty()); + assertSame(s, Sets.empty()); + } + + @Test + public void testPrimitiveEquality() { + // different primitive objects with same numeric value should not collide in set + CVMByte b=CVMByte.create(1); + ASet s=Sets.of(1L).include(b); + assertEquals(2L,s.count()); + + assertEquals(Sets.of(b, 1L), s); + } + + @Test + public void testSetToArray() { + assertEquals(3, Sets.of(1, 2, 3).toArray().length); + assertEquals(0, Sets.empty().toArray().length); + } + + @Test + public void testContainsAll() { + assertTrue(Sets.of(1, 2, 3).containsAll(Sets.of(2, 3))); + assertFalse(Sets.of(1, 2).containsAll(Sets.of(2, 3, 4))); + } + + @Test + public void testSubsets() { + ASet EM=Sets.empty(); + assertTrue(EM.isSubset(EM)); + assertTrue(EM.isSubset(Samples.INT_SET_300)); + assertTrue(EM.isSubset(Samples.INT_SET_10)); + assertFalse(Samples.INT_SET_10.isSubset(EM)); + assertFalse(Samples.INT_SET_300.isSubset(EM)); + + { + ASet s=Samples.createRandomSubset(Samples.INT_SET_300,0.5,1); + assertTrue(s.isSubset(Samples.INT_SET_300)); + } + { + ASet s=Samples.createRandomSubset(Samples.INT_SET_10,0.5,2); + assertTrue(s.isSubset(Samples.INT_SET_10)); + } + + assertTrue(Samples.INT_SET_300.isSubset(Samples.INT_SET_300)); + assertTrue(Samples.INT_SET_10.isSubset(Samples.INT_SET_300)); + assertTrue(Samples.INT_SET_10.isSubset(Samples.INT_SET_10)); + assertFalse(Samples.INT_SET_300.isSubset(Samples.INT_SET_10)); + } + + @Test + public void testMerging() { + ASet a = Sets.of(1, 2, 3); + ASet b = Sets.of(2, 4, 6); + assertTrue(a.contains(RT.cvm(3L))); + assertFalse(b.contains(RT.cvm(3L))); + + assertSame(Sets.empty(), a.disjAll(a)); + assertEquals(Sets.of(1, 2, 3, 4, 6), a.conjAll(b)); + assertEquals(Sets.of(1, 3), a.disjAll(b)); + } + + @Test + public void regressionRead() throws BadFormatException { + ASet v1=Sets.of(43); + Blob b1 = Format.encodedBlob(v1); + + ASet v2=Format.read(b1); + Blob b2 = Format.encodedBlob(v2); + + assertEquals(v1, v2); + assertEquals(b1,b2); + } + + @Test + public void regressionNils() throws InvalidDataException { + AMap m = Maps.of(null, null); + assertEquals(1, m.size()); + assertTrue(m.containsKey(null)); + + ASet s = Sets.of(m); + s.validate(); + s = s.include( m); + s.validate(); + } + + + + @Test + public void testMergingIdentity() { + ASet a = Sets.of(1L, 2L, 3L); + assertSame(a, a.include(RT.cvm(2L))); + assertSame(a, a.includeAll(Sets.of(1L, 3L))); + } + + @Test + public void testIntersection() { + ASet a = Sets.of(1, 2, 3); + + // (intersect a a) => a + assertSame(a,a.intersectAll(a)); + + // (intersect a #{}) => #{} + assertSame(Sets.empty(),a.intersectAll(Sets.of(5,6))); + + // (intersect a b) => a if (subset? a b) + assertEquals(a,a.intersectAll(Samples.INT_SET_10)); + assertEquals(a,a.intersectAll(Samples.INT_SET_300)); + + // regular intersection + assertEquals(Sets.of(2,3),a.intersectAll(Sets.of(2,3,4))); + + assertThrows(Throwable.class,()->a.intersectAll(null)); + } + + @Test + public void testBigMerging() { + ASet s = Sets.create(Samples.INT_VECTOR_300); + CollectionsTest.doSetTests(s); + + ASet s2 = s.includeAll(Sets.of(1, 2, 3, 100)); + assertEquals(s, s2); + assertSame(s, s2); + + ASet s3 = s.disjAll(Samples.INT_VECTOR_300); + assertSame(s3, Sets.empty()); + + ASet s4 = s.excludeAll(Sets.of(-1000)); + assertSame(s, s4); + + ASet s5a = Sets.of(1, 3, 7, -1000); + ASet s5 = s5a.disjAll(s); + assertEquals(Sets.of(-1000), s5); + } + + @Test + public void testIncrementalBuilding() { + ASet set=Sets.empty(); + for (int i=0; i<320; i++) { + assertEquals(i,set.size()); + + // extend set with one new element + CVMLong v=CVMLong.create(i); + ASet newSet=set.conj(v); + + // new Set contains previous set + assertTrue(newSet.containsAll(set)); + + assertNotEquals(set,newSet); + assertTrue(newSet.contains(v)); + assertFalse(set.contains(v)); + + // removing element should get back to original set + assertEquals(set,newSet.exclude(v)); + + // removing original set should leave one element + assertEquals(Sets.of(v),newSet.excludeAll(set)); + + set=newSet; + } + + doSetTests(set); + } + + public static void doSetTests(ASet a) { + + CollectionsTest.doSetTests(a); + } +} diff --git a/convex-core/src/test/java/convex/core/data/SignedDataTest.java b/convex-core/src/test/java/convex/core/data/SignedDataTest.java new file mode 100644 index 000000000..678286bd6 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/SignedDataTest.java @@ -0,0 +1,110 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import convex.core.crypto.AKeyPair; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadSignatureException; +import convex.core.init.InitTest; +import convex.core.lang.RT; +import convex.test.Samples; + +public class SignedDataTest { + @SuppressWarnings("unchecked") + @Test + public void testBadSignature() { + Ref dref = Ref.get(RT.cvm(13L)); + SignedData sd = SignedData.create(Samples.BAD_ACCOUNTKEY, Samples.BAD_SIGNATURE, dref); + + // should not yet be checked + assertFalse(sd.isSignatureChecked()); + + // Signature check should fail since bad signature + assertFalse(sd.checkSignature()); + + // should now be checked + assertTrue(sd.isSignatureChecked()); + + assertTrue((sd.getRef().getFlags()&Ref.BAD_MASK)!=0); + assertEquals(13L, sd.getValue().longValue()); + assertSame(Samples.BAD_ACCOUNTKEY, sd.getAccountKey()); + assertNotNull(sd.toString()); + + assertThrows(BadSignatureException.class, () -> sd.validateSignature()); + + ACell.createPersisted(sd); + + SignedData sd1 = (SignedData) Ref.forHash(sd.getHash()).getValue(); + // should have cached checked signature + assertTrue(sd1.isSignatureChecked()); + assertFalse(sd1.checkSignature()); + + ObjectsTest.doAnyValueTests(sd); + ObjectsTest.doAnyValueTests(sd1); + } + + @Test + public void testEmbeddedSignature() throws BadSignatureException { + CVMLong cl=RT.cvm(158587); + + AKeyPair kp = InitTest.HERO_KEYPAIR; + SignedData sd = kp.signData(cl); + + // should be checked by default + assertTrue(sd.isSignatureChecked()); + + assertTrue(sd.checkSignature()); + + sd.validateSignature(); + assertEquals(cl, sd.getValue()); + + assertTrue(sd.getDataRef().isEmbedded()); + } + + @SuppressWarnings("unchecked") + @Test + public void testSignatureCache() { + CVMLong cl=RT.cvm(1585856457); + AKeyPair kp = InitTest.HERO_KEYPAIR; + SignedData sd = kp.signData(cl); + ACell.createPersisted(sd); + + SignedData sd1 = (SignedData) Ref.forHash(sd.getHash()).getValue(); + // should have cached checked signature + assertTrue(sd1.isSignatureChecked()); + assertTrue(sd1.checkSignature()); + } + + @Test + public void testNullValueSignings() throws BadSignatureException { + SignedData sd = SignedData.create(InitTest.HERO_KEYPAIR, null); + assertNull(sd.getValue()); + assertTrue(sd.checkSignature()); + } + + @Test + public void testDataStructureSignature() throws BadSignatureException { + AKeyPair kp = InitTest.HERO_KEYPAIR; + AVector v = Vectors.of(1L, 2L, 3L); + SignedData> sd = kp.signData(v); + + assertEquals(1,sd.getRefCount()); + + assertTrue(sd.checkSignature()); + + sd.validateSignature(); + assertEquals(v, sd.getValue()); + + assertEquals(kp.getAccountKey(),sd.getAccountKey()); + + ObjectsTest.doAnyValueTests(sd); + } +} diff --git a/convex-core/src/test/java/convex/core/data/StreamsTest.java b/convex-core/src/test/java/convex/core/data/StreamsTest.java new file mode 100644 index 000000000..e6d074490 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/StreamsTest.java @@ -0,0 +1,33 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.InvalidDataException; +import convex.core.exceptions.ValidationException; +import convex.test.Samples; + +public class StreamsTest { + + @Test + public void testIntStream() throws InvalidDataException, ValidationException { + AVector v = Samples.INT_VECTOR_300; + Stream s = v.stream(); + + List list = s.map(i -> i).collect(Collectors.toList()); + assertEquals(v.size(), list.size()); + + AVector v2 = Vectors.create(list); + v2.validate(); + assertEquals(v.getClass(), v2.getClass()); + assertEquals(v, v2); + + } + +} diff --git a/convex-core/src/test/java/convex/core/data/StringsTest.java b/convex-core/src/test/java/convex/core/data/StringsTest.java new file mode 100644 index 000000000..12951b4c6 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/StringsTest.java @@ -0,0 +1,31 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class StringsTest { + + + @Test public void testStringShort() { + String t="Test"; + StringShort ss=StringShort.create(t); + + assertEquals(t.length(),ss.toString().length()); + assertEquals(t,ss.toString()); + } + + @Test public void testTreeShift() { + assertEquals(10,StringTree.calcShift(1025)); + assertEquals(10,StringTree.calcShift(4096)); + assertEquals(10,StringTree.calcShift(16384)); + assertEquals(14,StringTree.calcShift(16385)); + assertEquals(14,StringTree.calcShift(99999)); + assertEquals(14,StringTree.calcShift(262144)); + assertEquals(18,StringTree.calcShift(262145)); + + assertThrows(IllegalArgumentException.class,()->StringTree.calcShift(0)); + assertThrows(IllegalArgumentException.class,()->StringTree.calcShift(1024)); + } +} diff --git a/convex-core/src/test/java/convex/core/data/SymbolTest.java b/convex-core/src/test/java/convex/core/data/SymbolTest.java new file mode 100644 index 000000000..6afb655de --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/SymbolTest.java @@ -0,0 +1,55 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import convex.core.Constants; +import convex.core.exceptions.BadFormatException; +import convex.core.lang.Symbols; +import convex.core.util.Text; + +public class SymbolTest { + + @Test + public void testBadSymbols() { + assertNotNull(Symbol.create(Text.whiteSpace(Constants.MAX_NAME_LENGTH))); + + assertNull(Symbol.create(Text.whiteSpace(Constants.MAX_NAME_LENGTH+1))); + assertNull(Symbol.create("")); + assertNull(Symbol.create((String)null)); + } + + @Test + public void testEmbedded() { + // max length Symbol should be embedded + Symbol s=Symbol.create(Text.whiteSpace(Constants.MAX_NAME_LENGTH)); + assertTrue(s.isEmbedded()); + } + + @Test + public void testToString() { + assertEquals("foo",Symbols.FOO.toString()); + } + + @Test + public void testBadFormat() { + // should fail because this is an empty String + assertThrows(BadFormatException.class, () -> Symbol.read(Blob.fromHex("00").toByteBuffer())); + } + + @Test + public void testNormalSymbol() { + Symbol k = Symbol.create("count"); + assertEquals(Symbols.COUNT, k); + + assertEquals("count", k.getName().toString()); + assertEquals("count", k.toString()); + assertEquals(7, k.getEncoding().length); // tag(1) + length(1) + name(5) + + } +} diff --git a/convex-core/src/test/java/convex/core/data/SyntaxTest.java b/convex-core/src/test/java/convex/core/data/SyntaxTest.java new file mode 100644 index 000000000..4bd27c0d1 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/SyntaxTest.java @@ -0,0 +1,59 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.RT; + +public class SyntaxTest { + + @Test public void testSyntaxEncodingEmptyMetaData() throws BadFormatException { + // check that empty metadata gets encoded as nil for efficiency. + Syntax emptyMeta=Syntax.create(RT.cvm(1L)); + Blob encoded = emptyMeta.getEncoding(); + assertEquals("88090100",encoded.toHexString()); + Syntax recovered = Format.read(encoded); + assertEquals(emptyMeta,recovered); + + // should be invalid to have an empty map as encoded metadata + assertThrows(BadFormatException.class,()->Format.read("880901820000")); + assertThrows(BadFormatException.class,()->Format.read("8800820000")); + } + + /** + * A Syntax wrapped in another Syntax should not be a valid encoding + * @throws BadFormatException + */ + @Test public void testNoDoubleWrapping() throws BadFormatException { + // A valid Syntax Object + Syntax inner=Syntax.create(null); + Syntax badSyntax=Syntax.createUnchecked(inner, Maps.empty()); + assertEquals(inner,badSyntax.getValue()); + assertSame(Maps.empty(),badSyntax.getMeta()); + + // Should fail validation + assertThrows(InvalidDataException.class,()->badSyntax.validate()); + } + + @Test public void testSyntaxMergingMetaData() { + // default to empty metadata + Syntax s1=Syntax.create(RT.cvm(1L)); + assertSame(Maps.empty(),s1.getMeta()); + + // Should wrap once only and merge metadata + Syntax s2=Syntax.create(s1,Maps.of(1,2,3,4)); + assertEquals(RT.cvm(1L),(CVMLong)s2.getValue()); + + // Should wrap once only and merge new metadata, overwriting original value + Syntax s3=Syntax.create(s2,Maps.of(3,7)); + assertEquals(RT.cvm(1L),(CVMLong)s3.getValue()); + assertEquals(Maps.of(1,2,3,7),s3.getMeta()); + + } +} diff --git a/convex-core/src/test/java/convex/core/data/TreeVectorTest.java b/convex-core/src/test/java/convex/core/data/TreeVectorTest.java new file mode 100644 index 000000000..1132a5793 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/TreeVectorTest.java @@ -0,0 +1,81 @@ +package convex.core.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.NoSuchElementException; +import java.util.Spliterator; + +import org.junit.jupiter.api.Test; + +import convex.core.data.prim.CVMLong; +import convex.core.lang.RT; +import convex.test.Samples; + +public class TreeVectorTest { + + @Test + public void testComputeShift() { + assertEquals(0, VectorTree.computeShift(0)); + assertEquals(0, VectorTree.computeShift(1)); + assertEquals(0, VectorTree.computeShift(16)); + + assertEquals(4, VectorTree.computeShift(17)); // overflow to next shift level + assertEquals(4, VectorTree.computeShift(32)); + assertEquals(4, VectorTree.computeShift(107)); + assertEquals(4, VectorTree.computeShift(256)); + + assertEquals(8, VectorTree.computeShift(257)); // overflow to next shift level + + assertEquals(8, VectorTree.computeShift(4096)); + assertEquals(12, VectorTree.computeShift(4097)); // overflow to next shift level + } + + @Test + public void testArraySize() { + assertEquals(2, VectorTree.computeArraySize(32)); // two tree chunks + assertEquals(3, VectorTree.computeArraySize(33)); // needs 3 chunks + assertEquals(16, VectorTree.computeArraySize(256)); // full 16 chunks + assertEquals(2, VectorTree.computeArraySize(257)); // needs 17 chunks, two children at next level + + assertEquals(16, VectorTree.computeArraySize(4096)); // full 16 children + assertEquals(2, VectorTree.computeArraySize(4097)); // two children at next level + + assertTrue(16 >= VectorTree.computeArraySize(967827895416073414L)); + } + + @Test + public void testIterator() { + AVector v = Samples.INT_VECTOR_256; + assertEquals(v.get(0), v.iterator().next()); + assertEquals(v.get(255), v.listIterator(256).previous()); + + assertThrows(NoSuchElementException.class, () -> v.listIterator().previous()); + assertThrows(NoSuchElementException.class, () -> v.listIterator(256).next()); + } + + @Test + public void testMap() { + AVector orig = Samples.INT_VECTOR_300; + AVector inc = orig.map(i -> RT.cvm(i.longValue() + 5)); + assertEquals(orig.count(), inc.count()); + assertNotEquals(orig, inc); + AVector dec = inc.map(i -> RT.cvm(i.longValue() - 5)); + assertEquals(orig, dec); + } + + @Test + public void testSpliterator() { + AVector a = Samples.INT_VECTOR_300.subVector(0, 256); + assertEquals(VectorTree.class, a.getClass()); + Spliterator spliterator = a.spliterator(); + assertEquals(256, spliterator.estimateSize()); + + long[] sum = new long[1]; + spliterator.forEachRemaining(i -> sum[0] += i.longValue()); + assertEquals((255 * 256) / 2, sum[0]); + + } +} diff --git a/convex-core/src/test/java/convex/core/data/VectorsTest.java b/convex-core/src/test/java/convex/core/data/VectorsTest.java new file mode 100644 index 000000000..e58792857 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/VectorsTest.java @@ -0,0 +1,337 @@ +package convex.core.data; + +import static convex.test.Assertions.assertCVMEquals; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ListIterator; +import java.util.Spliterator; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.jupiter.api.Test; + +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.lang.RT; +import convex.test.Samples; + +/** + * Example based tests for vectors. + * + * Also doVectorTests(...) implements generic tests for any vector. + */ +public class VectorsTest { + + @Test + public void testEmptyVector() { + AVector lv = Vectors.empty(); + AArrayBlob d = lv.getEncoding(); + assertArrayEquals(new byte[] { Tag.VECTOR, 0 }, d.getBytes()); + + assertSame(lv,Vectors.empty()); + } + + @Test + public void testSubVectors() { + AVector v = Samples.INT_VECTOR_300; + + AVector v1 = v.subVector(10, Vectors.CHUNK_SIZE); + assertEquals(VectorLeaf.class, v1.getClass()); + assertEquals(RT.cvm(10), v1.get(0)); + + AVector v2 = v.subVector(10, Vectors.CHUNK_SIZE * 2); + assertEquals(VectorTree.class, v2.getClass()); + assertEquals(RT.cvm(10), v2.get(0)); + + AVector v3 = v.subVector(10, Vectors.CHUNK_SIZE * 2 - 1); + assertEquals(VectorLeaf.class, v3.getClass()); + assertEquals(RT.cvm(10), v3.get(0)); + + AVector v4 = v3.conj(RT.cvm(1000L)); + assertEquals(VectorTree.class, v4.getClass()); + assertEquals(RT.cvm(26), v4.get(16)); + assertEquals(v1, v4.subVector(0, Vectors.CHUNK_SIZE)); + } + + @Test + public void testCreateSpecialCases() { + assertSame(Vectors.empty(), VectorLeaf.create(new ACell[0])); + assertSame(Vectors.empty(), VectorLeaf.create(new ACell[10], 3, 0)); + } + + @Test + public void testChunks() { + assertEquals(Samples.INT_VECTOR_16, Samples.INT_VECTOR_300.getChunk(0)); + AVector v = Samples.INT_VECTOR_300.getChunk(0); + assertEquals(VectorTree.class, v.getChunk(0).concat(v).getClass()); + } + + @Test + public void testChunkConcat() { + VectorLeaf v = Samples.INT_VECTOR_300.getChunk(16); + AVector vv = v.concat(v); + assertEquals(VectorTree.class, vv.getClass()); + assertEquals(v, vv.getChunk(16)); + + assertSame(Samples.INT_VECTOR_16, Samples.INT_VECTOR_16.empty().appendChunk(Samples.INT_VECTOR_16)); + + assertThrows(IndexOutOfBoundsException.class, () -> vv.getChunk(3)); + + // can't append chunk unless initial size is correct + assertThrows(IllegalArgumentException.class, () -> Samples.INT_VECTOR_10.appendChunk(Samples.INT_VECTOR_16)); + assertThrows(IllegalArgumentException.class, () -> Samples.INT_VECTOR_300.appendChunk(Samples.INT_VECTOR_16)); + + + VectorLeaf tooSmall=VectorLeaf.create(new CVMLong[] {CVMLong.create(1),CVMLong.create(2)}); + // can't append wrong chunk size + assertThrows(IllegalArgumentException.class, + () -> Samples.INT_VECTOR_16.appendChunk(tooSmall)); + + } + + @Test + public void testIndexOf() { + AVector v = Samples.INT_VECTOR_300; + CVMLong last=v.get(v.count()-1); + assertEquals(299, v.indexOf(last)); + assertEquals(299L, v.longIndexOf(last)); + assertEquals(299, v.lastIndexOf(last)); + assertEquals(299L, v.longLastIndexOf(last)); + + CVMLong mid=v.get(29); + assertEquals(29, v.indexOf(mid)); + assertEquals(29L, v.longIndexOf(mid)); + assertEquals(29, v.lastIndexOf(mid)); + assertEquals(29L, v.longLastIndexOf(mid)); + + } + + @Test + public void testAppending() { + int SIZE = 300; + @SuppressWarnings("unchecked") + AVector lv = (VectorLeaf) VectorLeaf.EMPTY; + + for (int i = 0; i < SIZE; i++) { + CVMLong ci=RT.cvm(i); + lv = lv.append(ci); + assertEquals(i + 1L, lv.count()); + assertEquals(ci, lv.get(i)); + } + assertEquals(300L, lv.count()); + } + + @Test + public void testBigMatch() { + AVector v = Samples.INT_VECTOR_300; + assertTrue(v.anyMatch(i -> i.longValue() == 3)); + assertTrue(v.anyMatch(i -> i.longValue() == 299)); + assertFalse(v.anyMatch(i -> i.longValue() == -1)); + + assertFalse(v.allMatch(i -> i.longValue() == 3)); + assertTrue(v.allMatch(i -> i.longValue() >=0)); + } + + @Test + public void testAnyMatch() { + AVector v = Vectors.of(1, 2, 3, 4); + assertTrue(v.anyMatch(i -> i.longValue() == 3)); + assertFalse(v.anyMatch(i -> i.longValue() == 5)); + } + + @Test + public void testAllMatch() { + AVector v = Vectors.of(1, 2, 3, 4); + assertTrue(v.allMatch(i -> i instanceof CVMLong)); + assertFalse(v.allMatch(i -> i.longValue() < 3)); + } + + @Test + public void testMap() { + AVector v = Vectors.of(1, 2, 3, 4); + AVector exp = Vectors.of(2, 3, 4, 5); + assertEquals(exp, v.map(i -> CVMLong.create(i.longValue() + 1))); + } + + @Test + public void testSmallAssoc() { + AVector v = Vectors.of(1, 2, 3, 4); + AVector nv = v.assoc(2, RT.cvm(10L)); + assertEquals(Vectors.of(1, 2, 10, 4), nv); + } + + @Test + public void testBigAssoc() { + AVector v = Samples.INT_VECTOR_300; + AVector nv = v.assoc(100, RT.cvm(17L)); + assertEquals(RT.cvm(17L), nv.get(100)); + } + + @Test + public void testReduce() { + AVector vec = Vectors.of(1, 2, 3, 4); + assertEquals(110, (long) vec.reduce((s, v) -> s + v.longValue(), 100L)); + } + + @Test + public void testMapEntry() { + AVector v1 = Vectors.of(1L, 2L); + + MapEntry me=MapEntry.of(1L,2L); + assertEquals(v1, me); + assertEquals(v1, me.toVector()); + + assertEquals(me,me.toVector()); + assertFalse(me.isCanonical()); + + doVectorTests(me); + } + + @Test + public void testLastIndex() { + // regression test + AVector v = Samples.INT_VECTOR_300.concat(Vectors.of(1, null, 3, null)); + assertEquals(303L, v.longLastIndexOf(null)); + } + + @Test + public void testReduceBig() { + AVector vec = Samples.INT_VECTOR_300; + assertEquals(100 + (299 * 300) / 2, vec.reduce((s, v) -> s + v.longValue(), 100L)); + } + + // TODO: more sensible tests on embedded vector sizes + @Test + public void testEmbedding() { + // should embed, little values + AVector vec = Vectors.of(1, 2, 3, 4); + assertTrue(vec.isEmbedded()); + assertEquals(10L,vec.getEncoding().count()); + + // should embed, small enough + AVector vec2=Vectors.of(vec,vec); + assertTrue(vec2.isEmbedded()); + assertEquals(22L,vec2.getEncoding().count()); + + AVector vec3=Vectors.of(vec2,vec2,vec2,vec2,vec2,vec2,vec2,vec2); + assertFalse(vec3.isEmbedded()); + } + + @Test + public void testUpdateRefs() { + AVector vec = Vectors.of(1, 2, 3, 4); + AVector vec2 = vec.updateRefs(r -> CVMLong.create(1L+((CVMLong)r.getValue()).longValue()).getRef()); + assertEquals(Vectors.of(2, 3, 4, 5), vec2); + } + + @Test + public void testNext() { + AVector v1 = Samples.INT_VECTOR_256; + AVector v2 = v1.next(); + assertEquals(v1.get(1), v2.get(0)); + assertEquals(v1.get(255), v2.get(254)); + assertEquals(1L, v1.count() - v2.count()); + } + + @Test + public void testIterator() { + int SIZE = 100; + @SuppressWarnings("unchecked") + AVector lv = (VectorLeaf) VectorLeaf.EMPTY; + + for (int i = 0; i < SIZE; i++) { + lv = lv.append(RT.cvm(i)); + assertTrue(lv.isCanonical()); + } + assertEquals(4950L, lv.reduce((acc, v) -> acc + v.longValue(), 0L)); + + // forward iteration + ListIterator it = lv.listIterator(); + Spliterator split = lv.spliterator(); + AtomicLong splitAcc = new AtomicLong(0); + for (int i = 0; i < SIZE; i++) { + assertTrue(it.hasNext()); + assertTrue(split.tryAdvance(a -> splitAcc.addAndGet(a.longValue()))); + assertEquals(i, it.nextIndex()); + assertEquals(RT.cvm(i), it.next()); + } + assertEquals(100, it.nextIndex()); + assertEquals(4950, splitAcc.get()); + assertFalse(it.hasNext()); + + // backward iteration + ListIterator li = lv.listIterator(SIZE); + for (int i = SIZE - 1; i >= 0; i--) { + assertTrue(li.hasPrevious()); + assertEquals(i, li.previousIndex()); + assertCVMEquals(i, li.previous()); + } + assertEquals(-1, li.previousIndex()); + assertFalse(li.hasPrevious()); + } + + @Test + public void testEmptyVectorHash() { + AVector e = Vectors.empty(); + + // test the byte layout of the empty vector + assertEquals(e.getEncoding(), Blob.fromHex("8000")); + assertEquals(e.getHash(), Vectors.of((Object[])new VectorLeaf[0]).getHash()); + } + + @Test + public void testSmallVectorSerialisation() { + // test the byte layout of the vector + // value should be an int VLC encoded to two bytes (0x0701) + assertEquals(Blob.fromHex("80010901"), Vectors.of(1).getEncoding()); + + // value should be a negative int VLC encoded to two bytes (0x077F) + assertEquals(Blob.fromHex("8001097F"), Vectors.of(-1).getEncoding()); + } + + @Test + public void testPrefixLength() throws BadFormatException { + assertEquals(2, Vectors.of(1, 2, 3).commonPrefixLength(Vectors.of(1, 2))); + assertEquals(2, Vectors.of(1, 2).commonPrefixLength(Vectors.of(1, 2, 8))); + assertEquals(0, Vectors.of(1, 2, 3).commonPrefixLength(Vectors.of(2, 2, 3))); + + AVector v1 = Vectors.of(0, 1, 2, 3, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1); + assertEquals(5, v1.commonPrefixLength(Samples.INT_VECTOR_300)); + assertEquals(5, Samples.INT_VECTOR_300.commonPrefixLength(v1)); + assertEquals(v1.count(), v1.commonPrefixLength(v1)); + + assertEquals(10, Samples.INT_VECTOR_10.commonPrefixLength(Samples.INT_VECTOR_23)); + assertEquals(256, Samples.INT_VECTOR_300.commonPrefixLength(Samples.INT_VECTOR_256)); + assertEquals(256, Samples.INT_VECTOR_300.commonPrefixLength(Samples.INT_VECTOR_256.append(RT.cvm(17L)))); + } + + /** + * Generic tests for any vector + * @param v Any Vector + */ + public static void doVectorTests(AVector v) { + long n = v.count(); + + if (n == 0) { + assertSame(Vectors.empty(), v); + } else { + T last = v.get(n - 1); + T first = v.get(0); + assertEquals(n - 1, v.longLastIndexOf(last)); + assertEquals(0L, v.longIndexOf(first)); + + AVector v2 = v.append(first); + assertEquals(first, v2.get(n)); + } + + assertEquals(v.toVector(), Vectors.of(v.toArray())); + + CollectionsTest.doSequenceTests(v); + } + +} diff --git a/convex-core/src/test/java/convex/core/data/prim/ByteTest.java b/convex-core/src/test/java/convex/core/data/prim/ByteTest.java new file mode 100644 index 000000000..2340ea045 --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/prim/ByteTest.java @@ -0,0 +1,39 @@ +package convex.core.data.prim; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.junit.jupiter.api.Test; + +import convex.core.data.Hash; +import convex.core.data.ObjectsTest; +import convex.core.lang.RT; + +public class ByteTest { + + @Test public void testCache() { + assertSame(CVMByte.create(1),CVMByte.create(1)); + } + + @Test public void testValues() { + for (int i=0; i<256; i++) { + CVMByte b=CVMByte.create(i); + assertSame(b,CVMByte.create((byte)i)); + + Hash h=b.getHash(); + assertNotNull(h); + + // check hash is cached correctly + assertSame(h,b.getHash()); + + ObjectsTest.doAnyValueTests(b); + } + + } + + @Test public void testCVMCast() { + // CVM converts all numbers to Long + assertEquals(RT.cvm(1L),(CVMLong)RT.cvm((byte)1)); + } +} diff --git a/convex-core/src/test/java/convex/core/data/prim/LongTest.java b/convex-core/src/test/java/convex/core/data/prim/LongTest.java new file mode 100644 index 000000000..8aa2cc70d --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/prim/LongTest.java @@ -0,0 +1,14 @@ +package convex.core.data.prim; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class LongTest { + + @Test + public void testEquality() { + long v=666666; + assertEquals(CVMLong.create(v),CVMLong.create(v)); + } +} diff --git a/convex-core/src/test/java/convex/core/data/type/TypesTest.java b/convex-core/src/test/java/convex/core/data/type/TypesTest.java new file mode 100644 index 000000000..0a36e9e6f --- /dev/null +++ b/convex-core/src/test/java/convex/core/data/type/TypesTest.java @@ -0,0 +1,183 @@ +package convex.core.data.type; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.data.ObjectsTest; +import convex.core.data.prim.CVMByte; +import convex.core.data.prim.CVMDouble; +import convex.core.data.prim.CVMLong; +import convex.core.lang.RT; +import convex.core.util.Utils; +import convex.test.Samples; +import convex.test.Samples.ValueArgumentsProvider; + +public class TypesTest { + + + + @Test + public void testNil() { + AType t=Types.NIL; + assertTrue(t.check(null)); + assertFalse(t.check(CVMLong.ONE)); + } + + @Test + public void testLong() { + AType t=Types.LONG; + assertFalse(t.check(null)); + assertTrue(t.check(CVMLong.ONE)); + assertFalse(t.check(CVMDouble.ONE)); + } + + @Test + public void testAddress() { + AType t=Types.ADDRESS; + assertFalse(t.check(null)); + assertFalse(t.check(convex.core.data.Blob.EMPTY)); + assertTrue(t.check(Address.ZERO)); + } + + @Test + public void testAny() { + AType t=Types.ANY; + assertTrue(t.check(null)); + assertTrue(t.check(CVMLong.ONE)); + assertTrue(t.check(CVMDouble.ONE)); + } + + @Test + public void testCollection() { + AType t=Types.COLLECTION; + assertFalse(t.check(null)); + assertFalse(t.check(CVMLong.ONE)); + assertFalse(t.check(CVMDouble.ONE)); + assertTrue(t.check(Samples.LONG_SET_10)); + assertTrue(t.check(Samples.INT_VECTOR_300)); + assertTrue(t.check(Samples.INT_LIST_10)); + } + + @Test + public void testVector() { + AType t=Types.VECTOR; + assertFalse(t.check(null)); + assertFalse(t.check(CVMLong.ONE)); + assertFalse(t.check(CVMDouble.ONE)); + assertFalse(t.check(Samples.LONG_SET_10)); + assertTrue(t.check(Samples.INT_VECTOR_300)); + assertFalse(t.check(Samples.INT_LIST_10)); + } + + @Test + public void testSet() { + AType t=Types.SET; + assertFalse(t.check(null)); + assertFalse(t.check(CVMLong.ONE)); + assertTrue(t.check(Samples.LONG_SET_100)); + assertFalse(t.check(Samples.INT_VECTOR_300)); + } + + @Test + public void testList() { + AType t=Types.LIST; + assertFalse(t.check(null)); + assertFalse(t.check(CVMDouble.ONE)); + assertFalse(t.check(Samples.INT_VECTOR_300)); + assertTrue(t.check(Samples.INT_LIST_10)); + } + + @Test + public void testNumber() { + AType t=Types.NUMBER; + assertFalse(t.check(null)); + assertTrue(t.check(CVMLong.ONE)); + assertTrue(t.check(CVMByte.ONE)); + assertTrue(t.check(CVMDouble.ONE)); + } + + @ParameterizedTest + @ArgumentsSource(ValueArgumentsProvider.class) + public void testSampleValues(ACell a) { + AType t=RT.getType(a); + assertTrue(t.check(a)); + assertSame(a,t.implicitCast(a),"Implicit cast to same runtime type should not change a value"); + + assertNotSame(t,Types.ANY,"Runtime type of a value should not be Any"); + + Class klass=t.getJavaClass(); + assertTrue((a==null)||klass.isInstance(a)); + } + + @Test + public void testTypeNames() { + HashMap names=new HashMap<>(); + Stream.of(Types.ALL_TYPES).forEach(t -> { + String name=t.toString(); + assertFalse(names.containsKey(name),"Name clash "+Utils.getClassName(t)+" has same name ("+name+" ) as type "+Utils.getClassName(names.get(name))); + names.put(name, t); + }); + } + + @Test + public void testTypeCoverage() { + HashSet types=new HashSet<>(); + Stream.of(Types.ALL_TYPES).forEach(t -> { + assertFalse(types.contains(t),"Duplicate type: "+t); + types.add(t); + }); + + Stream.of(Samples.VALUES).forEach(v -> { + AType t=RT.getType(v); + types.remove(t); + }); + + // TODO: differentiate between concrete types and superclasses + // assertTrue(types.isEmpty(),"Types not covered with test values: "+types); + } + + @ParameterizedTest + @ArgumentsSource(TypeArgumentsProvider.class) + public void testAllTypes(AType t) { + ACell a=t.defaultValue(); + + assertTrue(t.check(a)); + assertSame(a,t.implicitCast(a)); + + if (t.allowsNull()) { + assertTrue(t.check(null)); + } else { + assertFalse(t.check(null)); + } + + Class klass=t.getJavaClass(); + assertNotNull(klass); + assertTrue((a==null)||klass.isInstance(a)); + + ObjectsTest.doAnyValueTests(a); + } + + + public static class TypeArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(Types.ALL_TYPES).map(t -> Arguments.of(t)); + } + } +} diff --git a/convex-core/src/test/java/convex/core/init/InitTest.java b/convex-core/src/test/java/convex/core/init/InitTest.java new file mode 100644 index 000000000..db9b025dd --- /dev/null +++ b/convex-core/src/test/java/convex/core/init/InitTest.java @@ -0,0 +1,124 @@ +package convex.core.init; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import convex.core.Constants; +import convex.core.State; +import convex.core.crypto.AKeyPair; +import convex.core.data.AccountKey; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.core.exceptions.InvalidDataException; +import convex.core.lang.ACVMTest; + +/** + * Tests for Init functionality + * + * Also includes static State instances for Testing + */ +public class InitTest extends ACVMTest { + + public static final AKeyPair[] KEYPAIRS = new AKeyPair[] { + AKeyPair.createSeeded(2), + AKeyPair.createSeeded(3), + AKeyPair.createSeeded(5), + AKeyPair.createSeeded(7), + AKeyPair.createSeeded(11), + AKeyPair.createSeeded(13), + AKeyPair.createSeeded(17), + AKeyPair.createSeeded(19), + }; + + public static ArrayList PEER_KEYPAIRS=(ArrayList) Arrays.asList(KEYPAIRS).stream().collect(Collectors.toList()); + public static ArrayList PEER_KEYS=(ArrayList) Arrays.asList(KEYPAIRS).stream().map(kp->kp.getAccountKey()).collect(Collectors.toList()); + + public static final AKeyPair FIRST_PEER_KEYPAIR = KEYPAIRS[0]; + public static final AccountKey FIRST_PEER_KEY = FIRST_PEER_KEYPAIR.getAccountKey(); + + public static final AKeyPair HERO_KEYPAIR = KEYPAIRS[0]; + public static final AKeyPair VILLAIN_KEYPAIR = KEYPAIRS[1]; + + public static final AccountKey HERO_KEY = HERO_KEYPAIR.getAccountKey(); + + + /** + * Standard Genesis state used for testing + */ + public static final State STATE= createState(); + public static final State BASE = Init.createBaseState(PEER_KEYS); + + + public static State createState() { + return Init.createState(PEER_KEYS); + } + + public static Address HERO=Init.getGenesisAddress(); + public static Address VILLAIN=Init.getGenesisPeerAddress(1); + + public static final Address FIRST_PEER_ADDRESS = Init.getGenesisPeerAddress(0); + + + protected InitTest() { + super(STATE); + } + + @Test + public void testDeploy() { + assertTrue(evalA("(call *registry* (cns-resolve 'asset.box))")); + assertTrue(evalA("(call *registry* (cns-resolve 'asset.box.actor))")); + assertTrue(evalA("(call *registry* (cns-resolve 'asset.nft.simple))")); + assertTrue(evalA("(call *registry* (cns-resolve 'asset.nft.tokens))")); + assertTrue(evalA("(call *registry* (cns-resolve 'convex.asset))")); + assertTrue(evalA("(call *registry* (cns-resolve 'convex.fungible))")); + assertTrue(evalA("(call *registry* (cns-resolve 'convex.play))")); + assertTrue(evalA("(call *registry* (cns-resolve 'convex.trusted-oracle.actor))")); + assertTrue(evalA("(call *registry* (cns-resolve 'convex.trusted-oracle))")); + assertTrue(evalA("(call *registry* (cns-resolve 'torus.exchange))")); + + assertEquals(Init.CORE_ADDRESS, eval("(call *registry* (cns-resolve 'convex.core))")); + assertEquals(Init.REGISTRY_ADDRESS, eval("(call *registry* (cns-resolve 'convex.registry))")); + assertEquals(Init.TRUST_ADDRESS, eval("(call *registry* (cns-resolve 'convex.trust))")); + } + + @Test + public void testInitState() throws InvalidDataException { + STATE.validate(); + assertEquals(0,context().getDepth()); + assertNull(context().getResult()); + + assertEquals(Constants.MAX_SUPPLY, STATE.computeTotalFunds()); + } + + @Test + public void testMemoryExchange() { + AccountStatus as = STATE.getAccount(Init.MEMORY_EXCHANGE_ADDRESS); + assertNotNull(as); + assertTrue(as.getMemory() > 0L); + } + + @Test + public void testHero() { + AccountStatus as=STATE.getAccount(HERO); + assertNotNull(as); + assertEquals(Constants.INITIAL_ACCOUNT_ALLOWANCE,as.getMemory()); + } + + @Test + public void testVILLAIN() { + AccountStatus as=STATE.getAccount(VILLAIN); + assertNotNull(as); + assertEquals(Constants.INITIAL_ACCOUNT_ALLOWANCE,as.getMemory()); + assertNotEquals(HERO,VILLAIN); + } + +} diff --git a/convex-core/src/test/java/convex/core/lang/ACVMTest.java b/convex-core/src/test/java/convex/core/lang/ACVMTest.java new file mode 100644 index 000000000..afd42ec1d --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/ACVMTest.java @@ -0,0 +1,240 @@ +package convex.core.lang; + +import convex.core.State; +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.data.prim.CVMBool; +import convex.core.data.prim.CVMDouble; +import convex.core.data.prim.CVMLong; +import convex.core.init.Init; +import convex.core.init.InitTest; +import convex.core.util.Utils; + +/** + * Base class for CVM tests that work from a given initial state and context. + * + * Provides utility functions for CVM code execution. + */ +public abstract class ACVMTest { + + protected State INITIAL; + private Context CONTEXT; + protected long INITIAL_JUICE; + + /** + * Address of the HERO, equal to the genesis address + */ + protected final Address HERO; + + /** + * Address of the villain: has compromised peer at index 1 (i.e. the second peer) + */ + protected final Address VILLAIN; + + /** + * Balance of hero's account before spending any juice / funds + */ + public final long HERO_BALANCE; + + /** + * Balance of villain's account before spending any juice / funds + */ + public final long VILLAIN_BALANCE; + + /** + * Constructor using a specified Genesis State + * @param genesis Genesis State to use for this CVM test + */ + protected ACVMTest(State genesis) { + this.INITIAL=genesis; + CONTEXT=Context.createFake(genesis,Init.GENESIS_ADDRESS); + HERO=InitTest.HERO; + VILLAIN=InitTest.VILLAIN; + INITIAL_JUICE=CONTEXT.getJuice(); + HERO_BALANCE = INITIAL.getAccount(InitTest.HERO).getBalance(); + VILLAIN_BALANCE = INITIAL.getAccount(InitTest.VILLAIN).getBalance(); + } + + /** + * Default Constructor uses standard testing Genesis State + */ + protected ACVMTest() { + this(InitTest.STATE); + } + + @SuppressWarnings("unchecked") + protected Context context() { + return (Context) CONTEXT.fork(); + } + + /** + * Steps execution in a new forked Context + * @param Type of result + * @param ctx Initial context to fork + * @param source Source form to read + * @return New forked context containing step result + */ + public Context step(Context ctx, String source) { + ACell form = Reader.read(source); + return step(ctx,form); + } + + /** + * Steps execution in a new forked Context + * @param Type of result + * @param ctx Initial context to fork + * @param form Form to compile and execute execute + * @return New forked context containing step result + */ + @SuppressWarnings("unchecked") + public Context step(Context ctx, ACell form) { + // Run form in separate forked context to get result context + Context rctx = ctx.fork(); + rctx=(Context) rctx.run(form); + assert(rctx.getDepth()==0):"Invalid depth after step: "+rctx.getDepth(); + return rctx; + } + + + @SuppressWarnings("unchecked") + public AOp compile(Context c, String source) { + c=c.fork(); + try { + ACell form = Reader.read(source); + AOp op = (AOp) c.expandCompile(form).getResult(); + return op; + } catch (Exception e) { + throw Utils.sneakyThrow(e); + } + } + + + + public T read(String source) { + return Reader.read(source); + } + + /** + * Runs an execution step as a different address. Returns value after restoring + * the original address. + * @param address Address to run as + * @param c Initial Context. Will not be modified. + * @param source Source form + * @return Updates Context + */ + @SuppressWarnings("unchecked") + public Context stepAs(Address address, Context c, String source) { + Context rc = Context.createFake(c.getState(), address); + rc = step(rc, source); + return (Context) Context.createFake(rc.getState(), c.getAddress()).withValue(rc.getValue()); + } + + public boolean evalA(String source) { + return evalA(CONTEXT, source); + } + + public boolean evalA(Context ctx, String source) { + return eval(ctx, source) instanceof Address; + + } + + public boolean evalB(String source) { + return ((CVMBool)eval(source)).booleanValue(); + } + + public boolean evalB(Context ctx, String source) { + return ((CVMBool)eval(ctx, source)).booleanValue(); + } + + public double evalD(Context ctx, String source) { + ACell result=eval(ctx,source); + CVMDouble d=RT.castDouble(result); + if (d==null) throw new ClassCastException("Expected Double, but got: "+RT.getType(result)); + return d.doubleValue(); + } + + public double evalD(String source) { + return evalD(CONTEXT,source); + } + + public long evalL(Context ctx, String source) { + ACell result=eval(ctx,source); + CVMLong d=RT.castLong(result); + if (d==null) throw new ClassCastException("Expected Long, but got: "+RT.getType(result)); + return d.longValue(); + } + + public long evalL(String source) { + return evalL(CONTEXT,source); + } + + public String evalS(String source) { + return eval(source).toString(); + } + + @SuppressWarnings("unchecked") + public T eval(String source) { + return (T) step(source).getResult(); + } + + @SuppressWarnings("unchecked") + public T eval(ACell form) { + return (T) step(CONTEXT,form).getResult(); + } + + public T eval(Context c, String source) { + Context rc = step(c,source); + return rc.getResult(); + } + + public Context step(String source) { + return step(CONTEXT, source); + } + + @SuppressWarnings("unchecked") + public > T comp(ACell form, Context context) { + context=context.fork(); // fork to avoid corrupting original context + AOp code = context.expandCompile(form).getResult(); + return (T) code; + } + + public > T comp(String source, Context context) { + return comp(Reader.read(source),context); + } + + /** + * Compiles source code to a CVM Op + * @param + * @param source + * @return CVM Op + */ + public > T comp(String source) { + return comp(Reader.read(source),CONTEXT); + } + + /** + * Compiles source code to a CVM Op + * @param + * @param code Source code to compile as a form + * @return CVM Op + */ + public > T comp(ACell code) { + return comp(code,CONTEXT); + } + + public ACell expand(ACell form) { + Context ctx=CONTEXT.fork(); + ACell expanded =ctx.expand(form).getResult(); + return expanded; + } + + public ACell expand(String source) { + try { + ACell form=Reader.read(source); + return expand(form); + } + catch (Exception e) { + throw Utils.sneakyThrow(e); + } + } +} diff --git a/convex-core/src/test/java/convex/core/lang/AliasTest.java b/convex-core/src/test/java/convex/core/lang/AliasTest.java new file mode 100644 index 000000000..381ddeaf4 --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/AliasTest.java @@ -0,0 +1,120 @@ +package convex.core.lang; + +import static convex.core.lang.TestState.eval; +import static convex.core.lang.TestState.evalL; +import static convex.core.lang.TestState.step; +import static convex.test.Assertions.assertArityError; +import static convex.test.Assertions.assertAssertError; +import static convex.test.Assertions.assertCastError; +import static convex.test.Assertions.assertStateError; +import static convex.test.Assertions.assertUndeclaredError; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +import convex.core.data.Address; + +public class AliasTest { + + @Test public void testLibraryAlias() { + Context ctx=step("(def lib (deploy '(do (def foo 100) (defn bar [] (inc foo)) (defn baz [f] (f foo)))))"); + Address libAddress=eval(ctx,"lib"); + assertNotNull(libAddress); + + // no alias should exist yet + assertUndeclaredError(step(ctx,"foo")); + assertUndeclaredError(step(ctx,"mylib/foo")); + + ctx=step(ctx,"(def mylib lib)"); + + // Alias should now work + assertEquals(100L,evalL(ctx,"mylib/foo")); + + // Use of function with access to library namespace should work + assertEquals(101L,evalL(ctx,"(mylib/bar)")); + assertEquals(101L,evalL(ctx,"(let [f mylib/bar] (f))")); + + // Shouldn't be able to call as an actor + assertStateError(step(ctx,"(call lib (bar))")); + + // should be able to pass a closure to the library + assertEquals(10000L, evalL(ctx,"(let [f (fn [x] (* x x))] (mylib/baz f))")); + assertEquals(99L, evalL(ctx,"(do (def f (fn [x] (dec x))) (mylib/baz f))")); + } + + @Test + public void testImport() { + Context ctx = step("(def lib (deploy '(def foo 100)))"); + Address libAddress = eval(ctx, "lib"); + assertNotNull(libAddress); + + // no alias should exist yet + assertUndeclaredError(step(ctx, "foo")); + assertUndeclaredError(step(ctx, "mylib/foo")); + + ctx = step(ctx, "(import ~lib :as mylib)"); + + // Alias should now work + assertEquals(100L, evalL(ctx, "mylib/foo")); + } + + @Test + public void testBadImports() { + Context ctx = step("(def lib (deploy `(def foo 100)))"); + Address lib = (Address) ctx.getResult(); + assertNotNull(lib); + + assertArityError(step(ctx,"(import)")); + assertArityError(step(ctx,"(import ~lib)")); + assertArityError(step(ctx,"(import ~lib :as)")); + assertArityError(step(ctx,"(import ~lib :as foo bar)")); + + // check for bad keyword + assertAssertError(step(ctx,"(import ~lib :blazzzz mylib)")); + + // can't have bad alias + assertAssertError(step(ctx,"(import ~lib :as nil)")); + + // can't have non-address first argument + assertCastError(step(ctx,"(import :foo :as mylib)")); + } + + @Test + public void testTransitiveImports() { + // create first library + Context ctx = step("(def lib1 (deploy '(do (def foo 101))))"); + Address lib1 = (Address) ctx.getResult(); + assertNotNull(lib1); + + ctx = step(ctx,"(def lib2 (deploy '(do (import 0x"+lib1.toHexString()+" :as lib1) (def foo (inc lib1/foo)))))"); + Address lib2 = (Address) ctx.getResult(); + assertNotNull(lib2); + + ctx=step(ctx,"(do (import 0x"+lib1.toHexString()+" :as mylib1) (import 0x"+lib2.toHexString()+" :as mylib2))"); + + assertEquals(101,evalL(ctx,"mylib1/foo")); + assertEquals(102,evalL(ctx,"mylib2/foo")); + assertUndeclaredError(step(ctx,"foo")); + assertUndeclaredError(step(ctx,"mylib1/baddy")); + } + + @Test + public void testLibraryAssumptions() { + Context ctx = step("(def lib (deploy '(defn run [code] (eval code))))"); + Address lib = (Address) ctx.getResult(); + ctx=step(ctx,"(do (import 0x"+lib.toHexString()+" :as lib))"); + + // context setup should not change + assertEquals(ctx.getAddress(),eval(ctx,"(lib/run '*address*)")); + assertEquals(ctx.getOrigin(),eval(ctx,"(lib/run '*origin*)")); + assertEquals(ctx.getCaller(),eval(ctx,"(lib/run '*caller*)")); + assertEquals(ctx.getState(),eval(ctx,"(lib/run '*state*)")); + + // library def should define values in the current user's environment. + assertEquals(1337L,evalL(ctx,"(do (lib/run '(def x 1337)) x)")); + + // shouldn't be possible by default to call on library code. Could be dangerous! + assertStateError(step(ctx,"(call lib (run 1))")); + } +} diff --git a/convex-core/src/test/java/convex/core/lang/CompilerTest.java b/convex-core/src/test/java/convex/core/lang/CompilerTest.java new file mode 100644 index 000000000..8d67998b3 --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/CompilerTest.java @@ -0,0 +1,595 @@ +package convex.core.lang; + +import static convex.test.Assertions.assertArityError; +import static convex.test.Assertions.assertBoundsError; +import static convex.test.Assertions.assertCVMEquals; +import static convex.test.Assertions.assertCastError; +import static convex.test.Assertions.assertCompileError; +import static convex.test.Assertions.assertDepthError; +import static convex.test.Assertions.assertJuiceError; +import static convex.test.Assertions.assertNotError; +import static convex.test.Assertions.assertUndeclaredError; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import convex.core.data.ACell; +import convex.core.data.AHashMap; +import convex.core.data.AList; +import convex.core.data.AVector; +import convex.core.data.Address; +import convex.core.data.Keywords; +import convex.core.data.Lists; +import convex.core.data.Maps; +import convex.core.data.Sets; +import convex.core.data.Strings; +import convex.core.data.Symbol; +import convex.core.data.Syntax; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMBool; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.ParseException; +import convex.core.init.InitTest; +import convex.core.lang.ops.Constant; +import convex.core.lang.ops.Def; +import convex.core.lang.ops.Do; +import convex.core.lang.ops.Invoke; +import convex.core.lang.ops.Lambda; +import convex.core.lang.ops.Local; +import convex.core.lang.ops.Lookup; +import convex.core.util.Utils; +import convex.test.Samples; + +/** + * Tests for basic language features and compiler functionality. + * + * State setup includes only basic accounts and core library. + */ +public class CompilerTest extends ACVMTest { + + protected CompilerTest() { + super(InitTest.STATE); + } + + @Test + public void testConstants() { + assertEquals(1L,evalL("1")); + assertEquals(Samples.FOO,eval(":foo")); + assertCVMEquals('d',eval("\\d")); + assertCVMEquals("baz",eval("\"baz\"")); + + assertSame(Vectors.empty(),eval("[]")); + assertSame(Lists.empty(),eval("()")); + + assertNull(eval("nil")); + assertSame(CVMBool.TRUE,eval("true")); + assertSame(CVMBool.FALSE,eval("false")); + } + + @Test public void testDo() { + assertEquals(1L,evalL("(do 2 1)")); + assertEquals(1L,evalL("(do *depth*)")); // Adds one level to initial depth + assertEquals(2L,evalL("(do (do *depth*))")); + } + + @Test public void testMinCompileRegression() throws IOException { + Context c=context(); + String src=Utils.readResourceAsString("testsource/min.con"); + ACell form=Reader.read(src); + Context exp=c.expand(form); + assertNotError(exp); + Context> com=c.compile(exp.getResult()); + + assertNotError(com); + } + + @Test public void testFnCasting() { + assertEquals(1L,evalL("({2 1} 2)")); + assertNull(eval("({2 1} 1)")); + assertEquals(3L,evalL("({2 1} 1 3)")); + assertCVMEquals(Boolean.TRUE,eval("(#{2 1} 1)")); + assertCVMEquals(Boolean.TRUE,eval("(#{nil 1} nil)")); + assertCVMEquals(Boolean.FALSE,eval("(#{2 1} 7)")); + assertCVMEquals(Boolean.FALSE,eval("(#{2 1} nil)")); + assertEquals(7L,evalL("([] 3 7)")); + + assertEquals(3L,evalL("(:foo {:bar 1 :foo 3})")); + assertNull(eval("(:foo {:bar 1})")); + assertEquals(7L,evalL("(:baz {:bar 1 :foo 3} 7)")); + assertEquals(2L,evalL("(:foo nil 2)")); // TODO: is this sane? treat nil as empty? + + // zero arity failing + assertArityError(step("(:foo)")); + assertArityError(step("({})")); + assertArityError(step("(#{})")); + assertArityError(step("([])")); + + // non-associative lookup + assertCastError(step("(:foo 1 2)")); + + // too much arity + assertArityError(step("({} 1 2 3)")); + assertBoundsError(step("([] 1)")); + assertArityError(step("([] 1 2 3)")); + assertArityError(step("(:foo 1 2 3)")); // arity > type + } + + @Test public void testApply() { + assertCVMEquals(true,eval("(apply = nil)")); + assertCVMEquals(true,eval("(apply = [1 1])")); + assertCVMEquals(false,eval("(apply = [1 1 nil])")); + + assertArityError(step("(apply)")); + + } + + @Test public void testLambda() { + assertEquals(2L,evalL("((fn [a] 2) 3)")); + assertEquals(3L,evalL("((fn [a] a) 3)")); + + assertEquals(1L,evalL("((fn [a] *depth*) 3)")); // Level of invoke depth + } + + + @Test public void testDef() { + assertEquals(2L,evalL("(do (def a 2) (def b 3) a)")); + assertEquals(7L,evalL("(do (def a 2) (def a 7) a)")); + + // TODO: check if these are most logical error types? + assertCompileError(step("(def :a 1)")); + assertCompileError(step("(def a)")); + assertCompileError(step("(def a 2 3)")); + } + + @Test public void testDefMetadataOnLiteral() { + Context ctx=step("(def a ^:foo 2)"); + assertNotError(ctx); + AHashMap m=ctx.getMetadata().get(Symbol.create("a")); + assertSame(CVMBool.TRUE,m.get(Keywords.FOO)); + } + + @Test public void testDefMetadataOnForm() { + String code="(def a ^:foo (+ 1 2))"; + Symbol sym=Symbol.create("a"); + + Context ctx=step(code); + assertNotError(ctx); + ACell v=ctx.getEnvironment().get(sym); + assertCVMEquals(3L,v); + assertSame(CVMBool.TRUE,ctx.getMetadata().get(sym).get(Keywords.FOO)); + } + + @Test public void testDefMetadataOnSymbol() { + Context ctx=step("(def ^{:foo true} a (+ 1 2))"); + assertNotError(ctx); + + Symbol sym=Symbol.create("a"); + ACell v=ctx.getEnvironment().get(sym); + assertCVMEquals(3L,v); + assertSame(CVMBool.TRUE,ctx.getMetadata().get(sym).get(Keywords.FOO)); + } + + @Test public void testCond() { + assertEquals(1L,evalL("(cond nil 2 1)")); + assertEquals(4L,evalL("(cond nil 2 false 3 4)")); + assertEquals(2L,evalL("(cond 1 2 3 4)")); + assertNull(eval("(cond)")); + assertNull(eval("(cond false true)")); + } + + @Test public void testIf() { + assertEquals(read("(cond false 4)"),expand("(if false 4)")); + assertNull(eval("(if false 4)")); + assertEquals(4L,evalL("(if true 4)")); + assertEquals(2L,evalL("(if 1 2 3)")); + assertEquals(3L,evalL("(if nil 2 3)")); + assertEquals(7L,evalL("(if :foo 7)")); + assertEquals(1L,evalL("(if true *depth*)")); + + // test that if macro expansion happens correctly inside vector + assertEquals(Vectors.of(3L,2L),eval("[(if nil 2 3) (if 1 2 3)]")); + + // test that if macro expansion happens correctly inside other macro + assertEquals(3L,evalL("(if (if 1 nil 3) 2 3)")); + + // ARITY error if too few or too many branches + assertArityError(step("(if :foo)")); + assertArityError(step("(if :foo 1 2 3 4 5)")); + } + + @Test + public void testStackOverflow() { + // fake state with default juice + Context c=context(); + + AOp op=Do.create( + // define a nasty function that calls its argument recursively on itself + Def.create("fubar", + Lambda.create(Vectors.of(Symbol.create("func")), + Invoke.create(Local.create(0),Local.create(0)))), + // call the nasty function on itself + Invoke.create(Invoke.create(Lookup.create("fubar"),Lookup.create("fubar"))) + ); + + assertDepthError(c.execute(op)); + } + + @Test + public void testMissingVar() { + assertUndeclaredError(step("this-should-not-resolve")); + } + + @Test + public void testBadEval() { + assertThrows(ParseException.class,()->eval("((")); + } + + @Test + public void testUnquote() { + // Unquote used to execute code at compile time + assertEquals(RT.cvm(3L),eval("~(+ 1 2)")); + assertEquals(Constant.of(3L),comp("~(+ 1 2)")); + + assertEquals(RT.cvm(3L),eval("`~`~(+ 1 2)")); + + // Misc cases + assertNull(eval("`~nil")); + assertEquals(Keywords.STATE,eval("(let [a :state] `~a)")); + assertEquals(Vectors.of(1L,3L),eval("`[1 ~(+ 1 2)]")); + assertEquals(Lists.of(Symbols.INC,3L),eval("`(inc ~(+ 1 2))")); + assertUndeclaredError(step("`~undefined-1")); + assertUndeclaredError(step("~'undefined-1")); + + // not we require compilation down to a single constant + assertEquals(Constant.of(7L),comp("~(+ 7)")); + + assertArityError(step("~(inc)")); + assertCastError(step("~(inc :foo)")); + + // TODO: what are right error types here? + assertCompileError(step("(unquote)")); + assertCompileError(step("(unquote 1 2)")); + } + + @Test + public void testSetHandling() { + // sets used as functions act as a predicate + assertCVMEquals(Boolean.TRUE,eval("(#{1 2} 1)")); + + // get returns value or nil + assertEquals(1L,evalL("(get #{1 2} 1)")); + assertSame(CVMBool.FALSE,eval("(get #{1 2} 3)")); + } + + @Test + public void testQuote() { + assertEquals(Symbols.FOO,eval("(quote foo)")); + assertEquals(Symbols.COUNT,eval("'count")); + assertNull(eval("'nil")); + assertEquals(Lists.of(Symbols.QUOTE,Symbols.COUNT),eval("''count")); + assertEquals(Lists.of(Symbols.QUOTE,Lists.of(Symbols.UNQUOTE,Symbols.COUNT)),eval("''~count")); + + assertEquals(Keywords.STATE,eval("':state")); + assertEquals(Lists.of(Symbols.INC,3L),eval("'(inc 3)")); + + assertEquals(Vectors.of(Symbols.INC,Symbols.DEC),eval("'[inc dec]")); + + assertSame(CVMBool.TRUE,eval("(= (quote a/b) 'a/b)")); + + assertEquals(Symbol.create("undefined-1"),eval("'undefined-1")); + } + + @Test + public void testQuoteDataStructures() { + assertEquals(Maps.of(1,2,3,4), eval("`{~(inc 0) 2 3 ~(dec 5)}")); + assertEquals(Sets.of(1,2,3),eval("`#{1 2 ~(dec 4)}")); + + // TODO: unquote-splicing in data structures. + } + + @Test + public void testQuoteCases() { + // Tests from Racket / Scheme + Context ctx=step("(def x 1)"); + assertEquals(read("(a b c)"),eval(ctx,"`(a b c)")); + assertEquals(read("(a b 1)"),eval(ctx,"`(a b ~x)")); + assertEquals(read("(a b 3)"),eval(ctx,"`(a b ~(+ x 2))")); + assertEquals(read("(a `(b ~x))"),eval(ctx,"`(a `(b ~x))")); + assertEquals(read("(a `(b ~1))"),eval(ctx,"`(a `(b ~~x))")); + assertEquals(read("(a `(b ~1))"),eval(ctx,"`(a `(b ~~`~x))")); + assertEquals(read("(a `(b ~x))"),eval(ctx,"`(a `(b ~~'x))")); + + // Unquote does nothing inside a regular quote + assertEquals(read("(a b (unquote x))"),eval(ctx,"'(a b ~x)")); + assertEquals(read("(unquote x)"),eval(ctx,"'~x")); + + // Unquote escapes surrounding quasiquote + assertEquals(read("(a b (quote 1))"),eval(ctx,"`(a b '~x)")); + } + + + @Test + public void testNestedQuote() { + assertEquals(RT.cvm(10L),eval("(+ (eval `(+ 1 ~2 ~(eval 3) ~(eval `(+ 0 4)))))")); + + assertEquals(RT.cvm(10L),eval("(let [a 2 b 3] (eval `(+ 1 ~a ~(+ b 4))))")); + } + + @Test + public void testQuotedMacro() { + + assertEquals(2L,evalL("(eval '(if true ~(if true 2 3)))")); + } + + @Test + public void testQuotedMetadata() { + // From issue #267 + assertEquals(eval("'(defn foo ^{:a :b} [])"),eval("`(defn foo ^{:a :b} [])")); + assertEquals(eval("'(defn foo ^{:a :b} [a])"),eval("`(defn foo ^{:a :b} [a])")); + } + + + @Test + public void testLet() { + assertEquals(Vectors.of(1L,3L),eval("(let [[a b] [3 1]] [b a])")); + assertEquals(Vectors.of(2L,3L),eval("(let [[a & more] [1 2 3]] more)")); + + // results of bindings should be available for subsequent bindings + assertEquals(Vectors.of(1L,2L,3L),eval("(let [a [1 2] b (conj a 3)] b)")); + + // Result of binding _ is ignored, though side effects must still happen + assertUndeclaredError(step("(let [_ 1] _)")); + assertEquals(Vectors.of(1L,2L),eval("(let [_ (def v [1 2])] v)")); + + // shouldn't be legal to let-bind qualified symbols + assertCompileError(step("(let [foo/bar 1] _)")); + + // ampersand edge cases + assertEquals(Vectors.of(1L,Vectors.of(2L),3L),eval("(let [[a & b c] [1 2 3]] [a b c])")); + + // bad uses of ampersand + assertCompileError(step("(let [[a &] [1 2 3]] a)")); // ampersand at end + assertCompileError(step("(let [[a & b & c] [1 2 3]] [a b c])")); // too many Cooks! + + } + + @Test + public void testLetRebinding() { + assertEquals(6L,evalL("(let [a 1 a (inc a) a (* a 3)] a)")); + + assertUndeclaredError(step("(do (let [a 1] a) a)")); + } + + @Test + public void testLoopBinding() { + assertEquals(Vectors.of(1L,3L),eval("(loop [[a b] [3 1]] [b a])")); + assertEquals(Vectors.of(2L,3L),eval("(loop [[a & more] [1 2 3]] more)")); + } + + @Test + public void testLoopRecur() { + // infinite loop should run out of juice + assertJuiceError(step("(loop [] (recur))")); + + // infinite loop with wrong arity should fail with arity error first + assertArityError(step("(loop [] (recur 1))")); + + assertEquals(Vectors.of(3L,2L,1L),eval ("(loop [v [] n 3] (cond (> n 0) (recur (conj v n) (dec n)) v))")); + + } + + @Test + public void testLookupAddress() { + Lookup l=comp("foo"); + assertEquals(Constant.of(HERO),l.getAddress()); + } + + @Test + public void testFnArity() { + assertNull(eval("((fn []))")); + assertEquals(Vectors.of(2L,3L),eval("((fn [a] a) [2 3])")); + assertArityError(step("((fn))")); + } + + @Test + public void testFnBinding() { + assertEquals(Vectors.of(1L,3L),eval("(let [f (fn [[a b]] [b a])] (f [3 1]))")); + assertEquals(Vectors.of(2L,3L),eval("(let [f (fn [[a & more]] more)] (f [1 2 3]))")); + assertEquals(Vectors.of(2L,3L),eval("(let [f (fn [[_ & more]] more)] (f [1 2 3]))")); + + // Test that parameter binding of outer fn is accessible in inner fn closure. + assertEquals(10L,evalL("(let [f (fn [g] (fn [x] (g x)))] ((f inc) 9))")); + + // this should fail because g is not in lexical bindings of f when defined + assertUndeclaredError(step("(let [f (fn [x] (g x)) g (fn [y] (inc y))] (f 3))")); + } + + @Test + public void testMultiFn() { + assertEquals(CVMLong.ZERO,eval("((fn ([] 0) ([x] 1)) )")); + assertEquals(CVMLong.ONE,eval("((fn ([] 0) ([x] 1)) :foo)")); + + // Test closing over lexical environment in MultiFn + assertEquals(43L,evalL("(let [a 42 f (fn ([b] (+ a b)) ([] 666)) ] (f 1))")); + } + + @Test + public void testBindings() { + assertEquals (2L,evalL("(let [[] nil] 2)")); // nil acts like empty sequence here? + assertEquals (2L,evalL("(let [[] []] 2)")); // empty binding vector is OK + + // empty binding vector (vararg length zero) + assertEquals (Vectors.empty(),eval("(let [[& more] []] more)")); + assertEquals (Vectors.empty(),eval("(let [[& more] nil] more)")); // nil acts like empty sequence? + + assertEquals (2L,evalL("(let [[a] #{2}] a)")); + assertEquals (Sets.of(1L, 2L),eval("(into #{} (let [[a b] #{1 2}] [a b]))")); + + // TODO: should we allow this? Technically just one vararg... + assertEquals (Vectors.of(1,2,3),eval("(let [[& &] [1 2 3]] &)")); + + } + + @Test + public void testBindingError() { + + + + // this should fail because of insufficient arguments + assertArityError(step("(let [[a b] [1]] a)")); + + // this should fail because of insufficient arguments + assertArityError(step("(let [[a b] #{2}] a)")); + + // these should fail because of incorrect argument type + assertArityError(step("(let [[a b] nil] a)")); // treated as empty sequence + assertCastError(step("(let [[a b] :foo] a)")); + + // this should fail because of too many arguments + assertArityError(step("(let [[a b] [1 2 3]] a)")); + + // this should fail because of bad ampersand usage + assertCompileError(step("((fn [a &]) 1 2)")); + + // this should fail because of multiple ampersand usage + assertCompileError(step("(let [[a & b & c] [1 2 3 4]] b)")); + + // insufficient arguments for variadic binding + assertArityError(step("(let [[a & b c d] [1 2]])")); + } + + @Test + public void testBindingParamPriority() { + // if closure is constructed correctly, fn param overrides previous lexical binding + assertEquals(2L,evalL("(let [a 3 f (fn [a] a)] (f 2))")); + + // likewise, lexical parameter should override definition in environment + assertEquals(2L,evalL("(do (def a 3) ((fn [a] a) 2))")); + } + + @Test + public void testLetVsDef() { + assertEquals(Vectors.of(3L,12L,11L),eval("(do (def a 2) [(let [a 3] a) (let [a (+ a 10)] (def a (dec a)) a) a])")); + } + + @Test + public void testDiabolicals() { + // 2^10000 map, too deep to expand + assertDepthError(context().expand(Samples.DIABOLICAL_MAP_2_10000)); + // 30^30 map, too much data to expand + assertJuiceError(context().expand(Samples.DIABOLICAL_MAP_30_30)); + } + + @Test + public void testDefExpander() { + Context c=context(); + String source="(defexpander bex [x e] (syntax \"foo\"))"; + ACell exp=expand(source); + assertTrue(exp instanceof AList); + + AOp compiled=comp(exp,c); + + c=c.execute(compiled); + assertNotError(c); + assertTrue(c.getEnvironment().get(Symbol.create("bex")) instanceof AFn); + assertTrue(c.getMetadata().get(Symbol.create("bex")).containsKey(Keywords.EXPANDER_Q)); + + compiled=comp("(bex 2)",c); + c=c.execute(compiled); + assertEquals(Strings.create("foo"),c.getResult()); + } + + @Test public void testExpansion() { + assertEquals(Keywords.FOO,expand(":foo")); + + assertEquals(Syntax.create(Keywords.FOO,Maps.of(Keywords.BAR,CVMBool.TRUE)),Reader.read("^:bar :foo")); + assertEquals(Syntax.create(Keywords.FOO,Maps.of(Keywords.BAR,CVMBool.TRUE)),expand("^:bar :foo")); + } + + @Test + public void testExpandQuote() { + assertEquals(null,expand("nil")); + assertEquals(Lists.of(Symbols.QUOTE,Symbols.FOO),expand("'foo")); + assertEquals(Lists.of(Symbols.QUOTE,Lists.of(Symbols.UNQUOTE,Symbols.FOO)),expand("'~foo")); + assertEquals(Lists.of(Symbols.QUOTE,Lists.of(Symbols.QUOTE,Lists.of(Symbols.UNQUOTE,Symbols.FOO))),expand("''~foo")); + + assertEquals(Lists.of(Symbols.QUASIQUOTE,Symbols.FOO),expand("`foo")); + + } + + @Test + public void testQuoteCompile() { + assertEquals(Constant.create((ACell)null),comp("nil")); + assertEquals(Lookup.create(HERO,Symbols.FOO),comp("foo")); + assertEquals(Lookup.create(HERO,Symbols.FOO),comp("`~foo")); + } + + @Test + public void testMacrosInMaps() { + assertEquals(Maps.of(1L,2L),eval("(eval '{(if true 1 2) (if false 1 2)})")); + assertEquals(Maps.of(1L,2L),eval("(eval `{(if true 1 2) ~(if false 1 2)})")); + } + + @Test + public void testMacrosNested() { + AVector expected=Vectors.of(1L,2L); + assertEquals(expected,eval("(when (or nil true) (and [1 2]))")); + } + + @Test + public void testMacrosInActor() { + Context ctx=context(); + ctx=step(ctx,"(def lib (deploy `(do (defmacro foo [x] :foo))))"); + Address addr=(Address) ctx.getResult(); + assertNotNull(addr); + + ctx=step(ctx,"(def baz (lib/foo 1))"); + assertEquals(Keywords.FOO,ctx.getResult()); + + ctx=step(ctx,"(def bar ("+addr+"/foo 2))"); + assertEquals(Keywords.FOO,ctx.getResult()); + } + + @Test + public void testMacrosInSets() { + assertEquals(Sets.of(1L,2L),eval("(eval '#{(if true 1 2) (if false 1 2)})")); + assertEquals(Sets.of(1L,2L),eval("(eval `#{(if true 1 2) ~(if false 1 2)})")); + } + + @Test + public void testEdgeCases() { + assertFalse(evalB("(= *juice* *juice*)")); + assertEquals(Maps.of(1L,2L),eval("{1 2 1 2}")); + + // TODO: sanity check? Does/should this depend on map ordering? + assertEquals(1L,evalL("(count {~(inc 1) 3 ~(dec 3) 4})")); + + assertEquals(Maps.of(11L,5L),eval("{~((fn [x] (do (return (+ x 7)) 100)) 4) 5}")); + assertEquals(Maps.of(1L,2L),eval("{(inc 0) 2}")); + + // TODO: figure out correct behaviour for this. Depends on read vs. readSyntax? + //assertEquals(4L,evalL("(count #{*juice* *juice* *juice* *juice*})")); + //assertEquals(2L,evalL("(count {*juice* *juice* *juice* *juice*})")); + } + + @Test + public void testInitialEnvironment() { + // list should be a core function + ACell eval=eval("list"); + assertTrue(eval instanceof AFn); + + // if should be a macro implemented as an expander + // assertTrue(eval("if") instanceof AExpander); + + // def should be a special form, and evaluate to a symbol + assertEquals(Symbols.DEF,eval("def")); + } +} diff --git a/convex-core/src/test/java/convex/core/lang/ContextTest.java b/convex-core/src/test/java/convex/core/lang/ContextTest.java new file mode 100644 index 000000000..70d974a7c --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/ContextTest.java @@ -0,0 +1,201 @@ +package convex.core.lang; + +import static convex.test.Assertions.assertCVMEquals; +import static convex.test.Assertions.assertDepthError; +import static convex.test.Assertions.assertJuiceError; +import static convex.test.Assertions.assertUndeclaredError; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import convex.core.Constants; +import convex.core.ErrorCodes; +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.Address; +import convex.core.data.BlobMaps; +import convex.core.data.Keyword; +import convex.core.data.Strings; +import convex.core.data.Symbol; +import convex.core.data.Vectors; +import convex.core.init.InitTest; +import convex.core.lang.ops.Special; + +/** + * Tests for basic execution Context mechanics and internals + */ +public class ContextTest extends ACVMTest { + + protected ContextTest() { + super(InitTest.BASE); + } + + private final Address ADDR=context().getAddress(); + + @Test + public void testDefine() { + Symbol sym = Symbol.create("the-test-symbol"); + + final Context c2 = context().define(sym, Strings.create("buffy")); + assertCVMEquals("buffy", c2.lookup(sym).getResult()); + + assertUndeclaredError(c2.lookup(Symbol.create("some-bad-symbol"))); + } + + @Test + public void testQuery() { + Context c2 = context(); + c2=c2.query(Reader.read("(+ 1 2)")); + assertNotSame(c2,context()); + assertCVMEquals(3L,c2.getResult()); + assertEquals(context().getDepth(),c2.getDepth(),"Query should preserve context depth"); + + c2=c2.query(Reader.read("*address*")); + assertEquals(c2.getAddress(),c2.getResult()); + } + + @Test + public void testSymbolLookup() { + Context CTX=context(); + Symbol sym1=Symbol.create("count"); + assertEquals(Core.COUNT,CTX.lookup(sym1).getResult()); + + } + + @Test + public void testUndefine() { + Symbol sym = Symbol.create("the-test-symbol"); + + final Context c2 = context().define(sym, Strings.create("vampire")); + assertCVMEquals("vampire", c2.lookup(sym).getResult()); + + final Context c3 = c2.undefine(sym); + assertUndeclaredError(c3.lookup(sym)); + + final Context c4 = c3.undefine(sym); + assertSame(c3,c4); + } + + @Test + public void testExceptionalState() { + Context ctx=context(); + + assertFalse(ctx.isExceptional()); + assertTrue(ctx.withError(ErrorCodes.ASSERT).isExceptional()); + assertTrue(ctx.withError(ErrorCodes.ASSERT,"Assert Failed").isExceptional()); + + assertThrows(IllegalArgumentException.class,()->ctx.withError((Keyword)null)); + + assertThrows(Error.class,()->ctx.withError(ErrorCodes.ASSERT).getResult()); + } + + @Test + public void testJuice() { + Context c=context(); + assertTrue(c.checkJuice(1000)); + + // get a juice error if too much juice consumed + assertJuiceError(c.consumeJuice(c.getJuice() + 1)); + + // no error if all juice is consumed + c=context(); + assertFalse(c.consumeJuice(c.getJuice()).isExceptional()); + } + + @Test + public void testDepth() { + Context c=context(); + assertEquals(0L,c.getDepth()); + assertEquals(0L,evalL("*depth*")); + assertEquals(1L,evalL("(do *depth*)")); + assertEquals(2L,evalL("(do (do *depth*))")); + + // functions should add one level of depth + assertEquals(1L,evalL("((fn [] *depth*))")); // invoke only + assertEquals(2L,evalL("(do (defn f [] *depth*) (f))")); // do + invoke + + // In compiler unquote + assertEquals(2L,evalL("~*depth*")); // compile, unquote + assertEquals(3L,evalL("~(do *depth*)")); // compile+ unquote + do + + // in custom expander + assertEquals(2L,evalL("(expand :foo (fn [x e] *depth*))")); // in expand, invoke + assertEquals(1L,evalL("(expand *depth* (fn [x e] x))")); // in expand arg + + + // In expansion, should be equivalent to expanded code + assertEquals(evalL("*depth*"),evalL("`~*depth*")); + assertEquals(evalL("(do *depth*)"),evalL("`~(do *depth*)")); + + } + + @Test + public void testDepthLimit() { + Context c=context().withDepth(Constants.MAX_DEPTH-1); + assertEquals(Constants.MAX_DEPTH-1,c.getDepth()); + + // Can run 1 deep at this depth + assertEquals(RT.cvm(Constants.MAX_DEPTH-1),c.execute(comp("*depth*")).getResult()); + assertNull(c.execute(comp("(do)")).getResult()); + + // Shouldn't be possible to execute any Op beyond max depth + assertDepthError(c.execute(comp("(do *depth*)"))); + } + + + @Test + public void testSpecial() { + Context ctx=context(); + assertEquals(ADDR, eval(Symbols.STAR_ADDRESS)); + assertEquals(ADDR, eval(Symbols.STAR_ORIGIN)); + assertNull(eval(Symbols.STAR_CALLER)); + + // Compiler returns Special Op + assertEquals(Special.forSymbol(Symbols.STAR_BALANCE),comp("*balance*")); + + assertNull(eval(Symbols.STAR_RESULT)); + assertCVMEquals(ctx.getJuice(), eval(Special.forSymbol(Symbols.STAR_JUICE))); + assertCVMEquals(0L,eval(Symbols.STAR_DEPTH)); + assertCVMEquals(ctx.getBalance(ADDR),eval(Symbols.STAR_BALANCE)); + assertCVMEquals(0L,eval(Symbols.STAR_OFFER)); + + assertCVMEquals(0L,eval(Symbols.STAR_SEQUENCE)); + + assertCVMEquals(Constants.INITIAL_TIMESTAMP,eval(Symbols.STAR_TIMESTAMP)); + + assertSame(ctx.getState(), eval(Symbols.STAR_STATE)); + assertSame(BlobMaps.empty(),eval(Symbols.STAR_HOLDINGS)); + + assertUndeclaredError(ctx.eval(Symbol.create("*bad-special-symbol*"))); + } + + @Test + public void testLog() { + Context c = context(); + assertTrue(c.getLog().isEmpty()); + + AVector v=Vectors.of(1,2,3); + c.appendLog(v); + + AVector> log=c.getLog(); + assertFalse(c.getLog().isEmpty()); + + + assertEquals(1,log.count()); + assertEquals(v,log.get(0).get(1)); + } + + @Test + public void testReturn() { + Context ctx=context(); + ctx = ctx.withResult(RT.cvm(100)); + assertEquals(ctx.getDepth(), ctx.getDepth()); + } + +} diff --git a/convex-core/src/test/java/convex/core/lang/CoreTest.java b/convex-core/src/test/java/convex/core/lang/CoreTest.java new file mode 100644 index 000000000..a8674f787 --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/CoreTest.java @@ -0,0 +1,3793 @@ +package convex.core.lang; + +import static convex.test.Assertions.assertArgumentError; +import static convex.test.Assertions.assertArityError; +import static convex.test.Assertions.assertAssertError; +import static convex.test.Assertions.assertBoundsError; +import static convex.test.Assertions.assertCVMEquals; +import static convex.test.Assertions.assertCastError; +import static convex.test.Assertions.assertCompileError; +import static convex.test.Assertions.assertDepthError; +import static convex.test.Assertions.assertError; +import static convex.test.Assertions.assertFundsError; +import static convex.test.Assertions.assertJuiceError; +import static convex.test.Assertions.assertMemoryError; +import static convex.test.Assertions.assertNobodyError; +import static convex.test.Assertions.assertNotError; +import static convex.test.Assertions.assertStateError; +import static convex.test.Assertions.assertTrustError; +import static convex.test.Assertions.assertUndeclaredError; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import convex.core.Block; +import convex.core.BlockResult; +import convex.core.Constants; +import convex.core.ErrorCodes; +import convex.core.State; +import convex.core.crypto.AKeyPair; +import convex.core.data.ABlob; +import convex.core.data.ACell; +import convex.core.data.AHashMap; +import convex.core.data.ASet; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.core.data.Blob; +import convex.core.data.BlobMap; +import convex.core.data.BlobMaps; +import convex.core.data.Format; +import convex.core.data.Hash; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.List; +import convex.core.data.Lists; +import convex.core.data.MapEntry; +import convex.core.data.Maps; +import convex.core.data.Sets; +import convex.core.data.Strings; +import convex.core.data.Symbol; +import convex.core.data.Syntax; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMBool; +import convex.core.data.prim.CVMByte; +import convex.core.data.prim.CVMDouble; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.BadSignatureException; +import convex.core.exceptions.InvalidDataException; +import convex.core.init.Init; +import convex.core.init.InitTest; +import convex.core.lang.impl.CorePred; +import convex.core.lang.impl.ICoreDef; +import convex.core.lang.ops.Constant; +import convex.core.lang.ops.Do; +import convex.core.lang.ops.Invoke; +import convex.core.lang.ops.Lookup; +import convex.core.lang.ops.Special; + +/** + * Test class for core functions in the initial environment. + * + * The state setup included core libraries such as the registry and trust monitors + * which require integration with core language features. + * + * Needs completely deterministic, fully specified behaviour if we want + * consistent results so we need to do a lot of negative testing here. + */ +public class CoreTest extends ACVMTest { + + + protected CoreTest() throws IOException { + super(InitTest.BASE); + } + + @Test + public void testAddress() { + Address a = HERO; + assertEquals(a, eval("(address \"" + a.toHexString() + "\")")); + assertEquals(a, eval("(address 0x" + a.toHexString() + ")")); + assertEquals(a, eval("(address (address \"" + a.toHexString() + "\"))")); + assertEquals(a, eval("(address (blob \"" + a.toHexString() + "\"))")); + assertEquals(a, eval("(address "+a.longValue()+")")); + + // bad arities + assertArityError(step("(address 1 2)")); + assertArityError(step("(address)")); + + // invalid address lengths - not a cast error since argument types (in general) are valid + assertArgumentError(step("(address \"1234abcd\")")); + assertArgumentError(step("(address 0x1234abcd)")); + + // invalid conversions + assertCastError(step("(address :foo)")); + assertCastError(step("(address nil)")); + } + + @Test + public void testBlob() { + Blob b = Blob.fromHex("cafebabe"); + assertEquals(b, eval("(blob \"Cafebabe\")")); + assertEquals(b, eval("(blob (blob \"cafebabe\"))")); + + assertEquals("cafebabe", evalS("(str (blob \"Cafebabe\"))")); + + assertEquals(eval("0x"),eval("(blob (str))")); // blob literal + + assertEquals(eval("*address*"),eval("(address (blob *address*))")); + + // Account key should be a Blob + assertEquals(eval("*key*"),eval("(blob *key*)")); + + + // round trip back to Blob + assertTrue(evalB("(blob? (blob (hash (encoding [1 2 3]))))")); + + assertArityError(step("(blob 1 2)")); + assertArityError(step("(blob)")); + + assertCastError(step("(blob \"f\")")); // odd length hex string bug #54 special case + assertCastError(step("(blob :foo)")); + assertCastError(step("(blob nil)")); + } + + @Test + public void testByte() { + assertSame(CVMByte.create(0x01), eval("(byte 1)")); + assertSame(CVMByte.create(0xff), eval("(byte 255)")); + assertSame(CVMByte.create(0xff), eval("(byte -1)")); + assertSame(CVMByte.create(0xff), eval("(byte (byte -1))")); + + assertCastError(step("(byte nil)")); + assertCastError(step("(byte :foo)")); + + assertArityError(step("(byte)")); + assertArityError(step("(byte nil nil)")); // arity before cast + } + + @Test + public void testDoc() { + assertEquals(42L, evalL("(do (def foo ^{:doc 42} nil) (doc foo))")); + assertEquals(42L, evalL("(do (def a (deploy '(def foo ^{:doc 42} nil))) (doc a/foo))")); + } + + @Test + public void testLet() { + + assertCastError(step("(let [[a b] :foo] b)")); + + assertArityError(step("(let [[a b] nil] b)")); + assertArityError(step("(let [[a b] [1]] b)")); + assertEquals(2L,evalL("(let [[a b] [1 2]] b)")); + assertEquals(2L,evalL("(let [[a b] '(1 2)] b)")); + + assertCompileError(step("(let ['(a b) '(1 2)] b)")); + + // badly formed lets - Issue #80 related + assertCompileError(step("(let)")); + assertCompileError(step("(let :foo)")); + assertCompileError(step("(let [a])")); + + + } + + @Test + public void testGet() { + assertEquals(2L, evalL("(get {1 2} 1)")); + assertEquals(4L, evalL("(get {1 2 3 4} 3)")); + assertEquals(4L, evalL("(get {1 2 3 4} 3 7)")); + assertNull(eval("(get {1 2} 2)")); // null if not present + assertEquals(7L, evalL("(get {1 2 3 4} 5 7)")); // fallback arg + + assertSame(CVMBool.TRUE, eval("(get #{1 2} 1)")); + assertSame(CVMBool.TRUE, eval("(get #{1 2} 2)")); + assertSame(CVMBool.FALSE, eval("(get #{1 2} 3)")); // null if not present + assertEquals(4L, evalL("(get #{1 2} 3 4)")); // fallback + + assertEquals(2L, evalL("(get [1 2 3] 1)")); + assertEquals(2L, evalL("(get [1 2 3] 1 7)")); + assertEquals(7L, evalL("(get [1 2 3] 4 7)")); + assertEquals(7L, evalL("(get [1 2] nil 7)")); + assertEquals(7L, evalL("(get [1 2] -5 7)")); + assertNull(eval("(get [1 2] :foo)")); + assertNull(eval("(get [1 2] 10)")); + assertNull(eval("(get [1 2] -1)")); + assertNull(eval("(get [1 2] 1.0)")); + + assertNull(eval("(get [1 2 3] (byte 1))")); // TODO: is this sane? + + assertNull(eval("(get nil nil)")); + assertNull(eval("(get nil 10)")); + assertEquals(3L, evalL("(get nil 2 3)")); + assertEquals(3L, evalL("(get nil nil 3)")); + + assertArityError(step("(get 1)")); // arity > cast + assertArityError(step("(get)")); + assertArityError(step("(get 1 2 3 4)")); + + assertCastError(step("(get 1 2 3)")); // 3 arg could work, so cast error on 1st arg + assertCastError(step("(get 1 1)")); // 2 arg could work, so cast error on 1st arg + } + + @Test + public void testGetIn() { + assertEquals(2L, evalL("(get-in {1 2} [1])")); + assertEquals(4L, evalL("(get-in {1 {2 4} 3 5} [1 2])")); + assertEquals(1L, evalL("(get-in #{1 2} [1])")); + assertEquals(2L, evalL("(get-in [1 2 3] [1])")); + assertEquals(2L, evalL("(get-in [1 2 3] [1] :foo)")); + assertEquals(3L, evalL("(get-in [1 2 3] '(2))")); + assertEquals(3L, evalL("(get-in (list 1 2 3) [2])")); + assertEquals(4L, evalL("(get-in [1 2 {:foo 4} 3 5] [2 :foo])")); + + // special case: don't coerce to collection if empty sequence of keys + // so non-collection value may be used safely + assertEquals(3L, evalL("(get-in 3 [])")); + + assertEquals(Maps.of(1L, 2L), eval("(get-in {1 2} nil)")); + assertEquals(Maps.of(1L, 2L), eval("(get-in {1 2} [])")); + assertEquals(Vectors.empty(), eval("(get-in [] [])")); + assertEquals(Lists.empty(), eval("(get-in (list) nil)")); + + assertEquals(Keywords.FOO, eval("(get-in {1 2} [3] :foo)")); + assertEquals(Keywords.FOO, eval("(get-in nil [3] :foo)")); + + assertNull(eval("(get-in nil nil)")); + assertNull(eval("(get-in [1 2 3] [:foo])")); + assertNull(eval("(get-in nil [])")); + assertNull(eval("(get-in nil [1 2])")); + assertNull(eval("(get-in #{} [1 2 3])")); + + assertArityError(step("(get-in 1)")); // arity > cast + assertArityError(step("(get-in 1 2 3 4)")); // arity > cast + + assertCastError(step("(get-in 1 2 3)")); + assertCastError(step("(get-in 1 [1])")); + assertCastError(step("(get-in [1] [0 2])")); + assertCastError(step("(get-in 1 {1 2})")); // keys not a sequence + + assertCastError(step("(get-in [1] 1)")); + } + + @Test + public void testLong() { + assertCVMEquals(1L, eval("(long 1)")); + assertEquals(128L, evalL("(long (byte 128))")); + assertEquals(97L, evalL("(long \\a)")); + assertEquals(2147483648L, evalL("(long 2147483648)")); + + assertEquals(4096L, evalL("(long 0x1000)")); + assertEquals(255L, evalL("(long 0xff)")); + assertEquals(4294967295L, evalL("(long 0xffffffff)")); + assertEquals(-1L, evalL("(long 0xffffffffffffffff)")); + assertEquals(255L, evalL("(long 0xff00000000000000ff)")); // only taking last 8 bytes + assertEquals(-1L, evalL("(long 0xcafebabeffffffffffffffff)")); // interpret as big endian big integer + + // Currently we allow bools to cast to longs like this. TODO: maybe reconsider? + assertEquals(1L, evalL("(long true)")); + assertEquals(0L, evalL("(long false)")); + + + assertArityError(step("(long)")); + assertArityError(step("(long 1 2)")); + assertCastError(step("(long nil)")); + assertCastError(step("(long [])")); + assertCastError(step("(long :foo)")); + } + + @Test + public void testChar() { + assertCVMEquals('a', eval("\\a")); + assertCVMEquals('a', eval("(char 97)")); + assertCVMEquals('a', eval("(nth \"bar\" 1)")); + + assertCastError(step("(char nil)")); + assertCastError(step("(char {})")); + + assertArityError(step("(char)")); + assertArityError(step("(char nil nil)")); // arity before cast + + } + + @Test + public void testBoolean() { + // test precise values + assertSame(CVMBool.TRUE, eval("(boolean 1)")); + assertSame(CVMBool.FALSE, eval("(boolean nil)")); + + // nil and false should be falsey + assertFalse(evalB("(boolean false)")); + assertFalse(evalB("(boolean nil)")); + + // anything else should be truthy + assertTrue(evalB("(boolean true)")); + assertTrue(evalB("(boolean [])")); + assertTrue(evalB("(boolean #{})")); + assertTrue(evalB("(boolean 1)")); + assertTrue(evalB("(boolean :foo)")); + + assertArityError(step("(boolean)")); + assertArityError(step("(boolean 1 2)")); + } + + @Test public void testIf() { + // basic branching + assertEquals(1L,evalL("(if true 1 2)")); + assertEquals(2L,evalL("(if false 1 2)")); + + // expressions + assertEquals(6L,evalL("(if (= 1 1) (* 2 3) (* 3 4))")); + assertEquals(12L,evalL("(if (nil? false) (* 2 3) (* 3 4))")); + + + // null return for missing false branch + assertNull(eval("(if false 1)")); + + // TODO: should these be arity errors? + assertArityError(step("(if)")); + assertArityError(step("(if 1)")); + assertArityError(step("(if 1 2 3 4)")); + } + + @Test + public void testEquals() { + assertTrue(evalB("(= \\a)")); + assertTrue(evalB("(= 1 1)")); + assertFalse(evalB("(= 1 2)")); + assertFalse(evalB("(= 1 nil)")); + assertFalse(evalB("(= 1 1.0)")); + assertFalse(evalB("(= \\a \\b)")); + assertFalse(evalB("(= :foo :baz)")); + assertFalse(evalB("(= :foo 'foo)")); + assertTrue(evalB("(= :bar :bar :bar)")); + assertFalse(evalB("(= :bar :bar :bar 2)")); + assertFalse(evalB("(= *juice* *juice*)")); + assertTrue(evalB("(=)")); + assertTrue(evalB("(= = =)")); + assertTrue(evalB("(= nil nil)")); + assertTrue(evalB("(= ##NaN ##NaN)")); // value equality, but not numeric equality + + } + + @Test + public void testEqualsNumeric() { + assertTrue(evalB("(==)")); + assertTrue(evalB("(== ##Inf)")); + assertTrue(evalB("(== ##NaN)")); + assertTrue(evalB("(== 1 1.0)")); + assertFalse(evalB("(== ##NaN ##NaN)")); // value equality, but not numeric equality + + assertCastError(step("(== :foo)")); + assertCastError(step("(== 1 :foo)")); + assertCastError(step("(== #4)")); + assertCastError(step("(== [])")); + } + + @Test + public void testComparisons() { + assertTrue(evalB("(== 0 0.0)")); + assertTrue(evalB("(< 1)")); + assertTrue(evalB("(> 3 2 1)")); + assertTrue(evalB("(>= 3 2)")); + assertTrue(evalB("(<= 1 2.0 2)")); + + assertTrue(evalB("(==)")); + assertTrue(evalB("(<=)")); + assertTrue(evalB("(>=)")); + assertTrue(evalB("(<)")); + assertTrue(evalB("(>)")); + + assertCastError(step("(== nil nil)")); + assertCastError(step("(> nil)")); + assertCastError(step("(< 1 :foo)")); + assertCastError(step("(<= 1 3 \"hello\")")); + assertCastError(step("(>= nil 1.0)")); + + // ##NaN behaviour + assertFalse(evalB("(<= ##NaN ##NaN)")); + assertFalse(evalB("(<= ##NaN 42)")); + assertFalse(evalB("(< ##NaN 42)")); + assertFalse(evalB("(> 42 ##NaN)")); + assertFalse(evalB("(<= 42 ##NaN)")); + assertFalse(evalB("(== ##NaN 42)")); + assertFalse(evalB("(== ##NaN ##NaN)")); + assertFalse(evalB("(>= ##NaN 42)")); + + // TODO: decide if we want short-circuiting behaviour on casts? Probably not? + // assertCastError(step("(>= 1 2 3 '*balance*)")); + assertFalse(evalB("(>= 1 2 3 '*balance*)")); + } + + @Test + public void testLog() { + AVector v0=Vectors.of(1L, 2L); + + Context c=step("(log 1 2)"); + assertEquals(v0,c.getResult()); + AVector> log=c.getLog(); + assertEquals(1,log.count()); // only one address did a log + assertNotNull(log); + + assertEquals(1,log.count()); // one log entry only + assertEquals(v0,log.get(0).get(1)); + + // do second log in same context + AVector v1=Vectors.of(3L, 4L); + c=step(c,"(log 3 4)"); + log=c.getLog(); + + assertEquals(2,log.count()); // should be two entries now + assertEquals(v0,log.get(0).get(1)); + assertEquals(v1,log.get(1).get(1)); + } + + + @Test + public void testLogInActor() { + AVector v0=Vectors.of(1L, 2L); + + Context c=step("(deploy '(do (defn event ^{:callable? true} [& args] (apply log args)) (defn non-event ^{:callable? true} [& args] (rollback (apply log args)))))"); + Address actor=(Address) c.getResult(); + + assertEquals(0,c.getLog().count()); // Nothing logged so far + + // call actor function + c=step(c,"(call "+actor+" (event 1 2))"); + AVector> log = c.getLog(); + + assertEquals(1,log.count()); // should be one entry by the actor + assertEquals(v0,log.get(0).get(1)); + + // call actor function which rolls back - should also roll back log + c=step(c,"(call "+actor+" (non-event 3 4))"); + log = c.getLog(); + assertEquals(1,log.count()); // should be one entry by the actor + assertEquals(v0,log.get(0).get(1)); + + } + + @Test + public void testVector() { + assertEquals(Vectors.of(1L, 2L), eval("(vector 1 2)")); + assertEquals(Vectors.empty(), eval("(vector)")); + } + + @Test + public void testVectorTypes() { + assertEquals(Vectors.of(1L, 2L).getEncoding(),MapEntry.of(1L, 2L).getEncoding()); // should be same Hash / Encoding + assertEquals(Vectors.of(1L, 2L), eval("(first {1 2})")); // map entry is a vector + } + + @Test + public void testIdentity() { + assertNull(eval("(identity nil)")); + assertEquals(Vectors.of(1L, 2L), eval("(identity [1 2])")); + + assertArityError(step("(identity)")); + assertArityError(step("(identity 1 2)")); + } + + @Test + public void testConcat() { + assertNull(eval("(concat)")); + assertNull(eval("(concat nil nil nil)")); + + // singleton identity preservation + assertSame(Vectors.empty(), eval("(concat [])")); + assertSame(Vectors.empty(), eval("(concat nil [])")); + assertSame(Lists.empty(), eval("(concat () nil)")); + assertSame(Lists.empty(), eval("(concat nil ())")); + + assertCastError(step("(concat 1 2)")); + assertCastError(step("(concat \"Foo\" \"Bar\")")); + + assertEquals(Vectors.of(1L, 2L, 3L, 4L), eval("(concat [1 2] [3 4])")); + assertEquals(Vectors.of(1L, 2L, 3L, 4L), eval("(concat nil [1 2] '(3) [] [4])")); + assertEquals(List.of(1L, 2L, 3L, 4L), eval("(concat nil '(1 2) [3 4] nil)")); + } + + @Test + public void testMapcat() { + assertNull(eval("(mapcat (fn[x] x) nil)")); + assertEquals(Vectors.of(2L, 2L), eval("(mapcat (fn [x] [x x]) [2])")); + assertEquals(Vectors.empty(), eval("(mapcat (fn [x] nil) [1 2 3 4])")); + assertEquals(Lists.empty(), eval("(mapcat (fn [x] nil) '(1 2 3 4))")); + assertEquals(Vectors.of(1L, 2L, 3L), eval("(mapcat vector [1 2 3])")); + assertEquals(Vectors.of(1L, 2L, 2L, 3L, 3L, 4L), eval("(mapcat (fn [a b] [a b]) [1 2 3] [2 3 4])")); + + assertArityError(step("(mapcat identity)")); + assertCastError(step("(mapcat nil [1 2])")); + } + + + @Test + public void testHashMap() { + assertEquals(Maps.empty(), eval("(hash-map)")); + assertEquals(Maps.of(1L, 2L), eval("(hash-map 1 2)")); + assertEquals(Maps.of(null, null), eval("(hash-map nil nil)")); + assertEquals(Maps.of(1L, 2L, 3L, 4L), eval("(hash-map 3 4 1 2)")); + + // Check last value of equal keys is used + assertEquals(Maps.of(1L, 4L), eval("(hash-map 1 2 1 3 1 4)")); + assertEquals(Maps.of(1L, 2L), eval("(hash-map 1 4 1 3 1 2)")); + + assertArityError(step("(hash-map 1)")); + assertArityError(step("(hash-map 1 2 3)")); + assertArityError(step("(hash-map 1 2 3 4 5)")); + } + + @Test + public void testBlobMap() { + assertSame(BlobMaps.empty(), eval("(blob-map)")); + + assertEquals(eval("(blob-map 0xa2 :foo)"),eval("(assoc (blob-map) 0xa2 :foo)")); + assertEquals(eval("(blob-map 0xa2 :foo 0xb3 :bar)"),eval("(assoc (blob-map) 0xa2 :foo 0xb3 :bar)")); + + // Bad key types should result in argument errors + assertArgumentError(step("(blob-map :foo :bar)")); // See issue #162 + assertArgumentError(step("(assoc (blob-map) :foo 10)")); // See Issue #101 + + assertArityError(step("(blob-map 0xabcd)")); + assertArityError(step("(blob-map 0xa2 :foo 0xb3)")); + } + + @Test + public void testKeys() { + assertEquals(Vectors.empty(), eval("(keys {})")); + assertEquals(Vectors.of(1L), eval("(keys {1 2})")); + assertEquals(Sets.of(1L, 3L, 5L), eval("(set (keys {1 2 3 4 5 6}))")); + + assertEquals(Vectors.empty(),RT.keys(BlobMaps.empty())); + assertEquals(Vectors.of(HERO),RT.keys(BlobMap.of(HERO, 1L))); + + assertCastError(step("(keys 1)")); + assertCastError(step("(keys [])")); + assertCastError(step("(keys nil)")); // TODO: maybe empty set? + + assertArityError(step("(keys)")); + assertArityError(step("(keys {} {})")); + } + + @Test + public void testValues() { + assertEquals(Vectors.empty(), eval("(values {})")); + assertEquals(Vectors.of(2L), eval("(values {1 2})")); + assertEquals(Sets.of(2L, 4L, 6L), eval("(set (values {1 2 3 4 5 6}))")); + + assertCastError(step("(values 1)")); + assertCastError(step("(values [])")); + assertCastError(step("(values nil)")); // TODO: maybe empty set? + + assertArityError(step("(values)")); + assertArityError(step("(values {} {})")); + } + + @Test + public void testHashSet() { + assertEquals(Sets.empty(), eval("(hash-set)")); + assertEquals(Sets.of(1L, 2L), eval("(hash-set 1 2)")); + assertEquals(Sets.of((Object) null), eval("(hash-set nil nil)")); + assertEquals(Sets.of(1L, 2L, 3L, 4L), eval("(hash-set 3 4 1 2)")); + + // de-duplication + assertEquals(Sets.of(1L, 2L, 3L), eval("(hash-set 1 2 3 1)")); + assertEquals(Sets.of((Long)null), eval("(hash-set nil nil nil)")); + assertEquals(Sets.of(Sets.empty()), eval("(hash-set (hash-set) (hash-set))")); + + assertEquals(Sets.of((Object) null), eval("(hash-set nil)")); + } + + @Test + public void testStr() { + assertEquals("", evalS("(str)")); + assertEquals("1", evalS("(str 1)")); + assertEquals("12", evalS("(str 1 2)")); + assertEquals("42.0", evalS("(str 42.0)")); + assertEquals("##Inf", evalS("(str ##Inf)")); + assertEquals("##-Inf", evalS("(str ##-Inf)")); + assertEquals("##NaN", evalS("(str ##NaN)")); + assertEquals("255", evalS("(str (byte 0xff))")); + assertEquals("baz", evalS("(str \"baz\")")); + assertEquals("bazbar", evalS("(str \"baz\" \"bar\")")); + assertEquals("baz", evalS("(str \\b \\a \\z)")); + assertEquals(":foo", evalS("(str :foo)")); + assertEquals("nil", evalS("(str nil)")); + assertEquals("true", evalS("(str true)")); + assertEquals("cafebabe", evalS("(str (blob \"CAFEBABE\"))")); + + // Standalone chars are stringified Java-style whereas chars embedded in a container (eg. in a vector) + // must be EDN-style readable representations. + // + assertEquals("a", evalS("(str \\a)")); + assertEquals("conve x", evalS("(str \\c \\o \\n \"ve\" \\space \\x)")); + assertEquals("[\\a \\b (fn [] \\newline) (\\return {\\space \\tab})]", + evalS("(str [\\a \\b (fn [] \\newline) (list \\return {\\space \\tab})])")); + } + + @Test + public void testAssert() { + assertNull(eval("(assert)")); + assertNull(eval("(assert true)")); + assertNull(eval("(assert (= 1 1))")); + assertNull(eval("(assert '(= 1 2))")); // form itself is truthy, not evaluated + assertNull(eval("(assert '(assert false))")); // form itself is truthy, not evaluated + assertNull(eval("(assert 1 2 3)")); + + assertAssertError(step("(assert false)")); + assertAssertError(step("(assert true false)")); + assertAssertError(step("(assert (= 1 2))")); + } + + + @Test + public void testCeil() { + // Double cases + assertEquals(1.0,evalD("(ceil 0.001)")); + assertEquals(-1.0,evalD("(ceil -1.25)")); + + // Integral cases + assertEquals(-1.0,evalD("(ceil -1)")); + assertEquals(0.0,evalD("(ceil 0)")); + assertEquals(1.0,evalD("(ceil 1)")); + + // Special cases + assertEquals(Double.NaN,evalD("(ceil ##NaN)")); + assertEquals(Double.POSITIVE_INFINITY,evalD("(ceil (/ 1 0))")); + assertEquals(Double.NEGATIVE_INFINITY,evalD("(ceil (/ -1 0))")); + + assertCastError(step("(ceil #3)")); + assertCastError(step("(ceil :foo)")); + assertCastError(step("(ceil nil)")); + assertCastError(step("(ceil [])")); + + assertArityError(step("(ceil)")); + assertArityError(step("(ceil :foo :bar)")); // arity > cast + } + + @Test + public void testFloor() { + // Double cases + assertEquals(0.0,evalD("(floor 0.001)")); + assertEquals(-2.0,evalD("(floor -1.25)")); + + // Integral cases + assertEquals(-1.0,evalD("(floor -1)")); + assertEquals(0.0,evalD("(floor 0)")); + assertEquals(1.0,evalD("(floor 1)")); + + // Special cases + assertEquals(Double.NaN,evalD("(floor ##NaN)")); + assertEquals(Double.POSITIVE_INFINITY,evalD("(floor (/ 1 0))")); + assertEquals(Double.NEGATIVE_INFINITY,evalD("(floor (/ -1 0))")); + + assertCastError(step("(floor #666)")); + assertCastError(step("(floor :foo)")); + assertCastError(step("(floor nil)")); + assertCastError(step("(floor [])")); + + assertArityError(step("(floor)")); + assertArityError(step("(floor :foo :bar)")); // arity > cast + } + + @Test + public void testAbs() { + // Integer cases + assertEquals(1L,evalL("(abs 1)")); + assertEquals(10L,evalL("(abs (byte 10))")); + assertEquals(17L,evalL("(abs -17)")); + assertEquals(Long.MAX_VALUE,evalL("(abs 9223372036854775807)")); + + // Double cases + assertEquals(1.0,evalD("(abs 1.0)")); + assertEquals(13.0,evalD("(abs (double -13))")); + assertEquals(Math.pow(10,100),evalD("(abs (pow 10 100))")); + + // Fun Double cases + assertEquals(Double.NaN,evalD("(abs ##NaN)")); + assertEquals(Double.POSITIVE_INFINITY,evalD("(abs (/ 1 0))")); + assertEquals(Double.POSITIVE_INFINITY,evalD("(abs (/ -1 0))")); + + // long overflow case + assertEquals(Long.MIN_VALUE,evalL("(abs -9223372036854775808)")); + assertEquals(Long.MAX_VALUE,evalL("(abs -9223372036854775807)")); + + // Needs a numeric type, else CAST error + assertCastError(step("(abs :foo)")); + assertCastError(step("(abs nil)")); + assertCastError(step("(abs #78)")); + assertCastError(step("(abs [1])")); + + assertArityError(step("(abs)")); + assertArityError(step("(abs :foo :bar)")); // arity > cast + } + + @Test + public void testSignum() { + // Integer cases + assertEquals(1L,evalL("(signum 1)")); + assertEquals(1L,evalL("(signum (byte 10))")); + assertEquals(-1L,evalL("(signum -17)")); + assertEquals(1L,evalL("(signum 9223372036854775807)")); + assertEquals(-1L,evalL("(signum -9223372036854775808)")); + + // Double cases + assertEquals(0.0,evalD("(signum 0.0)")); + assertEquals(1.0,evalD("(signum 1.0)")); + assertEquals(-1.0,evalD("(signum (double -13))")); + assertEquals(1.0,evalD("(signum (pow 10 100))")); + + // Needs a numeric type, else cast error + assertCastError(step("(signum #1)")); + assertCastError(step("(signum 0xabab)")); + assertCastError(step("(signum nil)")); + assertCastError(step("(signum :foo)")); + + // Fun Double cases + assertEquals(Double.NaN,evalD("(signum ##NaN)")); + assertEquals(1.0,evalD("(signum ##Inf)")); + assertEquals(-1.0,evalD("(signum ##-Inf)")); + + assertArityError(step("(signum)")); + assertArityError(step("(signum :foo :bar)")); // arity > cast + } + + @Test + public void testNot() { + assertFalse(evalB("(not 1)")); + assertTrue(evalB("(not false)")); + assertTrue(evalB("(not nil)")); + assertFalse(evalB("(not [])")); + + assertArityError(step("(not)")); + assertArityError(step("(not true false)")); + } + + @Test + public void testNth() { + assertEquals(2L, evalL("(nth [1 2] 1)")); + assertEquals(2L, evalL("(nth [1 2] (byte 1))")); + assertCVMEquals('c', eval("(nth \"abc\" 2)")); + assertEquals(CVMByte.create(10), eval("(nth 0xff0a0b 1)")); // Blob nth byte + + assertArityError(step("(nth)")); + assertArityError(step("(nth [])")); + assertArityError(step("(nth [] 1 2)")); + assertArityError(step("(nth 1 1 2)")); // arity > cast + + // nth on Blobs + assertEquals(CVMByte.create(255),eval("(nth 0xFF 0)")); + assertFalse (evalB("(= 16 (nth 0x0010 1))")); + assertTrue(evalB("(== 16 (nth 0x0010 1))")); + + // cast errors for bad indexes + assertCastError(step("(nth [] :foo)")); + assertCastError(step("(nth [] nil)")); + + // cast errors for non-sequential objects + assertCastError(step("(nth :foo 0)")); + assertCastError(step("(nth 12 13)")); + + // BOUNDS error because treated as empty sequence + assertBoundsError(step("(nth nil 10)")); + + assertBoundsError(step("(nth 0x 0)")); + assertBoundsError(step("(nth nil 0)")); + assertBoundsError(step("(nth (str) 0)")); + assertBoundsError(step("(nth {} 10)")); + + assertBoundsError(step("(nth [1 2] 10)")); + assertBoundsError(step("(nth [1 2] -1)")); + assertBoundsError(step("(nth \"abc\" 3)")); + assertBoundsError(step("(nth \"abc\" -1)")); + } + + @Test + public void testList() { + assertEquals(Lists.of(1L, 2L), eval("(list 1 2)")); + assertEquals(Lists.of(Symbols.LIST), eval("(list 'list)")); + assertEquals(Lists.of(Symbols.LIST), eval("'(list)")); + assertEquals(Lists.empty(), eval("(list)")); + } + + @Test + public void testVec() { + assertSame(Vectors.empty(), eval("(vec nil)")); + assertSame(Vectors.empty(), eval("(vec [])")); + assertSame(Vectors.empty(), eval("(vec {})")); + assertSame(Vectors.empty(), eval("(vec (blob-map))")); + + assertEquals( eval("[\\a \\b \\c]"), eval("(vec \"abc\")")); + + assertEquals(Vectors.of(1,2,3,4), eval("(vec (list 1 2 3 4))")); + assertEquals(Vectors.of(MapEntry.of(1,2)), eval("(vec {1,2})")); + + assertCastError(step("(vec 1)")); + assertCastError(step("(vec :foo)")); + + assertArityError(step("(vec)")); + assertArityError(step("(vec 1 2)")); + } + + @Test + public void testReverse() { + assertSame(Lists.empty(), eval("(reverse nil)")); + assertSame(Lists.empty(), eval("(reverse [])")); + assertSame(Vectors.empty(), eval("(reverse ())")); + assertEquals(Vectors.of(1,2,3), eval("(reverse '(3 2 1))")); + assertEquals(Lists.of(1,2,3), eval("(reverse [3 2 1])")); + + assertCastError(step("(reverse #{})")); + assertCastError(step("(reverse {:foo :bar})")); + assertCastError(step("(reverse 0x1234)")); + + assertArityError(step("(reverse)")); + assertArityError(step("(reverse 1 2)")); + } + + @Test + public void testAssocNull() { + // nil is treated as an empty map + assertSame(Maps.empty(),eval("(assoc nil)")); + + // assoc promotes nil to maps + assertEquals(Maps.of(1L, 2L), eval("(assoc nil 1 2)")); + assertEquals(Maps.of(1L, 2L, 3L, 4L), eval("(assoc nil 1 2 3 4)")); + } + + @Test + public void testAssocMaps() { + // no key/values is OK + assertEquals(Maps.empty(), eval("(assoc {})")); + + assertEquals(Maps.of(1L, 2L), eval("(assoc {} 1 2)")); + assertEquals(Maps.of(1L, 2L), eval("(assoc {1 2})")); + assertEquals(Maps.of(1L, 2L, 3L, 4L), eval("(assoc {} 1 2 3 4)")); + assertEquals(Maps.of(1L, 2L), eval("(assoc {1 2} 1 2)")); + assertEquals(Maps.of(1L, 2L, 3L, 4L), eval("(assoc {1 2} 3 4)")); + assertEquals(Maps.of(1L, 2L, 3L, 4L, 5L, 6L), eval("(assoc {1 2} 3 4 5 6)")); + + assertArityError(step("(assoc {} 1 2 3)")); + assertArityError(step("(assoc {} 1)")); + + } + + @Test + public void testAssocIn() { + // empty index cases - type of first arg not checked since no idexing happens + assertEquals(2L, evalL("(assoc-in {} [] 2)")); // empty indexes returns value + assertEquals(2L, evalL("(assoc-in nil [] 2)")); // empty indexes returns value + assertEquals(2L, evalL("(assoc-in :old [] 2)")); // empty indexes returns value + assertEquals(2L, evalL("(assoc-in 13 nil 2)")); // empty indexes returns value (nil considered empty seq) + + // map cases + assertEquals(Maps.of(1L,2L), eval("(assoc-in {} [1] 2)")); + assertEquals(Maps.of(1L,2L,3L,4L), eval("(assoc-in {3 4} [1] 2)")); + assertEquals(Maps.of(1L,2L), eval("(assoc-in nil [1] 2)")); + assertEquals(Maps.of(1L,Maps.of(5L,6L),3L,4L), eval("(assoc-in {3 4} [1 5] 6)")); + + // vector cases + assertEquals(Vectors.of(1L, 5L, 3L),eval("(assoc-in [1 2 3] [1] 5)")); + assertEquals(Vectors.of(5L),eval("(assoc-in [1] [0] 5)")); + assertEquals(MapEntry.of(1L, 5L),eval("(assoc-in (first {1 2}) [1] 5)")); + + // Set cases + assertEquals(Sets.of(1L),eval("(assoc-in #{} [1] true)")); + assertEquals(Sets.of(1L),eval("(assoc-in #{1} [1] true)")); + assertEquals(Sets.of(1L,2L),eval("(assoc-in #{1} [2] true)")); + assertArgumentError(step("(assoc-in #{3} [2] :fail)")); // bad value type + assertCastError(step("(assoc-in #{3} [3 2] :fail)")); // 'true' is not a data structure + + // Cast error - wrong key types + assertCastError(step("(assoc-in (blob-map) :foo :bar)")); + + // Cast errors - not associative collections + assertCastError(step("(assoc-in 1 [2] 3)")); + + // Invalid keys + assertArgumentError(step("(assoc-in [1] [:foo] 3)")); + assertArgumentError(step("(assoc-in [] [42] :foo)")); // Issue #119 + + // cast errors - paths not sequences + assertCastError(step("(assoc-in {} #{:a :b} 42)")); // See Issue 95 + assertCastError(step("(assoc-in {} :foo 42)")); // See Issue 95 + + // Arity error + assertArityError(step("(assoc-in)")); + assertArityError(step("(assoc-in nil)")); + assertArityError(step("(assoc-in nil 1)")); + assertArityError(step("(assoc-in nil 1 2 3)")); + assertArityError(step("(assoc-in :bad-struct [1] 2 :blah)")); // ARITY before CAST + } + + @Test + public void testAssocFailures() { + assertCastError(step("(assoc 1 1 2)")); + assertCastError(step("(assoc :foo)")); + + + // assertCastError(step("(assoc #{} :foo true)")); + + // Invalid keys + assertArgumentError(step("(assoc [1 2 3] 1.4 :foo)")); + assertArgumentError(step("(assoc [1 2 3] nil :foo)")); + assertArgumentError(step("(assoc [] 2 :foo)")); + assertArgumentError(step("(assoc (list) 2 :fail)")); + assertArgumentError(step("(assoc (blob-map) 2 :fail)")); + + // Arity error + assertArityError(step("(assoc)")); + assertArityError(step("(assoc nil 1)")); + assertArityError(step("(assoc nil 1 2 3)")); + assertArityError(step("(assoc 1 1)")); // ARITY before CAST + } + + @Test + public void testAssocVectors() { + assertEquals(Vectors.empty(), eval("(assoc [])")); + assertEquals(Vectors.of(2L, 1L), eval("(assoc [1 2] 0 2 1 1)")); + + // Invalid keys + assertArgumentError(step("(assoc [] 1 7)")); + assertArgumentError(step("(assoc [] -1 7)")); + assertArgumentError(step("(assoc [1 2] :a 2)")); + + assertArityError(step("(assoc [] 1 2 3)")); + assertArityError(step("(assoc [] 1)")); + } + + @Test + public void testDissoc() { + assertEquals(Maps.empty(), eval("(dissoc {1 2} 1)")); + assertEquals(Maps.of(1L, 2L), eval("(dissoc {1 2 3 4} 3)")); + assertEquals(Maps.of(1L, 2L), eval("(dissoc {1 2} 3)")); + assertEquals(Maps.of(1L, 2L), eval("(dissoc {1 2})")); + assertEquals(Maps.empty(), eval("(dissoc nil 1)")); + assertEquals(Maps.empty(), eval("(dissoc {1 2 3 4} 1 3)")); + assertEquals(Maps.of(3L, 4L), eval("(dissoc {1 2 3 4} 1 2)")); + + // blob-map dissocs. Regression tests for #140 (fatal error in dissoc with non-blob keys) + assertSame(BlobMap.EMPTY,eval("(dissoc (blob-map) 1)")); + assertEquals(BlobMap.of(Blob.fromHex("a2"), Keywords.FOO),eval("(dissoc (into (blob-map) [[0xa2 :foo] [0xb3 :bar]]) 0xb3)")); + assertEquals(BlobMap.of(Blob.fromHex("a2"), Keywords.FOO),eval("(dissoc (into (blob-map) [[0xa2 :foo]]) :foo)")); + + assertCastError(step("(dissoc 1 1 2)")); + assertCastError(step("(dissoc #{})")); + assertCastError(step("(dissoc [])")); + + assertArityError(step("(dissoc)")); + } + + @Test + public void testContainsKey() { + assertFalse(evalB("(contains-key? {} 1)")); + assertFalse(evalB("(contains-key? {} nil)")); + assertTrue(evalB("(contains-key? {1 2} 1)")); + + assertFalse(evalB("(contains-key? #{} 1)")); + assertFalse(evalB("(contains-key? #{1 2 3} nil)")); + assertFalse(evalB("(contains-key? #{false} true)")); + assertTrue(evalB("(contains-key? #{1 2} 1)")); + assertTrue(evalB("(contains-key? #{nil 2 3} nil)")); + + assertFalse(evalB("(contains-key? [] 1)")); + assertFalse(evalB("(contains-key? [0 1 2] :foo)")); + assertTrue(evalB("(contains-key? [3 4] 1)")); + + assertFalse(evalB("(contains-key? nil 1)")); + + assertArityError(step("(contains-key? 3)")); + assertArityError(step("(contains-key? {} 1 2)")); + assertCastError(step("(contains-key? 3 4)")); + } + + @Test + public void testDisj() { + assertEquals(Sets.of(2L), eval("(disj #{1 2} 1)")); + assertEquals(Sets.of(1L, 2L), eval("(disj #{1 2} 1.0)")); + assertSame(Sets.empty(), eval("(disj #{1} 1)")); + assertSame(Sets.empty(), eval("(reduce disj #{1 2} [1 2])")); + assertEquals(Sets.empty(), eval("(disj #{} 1)")); + assertEquals(Sets.of(1L, 2L, 3L), eval("(disj (set [3 2 1 2 4]) 4)")); + assertEquals(Sets.of(1L), eval("(disj (set [3 2 1 2 4]) 2 3 4)")); + assertEquals(Sets.empty(), eval("(disj #{})")); + + // nil is treated as empty set + assertSame(Sets.empty(), eval("(disj nil 1)")); + assertSame(Sets.empty(), eval("(disj nil nil)")); + + assertCastError(step("(disj [] 1)")); + assertArityError(step("(disj)")); + } + + @Test + public void testSet() { + assertEquals(Sets.of(1L, 2L, 3L), eval("(set [3 2 1 2])")); + assertEquals(Sets.of(1L, 2L, 3L), eval("(set #{1 2 3})")); + + // equivalent of get with set-as-function + assertEquals(eval("(#{2 3} 2)"),eval("(get #{2 3} 2)")); + assertEquals(eval("(#{2 3} 1)"),eval("(get #{2 3} 1)")); + + assertEquals(Sets.empty(), eval("(set nil)")); // nil treated as empty set of elements + + assertArityError(step("(set)")); + assertArityError(step("(set 1 2)")); + + assertCastError(step("(set 1)")); + } + + @SuppressWarnings("unchecked") + @Test + public void testSetRegression153() throws InvalidDataException { + // See issue #153 + Context c=context(); + c=step(c, "(def s1 #{#5477106 \\*})"); + ASet s1=(ASet) c.getResult(); + s1.validate(); + c=step(c, "(def s2 #{#2 #0 true #3 0x61a049 #242411 #3478095 #9275832328719 #1489754187855142})"); + ASet s2=(ASet) c.getResult(); + s2.validate(); + + ASet u1=s2.includeAll(s1); + u1.validate(); + + c=step(c, "(def union1 (union s2 s1))"); + ASet u2=(ASet) c.getResult(); + u2.validate(); + + } + + + + @Test + public void testSubsetQ() { + assertTrue(evalB("(subset? #{} #{})")); + assertTrue(evalB("(subset? #{} #{1 2 3})")); + assertTrue(evalB("(subset? #{2 3} #{1 2 3 4})")); + + // check nil is handled as empty set + assertTrue(evalB("(subset? nil #{})")); + assertTrue(evalB("(subset? #{} nil)")); + assertTrue(evalB("(subset? nil #{1 2 3})")); + assertFalse(evalB("(subset? #{1 2 3} nil)")); + + + assertFalse(evalB("(subset? #{2 3} #{1 2})")); + assertFalse(evalB("(subset? #{1 2 3} #{0})")); + assertFalse(evalB("(subset? #{#{}} #{#{1}})")); + + assertArityError(step("(subset?)")); + assertArityError(step("(subset? 1)")); + assertArityError(step("(subset? 1 2 3)")); + + assertCastError(step("(subset? 1 2)")); + assertCastError(step("(subset? #{} [2])")); + } + + @Test + public void testSetUnion() { + assertEquals(Sets.empty(),eval("(union)")); + + assertEquals(Sets.empty(),eval("(union nil)")); + assertEquals(Sets.empty(),eval("(union #{})")); + assertEquals(Sets.of(1L,2L),eval("(union #{1 2})")); + + // nil treated as empty set in all cases + assertEquals(Sets.of(1L,2L),eval("(union nil #{1 2})")); + assertEquals(Sets.of(1L,2L),eval("(union #{1 2} nil)")); + + assertEquals(Sets.of(1L,2L,3L),eval("(union #{1 2} #{3})")); + + assertEquals(Sets.of(1L,2L,3L,4L,5L),eval("(union #{1 2} #{3} #{4 5})")); + + assertCastError(step("(union :foo)")); + assertCastError(step("(union [1] [2 3])")); + } + + @Test + public void testSetIntersection() { + assertEquals(Sets.empty(),eval("(intersection nil)")); + assertEquals(Sets.empty(),eval("(intersection #{})")); + assertEquals(Sets.of(1L,2L),eval("(intersection #{1 2})")); + + assertEquals(Sets.empty(),eval("(intersection #{1 2} #{3})")); + assertEquals(Sets.empty(),eval("(intersection #{1 2 3} nil)")); + + assertEquals(Sets.of(2L,3L),eval("(intersection #{1 2 3} #{2 3 4})")); + + assertEquals(Sets.of(3L),eval("(intersection #{1 2 3} #{2 3 4} #{3 4 5})")); + + assertArityError(step("(intersection)")); + + assertCastError(step("(intersection :foo)")); + assertCastError(step("(intersection [1] [2 3])")); + } + + @Test + public void testSetDifference() { + assertEquals(Sets.empty(),eval("(difference nil)")); + assertEquals(Sets.empty(),eval("(difference #{})")); + assertEquals(Sets.of(1L,2L),eval("(difference #{1 2})")); + + assertEquals(Sets.of(1L,2L),eval("(difference #{1 2} #{3})")); + + assertEquals(Sets.of(2L,3L),eval("(difference #{1 2 3} #{1 4})")); + + assertEquals(Sets.of(3L),eval("(difference #{1 2 3} #{2 4} #{1 5})")); + + assertArityError(step("(difference)")); + + assertCastError(step("(difference :foo)")); + assertCastError(step("(difference [1] [2 3])")); + } + + @Test + public void testDifferenceRegression155() { + Context c=step("(do (def arg+ [#{nil #5 #4 #2 #0 #7 #6 #3 #1} #{nil 0x500360a6 :B2Qrb9d1U5WH00h6c \"1pC\" true \\Ñ (quote A/aHAb7K2) #5278509802049781 #515}]) (def u (apply union arg+)) (def d (apply difference arg+)) (= #{} (difference d u)) )"); + assertEquals(CVMBool.TRUE,c.getResult()); + } + + + @Test + public void testFirst() { + assertEquals(1L, evalL("(first [1 2])")); + assertEquals(1L, evalL("(first '(1 2 3 4))")); + + assertBoundsError(step("(first [])")); + assertBoundsError(step("(first nil)")); + + assertArityError(step("(first)")); + assertArityError(step("(first [1] 2)")); + assertCastError(step("(first 1)")); + assertCastError(step("(first :foo)")); + } + + @Test + public void testSecond() { + assertEquals(2L, evalL("(second [1 2])")); + + assertBoundsError(step("(second [2])")); + assertBoundsError(step("(second nil)")); + + assertArityError(step("(second)")); + assertArityError(step("(second [1] 2)")); + assertCastError(step("(second 1)")); + } + + @Test + public void testLast() { + assertEquals(2L, evalL("(last [1 2])")); + assertEquals(4L, evalL("(last [4])")); + + assertBoundsError(step("(last [])")); + assertBoundsError(step("(last nil)")); + + assertArityError(step("(last)")); + assertArityError(step("(last [1] 2)")); + assertCastError(step("(last 1)")); + } + + @Test + public void testNext() { + assertEquals(Vectors.of(2L, 3L), eval("(next [1 2 3])")); + assertEquals(Lists.of(2L, 3L), eval("(next '(1 2 3))")); + + assertNull(eval("(next nil)")); + assertNull(eval("(next [1])")); + assertNull(eval("(next {1 2})")); + + assertArityError(step("(next)")); + assertArityError(step("(next [1] [2 3])")); + + assertCastError(step("(next 1)")); + assertCastError(step("(next :foo)")); + } + + @Test + public void testConj() { + assertEquals(Vectors.of(1L, 2L, 3L), eval("(conj [1 2] 3)")); + assertEquals(Lists.of(3L, 1L, 2L), eval("(conj (list 1 2) 3)")); + + // nil works like empty vector + assertEquals(Vectors.of(1L), eval("(conj nil 1)")); + assertEquals(Vectors.of(3L), eval("(conj nil 3)")); + assertEquals(Sets.of(3L), eval("(conj #{} 3)")); + assertEquals(Sets.of(3L), eval("(conj #{3} 3)")); + + // Maps conj with map entry vectors + assertEquals(Maps.of(1L, 2L), eval("(conj {} [1 2])")); + assertEquals(Maps.of(1L, 2L, 5L, 6L), eval("(conj {1 3 5 6} [1 2])")); + + assertEquals(Lists.of(1L), eval("(conj (list) 1)")); + assertEquals(Lists.of(1L, 2L), eval("(conj (list 2) 1)")); + assertEquals(Sets.of(1L, 2L, 3L), eval("(conj #{2 3} 1)")); + assertEquals(Sets.of(1L, 2L, 3L), eval("(conj #{2 3 1} 1)")); + + // arity 1 OK, no change + assertEquals(Vectors.of(1L, 2L), eval("(conj [1 2])")); + + // Blobmaps + assertEquals(BlobMaps.create(Blob.fromHex("a1"), Blob.fromHex("a2")),eval("(conj (blob-map) [0xa1 0xa2])")); + + // bad data structures + assertCastError(step("(conj :foo)")); + assertCastError(step("(conj :foo 1)")); + + // bad types of elements + assertArgumentError(step("(conj {} 2)")); // can't cast long to a map entry + assertArgumentError(step("(conj {} [1 2 3])")); // wrong size vector for a map entry + assertArgumentError(step("(conj {} '(1 2))")); // wrong type for a map entry + assertArgumentError(step("(conj (blob-map) [:foo 0xa2])")); // bad key type for blobmap + + assertCastError(step("(conj 1 2)")); + assertCastError(step("(conj (str :foo) 2)")); + + assertArityError(step("(conj)")); + } + + @Test + public void testCons() { + assertEquals(Lists.of(3L, 1L, 2L), eval("(cons 3 (list 1 2))")); + assertEquals(Lists.of(3L, 1L, 2L), eval("(cons 3 [1 2])")); + assertEquals(Lists.of(3L, 1L, 2L), eval("(cons 3 1 [2])")); + + assertEquals(Lists.of(3L), eval("(cons 3 nil)")); + assertEquals(Lists.of(1L, 3L), eval("(cons 1 #{3})")); + assertEquals(Lists.of(1L), eval("(cons 1 [])")); + + assertCastError(step("(cons 1 2)")); + + assertArityError(step("(cons [])")); + assertArityError(step("(cons 1)")); + assertArityError(step("(cons)")); + + assertCastError(step("(cons 1 2 3 4 5)")); + } + + @Test + public void testComp() { + assertEquals(43L, evalL("((comp inc) 42)")); + assertEquals(44L, evalL("((comp inc inc) 42)")); + assertEquals(45L, evalL("((comp inc inc inc) 42)")); + assertEquals(46L, evalL("((comp inc inc inc inc) 42)")); + + assertEquals(3.0, evalD("((comp sqrt +) 4 5)")); + + assertArityError(step("(comp)")); + + } + + @Test + public void testInto() { + // nil as data structure + assertNull(eval("(into nil nil)")); + assertEquals(Maps.of(1L,2L),eval("(into nil {1 2})")); + assertEquals(Vectors.empty(), eval("(into nil [])")); + assertEquals(Vectors.of(1L, 2L, 3L), eval("(into nil [1 2 3])")); + + assertEquals(Vectors.of(1L, 2L, 3L), eval("(into [1 2] [3])")); + assertEquals(Vectors.of(1L, 2L, 3L), eval("(into [1 2 3] nil)")); + assertEquals(Vectors.of(1L, 2L, 3L), eval("(into nil [1 2 3])")); + + assertEquals(Lists.of(2L, 1L, 3L, 4L), eval("(into '(3 4) '(1 2))")); + + assertEquals(Sets.of(1L, 2L, 3L), eval("(into #{} [1 2 1 2 3])")); + + // map as data structure + assertEquals(Maps.empty(), eval("(into {} [])")); + assertEquals(Maps.of(1L, 2L, 3L, 4L), eval("(into {} [[1 2] [3 4] [1 2]])")); + + assertEquals(Vectors.of(MapEntry.of(1L, 2L)), eval("(into [] {1 2})")); + + assertCastError(step("(into 1 [2 3])")); // long is not a conjable data structure + assertCastError(step("(into nil :foo)")); // keyword is not a sequence of elements + + // See #151 + assertCastError(step("(into (list) #0)")); // Address is not a sequential data structure + assertCastError(step("(into #0 [])")); // Address is not a conjable data structure + assertCastError(step("(into #0 [1 2])")); // Address is not a conjable data structure + assertCastError(step("(into 0 [])")); // Long is not a conjable data structure + + // bad element types + assertArgumentError(step("(into {} [nil])")); // nil is not a MapEntry + assertArgumentError(step("(into {} [[:foo]])")); // length 1 vector shouldn't convert to MapEntry + assertArgumentError(step("(into {} [[:foo :bar :baz]])")); // length 1 vector shouldn't convert to MapEntry + assertArgumentError(step("(into {1 2} [2 3])")); // longs are not map entries + assertArgumentError(step("(into {1 2} [[] []])")); // empty vectors are not map entries + assertArgumentError(step("(into (blob-map) [[1 2]])")); + + assertArityError(step("(into)")); + assertArityError(step("(into inc)")); + assertArityError(step("(into 1 2 3)")); // arity > cast + } + + @Test + public void testMerge() { + assertEquals(Maps.empty(),eval("(merge)")); + assertEquals(Maps.empty(),eval("(merge nil)")); + assertEquals(Maps.empty(),eval("(merge nil nil)")); + + assertEquals(Maps.of(1L,2L,3L,4L),eval("(merge {1 2} {3 4})")); + assertEquals(Maps.of(1L,2L,3L,4L),eval("(merge {1 2 3 4} {})")); + assertEquals(Maps.of(1L,2L,3L,4L),eval("(merge nil {1 2 3 4})")); + + assertEquals(Maps.of(1L,3L),eval("(merge {1 2} {1 3})")); + assertEquals(Maps.of(1L,3L),eval("(merge nil {1 2} nil {1 3} nil)")); + + assertCastError(step("(merge [])")); + assertCastError(step("(merge {} [1 2 3])")); + assertCastError(step("(merge nil :foo)")); + } + + @Test + public void testDotimes() { + assertEquals(Vectors.of(0L, 1L, 2L), eval("(do (def a []) (dotimes [i 3] (def a (conj a i))) a)")); + assertEquals(Vectors.empty(), eval("(do (def a []) (dotimes [i 0] (def a (conj a i))) a)")); + assertEquals(Vectors.empty(), eval("(do (def a []) (dotimes [i -1.5] (def a (conj a i))) a)")); + assertEquals(Vectors.empty(), eval("(do (def a []) (dotimes [i -1.5]) a)")); + + assertCastError(step("(dotimes [1 10])")); + assertCastError(step("(dotimes [i :foo])")); + assertCastError(step("(dotimes [:foo 10])")); + assertCastError(step("(dotimes :foo)")); + + assertArityError(step("(dotimes)")); + assertArityError(step("(dotimes [i])")); + assertArityError(step("(dotimes [i 2 3])")); + + } + + @Test + public void testDoouble() { + assertEquals(-13.0,evalD("(double -13)")); + assertEquals(1.0,evalD("(double true)")); // ?? cast OK? + + assertEquals(255.0,evalD("(double (byte -1))")); // byte should be 0-255 + + + assertCastError(step("(double :foo)")); + + assertArityError(step("(double)")); + assertArityError(step("(double :foo :bar)")); + } + + @Test + public void testMap() { + assertEquals(Vectors.of(2L, 3L), eval("(map inc [1 2])")); + assertEquals(Vectors.of(2L, 3L), eval("(map inc '(1 2))")); // TODO is this right? + assertEquals(Vectors.empty(), eval("(map inc nil)")); // TODO is this right? + assertEquals(Vectors.of(4L, 6L), eval("(map + [1 2] [3 4 5])")); + assertEquals(Vectors.of(3L), eval("(map + [1 2 3] [2])")); + assertEquals(Vectors.of(1L, 2L, 3L), eval("(map identity [1 2 3])")); + + assertCastError(step("(map 1 [1])")); + assertCastError(step("(map 1 [] [] [])")); + assertCastError(step("(map inc 1)")); + + assertArityError(step("(map)")); + assertArityError(step("(map inc)")); + assertArityError(step("(map 1)")); // arity > cast + } + + @Test + public void testFor() { + assertEquals(Vectors.empty(), eval("(for [x nil] (inc x))")); + assertEquals(Vectors.empty(), eval("(for [x []] (inc x))")); + assertEquals(Vectors.of(2L,3L), eval("(for [x '(1 2)] (inc x))")); + assertEquals(Vectors.of(2L,3L), eval("(for [x [1 2]] (inc x))")); + + // TODO: maybe dubious error types? + + assertCastError(step("(for 1 1)")); // bad binding form + assertArityError(step("(for [x] 1)")); // bad binding form + assertArityError(step("(for [x [1 2] [2 3]] 1)")); // bad binding form length + assertCastError(step("(for [x :foo] 1)")); // bad sequence + + assertArityError(step("(for)")); + assertArityError(step("(for [] nil nil)")); + assertCastError(step("(for 1)")); // arity > cast + } + + @Test + public void testMapv() { + assertEquals(Vectors.empty(), eval("(map inc nil)")); + assertEquals(Vectors.of(2L, 3L), eval("(mapv inc [1 2])")); + assertEquals(Vectors.of(4L, 6L), eval("(mapv + '(1 2) '(3 4 5))")); + + assertArityError(step("(mapv)")); + assertArityError(step("(mapv inc)")); + } + + @Test + public void testFilter() { + assertEquals(Vectors.of(1,2,3), eval("(filter number? [1 :foo 2 :bar 3])")); + assertEquals(Lists.of(Keywords.FOO), eval("(filter #{:foo} '(:foo 2 3))")); + assertNull(eval("(filter keyword? nil)")); + assertEquals(Maps.empty(), eval("(filter nil? {1 2 3 4})")); + assertEquals(Maps.of(Keywords.FOO,1), eval("(filter (fn [[k v]] (keyword? k)) {:foo 1 'bar 2})")); + assertEquals(Sets.of(1,2,3), eval("(filter number? #{1 2 3 :foo})")); + + assertCastError(step("(filter nil? 1)")); + assertCastError(step("(filter 1 [1 2 3])")); + + assertArityError(step("(filter +)")); + assertArityError(step("(filter 1 2 3)")); + } + + @Test + public void testLang() { + { + Context ctx=context(); + ctx=step(ctx,"(def *lang* (fn [x] x))"); + assertEquals(Symbols.COUNT,eval(ctx,"count")); + } + } + + @Test + public void testReduce() { + assertEquals(24L, evalL("(reduce * 1 [1 2 3 4])")); + assertEquals(2L, evalL("(reduce + 2 [])")); + assertEquals(2L, evalL("(reduce + 2 nil)")); + + // add values, indexing into map entries as vectors + assertEquals(10.0, evalD("(reduce (fn [acc me] (+ acc (me 1))) 0.0 {:a 1, :b 2, 107 3, nil 4})")); + // reduce over map, destructuring keys and values + assertEquals(100.0, evalD( + "(reduce (fn [acc [k v]] (let [x (double (v nil))] (+ acc (* x x)))) 0.0 {true {nil 10}})")); + + assertEquals(Lists.of(3,2,1), eval("(reduce conj '() '(1 2 3))")); + + // 2-arg reduce forms + assertEquals(24L, evalL("(reduce * [1 2 3 4])")); + assertEquals(1L, evalL("(reduce * nil)")); + assertEquals(1L, evalL("(reduce + [1])")); + assertEquals(0L, evalL("(reduce + [])")); + assertEquals(Keywords.FOO, eval("(reduce (fn [] :foo) [])")); // 0 arity + assertEquals(Keywords.FOO, eval("(reduce (fn [v] :foo) [:bar])")); // 1 arity + assertEquals(Keywords.FOO, eval("(reduce (fn [a b] :foo) [:bar :baz])")); // 2 arity + + // Errors in reduce function + assertCastError(step("(reduce + [:foo])")); + assertCastError(step("(reduce + 1 [:foo])")); + + assertCastError(step("(reduce 1 2 [])")); + assertCastError(step("(reduce + 2 :foo)")); + assertCastError(step("(reduce + 1)")); + + assertArityError(step("(reduce +)")); + assertArityError(step("(reduce + 1 [2] [3])")); + } + + @Test public void testReduceFail() { + // shouldn't fail because function never called + assertEquals(2L,evalL("(reduce address 2 [])")); + + assertArityError(step("(reduce address 2 [:foo :bar])")); + assertCastError(step("(reduce (fn [a x] (address x)) 2 [:foo :bar])")); + } + + @Test + public void testReduced() { + assertEquals(Vectors.of(2L,3L), eval("(reduce (fn [i v] (if (== v 3) (reduced [i v]) v)) 1 [1 2 3 4 5])")); + + // 2 arg reduce + assertEquals(Vectors.of(2L,3L), eval("(reduce (fn [i v] (if (== v 3) (reduced [i v]) v)) [1 2 3 4 5])")); + assertEquals(CVMLong.create(5L), eval("(reduce (fn [a b] (if (== b 1) (reduced :foo) b)) [1 2 3 4 5])")); // b is never 1 + assertEquals(CVMLong.create(1L), eval("(reduce (fn [v] (reduced v)) [1])")); // fn called with arity 1 + assertEquals(Keywords.FOO, eval("(reduce (fn [] (reduced :foo)) [])")); // fn called with arity 0 + + + assertArityError(step("(reduced)")); + assertArityError(step("(reduced 1 2)")); + + // reduced on its own is an :EXCEPTION Error + assertError(ErrorCodes.EXCEPTION,step("(reduced 1)")); + + // reduced cannot escape actor call boundary + { + Context ctx=context(); + ctx=step(ctx,"(def act (deploy `(do (defn foo ^{:callable? true} [] (reduced 1)))))"); + ctx=step(ctx,"(reduce (fn [_ _] (call act (foo))) nil [nil])"); + assertError(ErrorCodes.EXCEPTION,ctx); + } + + // reduced can escape nested function call + { + Context ctx=context(); + ctx=step(ctx,"(defn foo [x] (reduced x))"); + ctx=step(ctx,"(reduce (fn [i x] (foo x)) nil [1 2])"); + assertCVMEquals(1L,ctx.getResult()); + } + + } + + @Test + public void testReturn() { + // basic return mechanics + assertEquals(1L,evalL("(return 1)")); + + assertEquals(Vectors.empty(), eval("(let [f (fn [x] (+ 1 (return x)))] (f []))")); // return in function body + assertEquals(Vectors.empty(), eval("(let [f (fn [x] (let [a (return x)] 2))] (f []))")); // return in let + // binding + assertEquals(Vectors.empty(), eval("(let [f (fn [x] (let [a 2] (return x) a))] (f []))")); // return in let body + assertEquals(Vectors.empty(), eval("(let [f (fn [x] (do (return x) 2))] (f []))")); // return in do + assertEquals(Vectors.empty(), eval("(let [f (fn [x] (return (return x)))] (f []))")); // nested returns + assertEquals(Vectors.empty(), eval("(let [f (fn [x] ((return x) 2 3))] (f []))")); // return in function call + // position + assertEquals(Vectors.empty(), eval("(let [f (fn [x] (= 2 (return x) 3))] (f []))")); // return in function arg + assertEquals(Vectors.empty(), eval("(let [f (fn [x] (if (return x) 2 3))] (f []))")); // return in cond test + assertEquals(Vectors.empty(), eval("(let [f (fn [x] (if true (return x) 3))] (f []))")); // return in cond + // result + assertEquals(Vectors.empty(), eval("(let [f (fn [x] (if true [] (return x)))] (f 3))")); // return in cond + // default + assertArityError(step("(return)")); + assertArityError(step("(return 1 2)")); + } + + @Test + public void testLoop() { + assertNull(eval("(loop [])")); + assertEquals(Keywords.FOO,eval("(loop [] :foo)")); + assertEquals(Keywords.FOO,eval("(loop [] 1 2 3 :foo)")); + assertEquals(Keywords.FOO,eval("(loop [a :foo] :bar a)")); + + assertCompileError(step("(loop [a])")); + assertCompileError(step("(loop)")); // Issue #80 + assertCompileError(step("(loop :foo)")); // Not a binting vector + } + + @Test + public void testRecordLookup() { + assertEquals(INITIAL.getAccounts(),eval("(*state* :accounts)")); + assertEquals(Keywords.FOO,eval("(*state* [ 982788 ] :foo )")); + assertNull(eval("(*state* [1 2 3])")); // Issue #85 + + assertArityError(step("(*state* :accounts :foo :bar)")); + } + + @Test + public void testRecur() { + // test factorial with accumulator + assertEquals(120L, evalL("(let [f (fn [a x] (if (> x 1) (recur (* a x) (dec x)) a))] (f 1 5))")); + + assertArityError(step("(let [f (fn [x] (recur x x))] (f 1))")); + assertJuiceError(step("(let [f (fn [x] (recur x))] (f 1))")); + + // should hit depth limits before running out of juice + // TODO: think about letrec? + assertDepthError(step("(do (def f (fn [x] (recur (f x)))) (f 1))")); + + // Recur on its own is an :EXCEPTION Error + assertError(ErrorCodes.EXCEPTION,step("(recur 1)")); + } + + @Test + public void testRecurMultiFn() { + // test function that should exit on recur with value 13 + Context ctx=step("(defn f ([] 13) ([a] (inc (recur))))"); + + assertEquals(13L,evalL(ctx,"(f)")); + assertEquals(13L,evalL(ctx,"(f :foo)")); + assertArityError(step(ctx,"(f 1 2)")); + } + + @Test + public void testTailcall() { + assertEquals(Keywords.FOO,eval("(do (defn f [x] :foo) (defn g [] (tailcall (f 1))) (g))")); + assertEquals(RT.cvm(3L),eval("((fn [] (tailcall (+ 1 2))))")); + + // Undeclared function in tailcall + assertUndeclaredError(step("(do (defn f [x] :foo) (defn g [] (tailcall (h 1))) (g))")); + + // Arity error in tailcall + assertArityError(step("(do (defn g [] :foo) (defn f [x] (tailcall (g 1))) (f 1))")); + + // check we aren't consuming stack, should fail with :JUICE not :DEPTH + assertJuiceError(step("(do (def f (fn [x] (tailcall (f x)))) (f 1))")); + + // tailcall on its own is an :EXCEPTION Error + assertError(ErrorCodes.EXCEPTION,step("(tailcall (count 1))")); + } + + @Test + public void testHalt() { + assertEquals(1L, evalL("(do (halt 1) (assert false))")); + assertNull(eval("(do (halt) (assert false))")); + + // halt should not roll back state changes + { + Context ctx = step("(do (def a 13) (halt 2))"); + assertCVMEquals(2L, ctx.getResult()); + assertEquals(13L, evalL(ctx, "a")); + } + + // Halt should return from a smart contract call but still have state changes + { + Context ctx=step("(def act (deploy '(do (def g :foo) (defn f ^{:callable? true} [] (def g 3) (halt 2) 1))))"); + assertTrue(ctx.getResult() instanceof Address); + assertEquals(Keywords.FOO, eval(ctx,"(lookup act g)")); // initial value of g + ctx=step(ctx,"(call act (f))"); + assertCVMEquals(2L, ctx.getResult()); // halt value returned + assertCVMEquals(3L, eval(ctx,"(lookup act g)")); // g has been updated + } + + assertArityError(step("(halt 1 2)")); + } + + @Test + public void testFail() { + assertAssertError(step("(fail)")); + assertAssertError(step("(fail \"Foo\")")); + assertAssertError(step("(fail :ASSERT \"Foo\")")); + assertAssertError(step("(fail :foo)")); + + assertError(step("(fail 1 :bar)")); + assertCastError(step("(fail :CAST :bar)")); + + + assertAssertError(step("(fail)")); + + { // need to double-step this: can't define macro and use it in the same expression? + Context ctx=step("(defmacro check [condition reaction] `(if (not ~condition) ~reaction))"); + assertAssertError(step(ctx,"(check (= (+ 2 2) 5) (fail \"Laws of arithmetic violated\"))")); + } + + // cannot have null error code + assertArgumentError(step("(fail nil \"Hello\")")); + + assertArityError(step("(fail 1 \"Message\" 3)")); + } + + @Test + public void testFailContract() { + Context ctx=step("(def act (deploy '(do (defn set-and-fail ^{:callable? true} [x] (def foo x) (fail :NOPE (str x))))))"); + Address act=(Address) ctx.getResult(); + assertNotNull(act); + + ctx=step(ctx,"(call act (set-and-fail 100))"); + assertError(Keyword.create("NOPE"),ctx); + + // Foo shouldn't be defined + assertNull(ctx.getAccountStatus(act).getEnvironmentValue(Symbols.FOO)); + } + + @Test + public void testRollback() { + assertEquals(1L, evalL("(do (rollback 1) (assert false))")); + assertEquals(1L, evalL("(do (def a 1) (rollback a) (assert false))")); + + // rollback should roll back state changes + Context ctx = step("(def a 17)"); + ctx = step(ctx, "(do (def a 13) (rollback 2))"); + assertEquals(17L, evalL(ctx, "a")); + } + + @Test + public void testWhen() { + assertNull(eval("(when false 2)")); + assertNull(eval("(when true)")); + assertEquals(Vectors.empty(), eval("(when 2 3 4 5 [])")); + + // TODO: needs to fix / check? + assertArityError(step("(when)")); + } + + @Test + public void testIfLet() { + assertEquals(1L,evalL("(if-let [a 1] a)")); + assertEquals(2L,evalL("(if-let [a true] 2)")); + assertEquals(3L,evalL("(if-let [a []] 3 4)")); + assertEquals(4L,evalL("(if-let [a nil] 3 4)")); + assertEquals(5L,evalL("(if-let [a false] 3 5)")); + + // TODO: fix destructuring examples + //assertEquals(Vectors.of(2L,1L),eval("(if-let [[a b] [1 2]] [b a])")); + //assertNull(eval("(if-let [[a b] nil] [b a])")); + + assertNull(eval("(if-let [a false] 1)")); // null on false branch + + assertArityError(step("(if-let [:foo 1])")); + + // TODO: needs to fix / check? + assertArityError(step("(if-let [a true])")); // no branches + assertArityError(step("(if-let [a true] 1 2 3)")); // too many branches + assertArityError(step("(if-let)")); + assertArityError(step("(if-let [])")); + assertArityError(step("(if-let [foo] 1)")); + } + + @Test + public void testWhenLet() { + assertEquals(1L,evalL("(when-let [a 1] a)")); + assertEquals(2L,evalL("(when-let [a true] 2)")); + assertEquals(3L,evalL("(when-let [a 1] 2 3)")); + + assertNull(eval("(when-let [a true])")); // empty trye branch + assertNull(eval("(when-let [a false] 1)")); // null on false branch + assertNull(eval("(when-let [a false])")); // null on false branch + + assertCompileError(step("(when-let [:foo 1])")); + + // TODO: needs to fix / check? + assertArityError(step("(when-let)")); + assertArityError(step("(when-let [])")); + assertArityError(step("(when-let [foo] 1)")); + } + + @Test + public void testKeyword() { + assertEquals(Keywords.STATE, eval("(keyword 'state)")); + assertEquals(Keywords.STATE, eval("(keyword (name :state))")); + assertEquals(Keywords.STATE, eval("(keyword (str 'state))")); + assertEquals(Keywords.STATE, eval("(keyword 'state)")); + + // keyword lookups + assertNull(eval("((keyword :foo) nil)")); + + assertArgumentError(step("(keyword (str))")); // too short + + assertCastError(step("(keyword nil)")); + assertCastError(step("(keyword 1)")); + assertCastError(step("(keyword [])")); + + assertArityError(step("(keyword)")); + assertArityError(step("(keyword 1 3)")); + } + + @Test + public void testKeywordAsFunction() { + // lookups in maps + assertNull(eval("(:foo {})")); + assertNull(eval("(:foo {:bar 1} nil)")); + assertEquals(1L,evalL("(:foo {} 1)")); + assertEquals(1L,evalL("(:foo {:foo 1} 2)")); + + // lookups in sets + assertSame(CVMBool.FALSE,eval("(:foo #{})")); + assertEquals(1L,evalL("(:foo #{} 1)")); + assertSame(CVMBool.TRUE,eval("(:foo #{:foo})")); + assertNull(eval("(:foo #{:bar} nil)")); + assertSame(CVMBool.TRUE,eval("(:foo #{:foo} 2)")); + + // lookups in vectors + assertNull(eval("(:foo [])")); + assertNull(eval("(:foo [] nil)")); + assertEquals(1L,evalL("(:foo [1 2 3] 1)")); + + // lookups on nil + assertNull(eval("((keyword :foo) nil)")); + assertNull(eval("(:foo nil)")); + assertEquals(1L,evalL("(:foo nil 1)")); + } + + @Test + public void testName() { + assertEquals("count", evalS("(name :count)")); + assertEquals("count", evalS("(name 'count)")); + assertEquals("foo", evalS("(name \"foo\")")); + + // should extract symbol name + assertEquals("bar", evalS("(name 'bar)")); + + // longer strings OK for name + assertEquals("duicgidvgefiucefiuvfeiuvefiuvgifegvfuievgiuefgviuefgviufegvieufgviuefvgevevgi", evalS("(name \"duicgidvgefiucefiuvfeiuvefiuvgifegvfuievgiuefgviuefgviufegvieufgviuefvgevevgi\")")); + + assertCastError(step("(name nil)")); + assertCastError(step("(name [])")); + assertCastError(step("(name 12)")); + + assertArityError(step("(name)")); + assertArityError(step("(name 1 3)")); + } + + @Test + public void testSymbol() { + assertEquals(Symbols.COUNT, eval("(symbol :count)")); + assertEquals(Symbols.COUNT, eval("(symbol (name 'count))")); + assertEquals(Symbols.COUNT, eval("(symbol (str 'count))")); + assertEquals(Symbols.COUNT, eval("(symbol (name :count))")); + assertEquals(Symbols.COUNT, eval("(symbol (name \"count\"))")); + + // too short or too long results in ARGUMENT error + assertArgumentError(step("(symbol (str))")); + assertArgumentError( + step("(symbol \"duicgidvgefiucefiuvfeiuvefiuvgifegvfuievgiuefgviuefgviufegvieufgviuefvgevevgi\")")); + + assertCastError(step("(symbol nil)")); + assertCastError(step("(symbol [])")); + + assertArityError(step("(symbol)")); + assertArityError(step("(symbol 1 2 3)")); + } + + @Test + public void testImport() { + Context
ctx = step("(def lib (deploy '(do (def foo 100))))"); + Address libAddress=ctx.getResult(); + + { // tests with a typical import + Context ctx2=step(ctx,"(import ~lib :as mylib)"); + assertEquals(libAddress,ctx2.getResult()); + + assertEquals(100L, evalL(ctx2, "mylib/foo")); + assertUndeclaredError(step(ctx2, "mylib/bar")); + assertTrue(evalB(ctx2,"(map? (lookup-meta mylib 'foo))")); + } + + { // test deploy and CNS import in a single form. See #107 + Context ctx2=step(ctx,"(do (let [addr (deploy nil)] (call *registry* (cns-update 'foo addr)) (import foo :as foo2)))"); + assertNotError(ctx2); + } + + assertArityError(step(ctx,"(import)")); + assertArityError(step(ctx,"(import ~lib)")); + assertArityError(step(ctx,"(import ~lib :as)")); + assertArityError(step(ctx,"(import ~lib :as mylib :blah)")); + } + + @Test + public void testImportCore() { + Context ctx = step("(import convex.core :as cc)"); + assertNotError(ctx); + assertEquals(eval(ctx,"count"),eval(ctx,"cc/count")); + } + + + @Test + public void testLookup() { + assertSame(Core.COUNT, eval("(lookup count)")); + assertSame(Core.COUNT, eval("(lookup *address* count)")); + assertSame(Core.COUNT, eval("(lookup "+Init.CORE_ADDRESS+" count)")); + + // Lookups after def + assertEquals(1L,evalL("(do (def foo 1) (lookup foo))")); + assertEquals(1L,evalL("(do (def foo 1) (lookup *address* foo))")); + + // UNDECLARED if not declared + assertUndeclaredError(step("(lookup non-existent-symbol)")); + + // NOBODY for lookups in non-existent environment + assertNobodyError(step("(lookup #77777777 count)")); + assertNobodyError(step("(do (def foo 1) (lookup #66666666 foo))")); + + // COMPILE Errors for bad symbols + assertCompileError(step("(lookup :count)")); + assertCompileError(step("(lookup \"count\")")); + assertCompileError(step("(lookup :non-existent-symbol)")); + assertCompileError( + step("(lookup \"cdiubcidciuecgieufgvuifeviufegviufeviuefbviufegviufevguiefvgfiuevgeufigv\")")); + assertCompileError(step("(lookup nil)")); + assertCompileError(step("(lookup 10)")); + assertCompileError(step("(lookup [])")); + + // CAST Errors for bad Addresses + assertCastError(step("(lookup 8 count)")); + assertCastError(step("(lookup :foo count)")); + + assertArityError(step("(lookup)")); + assertArityError(step("(lookup 1 2 3)")); + } + + @Test + public void testLookupSyntax() { + AHashMap countMeta=Core.METADATA.get(Symbols.COUNT); + assertSame(countMeta, (eval("(lookup-meta 'count)"))); + assertSame(countMeta, (eval("(lookup-meta "+Init.CORE_ADDRESS+ " 'count)"))); + + assertNull(eval("(lookup-meta 'non-existent-symbol)")); + assertNull(eval("(lookup-meta #666666 'count)")); // invalid address + + assertSame(Maps.empty(),eval("(do (def foo 1) (lookup-meta 'foo))")); + assertSame(Maps.empty(),eval("(do (def foo 1) (lookup-meta *address* 'foo))")); + assertNull(eval("(do (def foo 1) (lookup-meta #0 'foo))")); + + // invalid name string (too long) + assertCastError( + step("(lookup-meta \"cdiubcidciuecgieufgvuifeviufegviufeviuefbviufegviufevguiefvgfiuevgeufigv\")")); + + // bad symbols + assertCastError(step("(lookup-meta count)")); + assertCastError(step("(lookup-meta nil)")); + assertCastError(step("(lookup-meta 10)")); + assertCastError(step("(lookup-meta [])")); + + // Bad addresses + assertCastError(step("(lookup-meta :foo 'bar)")); + assertCastError(step("(lookup-meta 8 'count)")); + + assertArityError(step("(lookup-meta)")); + assertArityError(step("(lookup-meta 1 2 3)")); + } + + @Test + public void testEmpty() { + assertNull(eval("(empty nil)")); + assertSame(Lists.empty(), eval("(empty (list 1 2))")); + assertSame(Maps.empty(), eval("(empty {1 2 3 4})")); + assertSame(Vectors.empty(), eval("(empty [1 2 3])")); + assertSame(Sets.empty(), eval("(empty #{1 2})")); + + assertCastError(step("(empty 1)")); + assertCastError(step("(empty :foo)")); + assertArityError(step("(empty)")); + assertArityError(step("(empty [1] [2])")); + } + + @Test + public void testMapAsFunction() { + assertEquals(1L, evalL("({2 1 1 2} 2)")); + assertNull(eval("({2 1 1 2} 3)")); + assertNull(eval("({} 3)")); + + // fall-through behaviour + assertEquals(10L, evalL("({2 1 1 2} 5 10)")); + assertNull(eval("({} 1 nil)")); + + // bad arity + assertArityError(step("({})")); + assertArityError(step("({} 1 2 3 4)")); + } + + @Test + public void testVectorAsFunction() { + assertEquals(5L, evalL("([1 3 5 7] 2)")); + + assertEquals(5L, evalL("([1 3 5 7] (byte 2))")); // TODO: is this sane? Implicit cast to Long is OK? + + // bounds checks get applied + assertBoundsError(step("([] 0)")); + assertBoundsError(step("([1 2 3] -1)")); + assertBoundsError(step("([1 2 3] 3)")); + + // Bad index types + assertCastError(step("([] nil)")); + assertCastError(step("([] :foo)")); + + // bad arity + assertArityError(step("([])")); + assertArityError(step("([] 1 2 3 4)")); + } + + @Test + public void testListAsFunction() { + assertEquals(5L, evalL("('(1 3 5 7) 2)")); + + // bounds checks get applied + assertBoundsError(step("(() 0)")); + assertBoundsError(step("('(1 2 3) -1)")); + assertBoundsError(step("('(1 2 3) 3)")); + + // cast error + assertCastError(step("(() nil)")); + assertCastError(step("(() :foo)")); + assertCastError(step("(() {})")); + + + // bad arity + assertArityError(step("(())")); + assertArityError(step("(() 1 2 3 4)")); + } + + @Test + public void testApply() { + // Basic data structure application + assertSame(Maps.empty(),eval("(apply assoc [nil])")); + assertSame(Vectors.empty(), eval("(apply vector ())")); + assertSame(BlobMaps.empty(), eval("(apply blob-map ())")); + assertSame(Lists.empty(), eval("(apply list [])")); + + assertEquals("foo", evalS("(apply str [\\f \\o \\o])")); + + assertEquals(10L,evalL("(apply + 1 2 [3 4])")); + assertEquals(3L,evalL("(apply + 1 2 nil)")); + + assertEquals(Vectors.of(1L, 2L, 3L, 4L), eval("(apply vector 1 2 (list 3 4))")); + assertEquals(List.of(1L, 2L, 3L, 4L), eval("(apply list 1 2 [3 4])")); + assertEquals(List.of(1L, 2L), eval("(apply list 1 2 nil)")); + + // Bad function type + assertCastError(step("(apply 666 1 2 [3 4])")); + + // Keyword works as a function lookup, but wrong arity (#79) + assertArityError(step("(apply :n 1 2 [3 4])")); + + // Insufficient args to apply itself + assertArityError(step("(apply)")); + assertArityError(step("(apply vector)")); + + // Arity failure before cast + assertArityError(step("(apply 666)")); + + // Insufficient args to applied function + assertArityError(step("(apply assoc nil)")); + + // Cast error if not applied to collection + assertCastError(step("(apply inc 1)")); + assertCastError(step("(apply inc :foo)")); + + // not a sequential collection + assertCastError(step("(apply + 1 2 {})")); + } + + + @Test + public void testNonExistentAccountBalance() { + // Address that doesn't exist address, shouldn't have any balance initially + long addr=7777777777L; + assertNull(eval("(let [a (address "+addr+")] (balance a))")); + } + + @Test + public void testBalance() { + + // hero balance, should reflect cost of initial juice + Long expectedHeroBalance = HERO_BALANCE; + assertEquals(expectedHeroBalance, evalL("(let [a (address " + HERO + ")] (balance a))")); + + // someone else's balance + Long expectedVillainBalance = VILLAIN_BALANCE; + assertEquals(expectedVillainBalance, evalL("(let [a (address " + VILLAIN + ")] (balance a))")); + + assertCastError(step("(balance nil)")); + assertCastError(step("(balance 0x00)")); + assertCastError(step("(balance :foo)")); + + assertArityError(step("(balance)")); + assertArityError(step("(balance 1 2)")); + } + + @Test + public void testCreateAccount() { + Context
ctx=step("(create-account 0x817934590c058ee5b7f1265053eeb4cf77b869e14c33e7f85b2babc85d672bbc)"); + Address addr=ctx.getResult(); + assertEquals(addr.longValue()+1,ctx.getState().getAccounts().count()); // should be last Address added + + assertCastError(step("(create-account :foo)")); + assertCastError(step("(create-account 1)")); + assertCastError(step("(create-account nil)")); + assertCastError(step("(create-account #666666)")); + + assertArityError(step("(create-account)")); + assertArityError(step("(create-account 1 2)")); + } + + @Test + public void testAccept() { + assertEquals(0L, evalL("(accept 0)")); + assertEquals(0L, evalL("(accept *offer*)")); // offer should be initially zero + assertEquals(0L, evalL("(accept (byte 0))")); // byte should widen to Long + + // accepting non-integer value -> CAST error + assertCastError(step("(accept :foo)")); + assertCastError(step("(accept :foo)")); + assertCastError(step("(accept 0.3)")); + + // accepting negative -> ARGUMENT error + assertArgumentError(step("(accept -1)")); + + // accepting more than is offered -> STATE error + assertStateError(step("(accept 1)")); + + assertArityError(step("(accept)")); + assertArityError(step("(accept 1 2)")); + } + + @Test + public void testAcceptInActor() { + Context ctx=context(); + ctx=step(ctx,"(def act (deploy '(do (defn receive-coin ^{:callable? true} [sender amount data] (accept amount)) (defn echo-offer ^{:callable? true} [] *offer*))))"); + + ctx=step(ctx,"(transfer act 100)"); + assertEquals(100L, (long)RT.jvm(ctx.getResult())); + assertEquals(100L,evalL(ctx,"(balance act)")); + assertEquals(999L,evalL(ctx,"(call act 999 (echo-offer))")); + + // send via contract call + ctx=step(ctx,"(call act 666 (receive-coin *address* 350 nil))"); + assertEquals(350L, (long)RT.jvm(ctx.getResult())); + assertEquals(450L,evalL(ctx,"(balance act)")); + + } + + @Test + public void testCall() { + Context
ctx = step("(def ctr (deploy '(do (defn foo ^{:callable? true} [] :bar))))"); + + assertEquals(Keywords.BAR,eval(ctx,"(call ctr (foo))")); // regular call + assertEquals(Keywords.BAR,eval(ctx,"(call ctr 100 (foo))")); // call with offer + + assertArityError(step(ctx, "(call)")); + assertArityError(step(ctx, "(call 12)")); + + assertCastError(step(ctx, "(call ctr :foo (bad-fn 1 2))")); // cast fail on offered value + assertStateError(step(ctx, "(call ctr 12 (bad-fn 1 2))")); // bad function + + // bad format for call + assertCompileError(step(ctx,"(call ctr foo)")); + assertCompileError(step(ctx,"(call ctr [foo])")); // not a list + assertCompileError(step(ctx,"(call ctr #{some-func 42})")); // See #135 + + assertNobodyError(step(ctx, "(call #666666 12 (bad-fn 1 2))")); // bad actor + assertArgumentError(step(ctx, "(call ctr -12 (bad-fn 1 2))")); // negative offer + + // bad actor takes precedence over bad offer + assertNobodyError(step(ctx, "(call #666666 -12 (bad-fn 1 2))")); + + } + + @Test + public void testCallSelf() { + Context
ctx = step("(def ctr (deploy '(do (defn foo ^{:callable? true} [] (call *address* (bar))) (defn bar ^{:callable? true} [] (= *address* *caller*)))))"); + Address actor=ctx.getResult(); + + assertTrue(evalB(ctx, "(call ctr (foo))")); // nested call to same actor + assertFalse(evalB(ctx, "(call ctr (bar))")); // call from hero only + + assertEquals(Sets.of(Symbols.FOO,Symbols.BAR),ctx.getAccountStatus(actor).getCallableFunctions()); + } + + @Test + public void testCallables() { + assertSame(Sets.empty(),context().getAccountStatus().getCallableFunctions()); + } + + + @Test + public void testCallStar() { + Context
ctx = step("(def ctr (deploy '(do :foo (defn f ^{:callable? true} [x] (inc x)) )))"); + + assertEquals(9L,evalL(ctx, "(call* ctr 0 'f 8)")); + assertCastError(step(ctx, "(call* ctr 0 :f 8)")); // cast fail on keyword function name + + assertArityError(step(ctx, "(call*)")); + assertArityError(step(ctx, "(call* 12)")); + assertArityError(step(ctx, "(call* 1 2)")); // no function + + assertCastError(step(ctx, "(call* ctr :foo 'bad-fn 1 2)")); // cast fail on offered value + assertStateError(step(ctx, "(call* ctr 12 'bad-fn 1 2)")); // bad function + } + + @Test + public void testDeploy() { + Context
ctx = step("(def ctr (deploy '(fn [] :foo :bar)))"); + Address ca = ctx.getResult(); + assertNotNull(ca); + AccountStatus as = ctx.getAccountStatus(ca); + assertNotNull(as); + assertEquals(ca, eval(ctx, "ctr")); // defined address in environment + + // initial deployed state + assertEquals(0L, as.getBalance()); + + // double-deploy should get different addresses + assertFalse(evalB("(let [cfn '(do 1)] (= (deploy cfn) (deploy cfn)))")); + } + + @Test + public void testActorQ() { + Context
ctx = step("(def ctr (deploy '(fn [] :foo :bar)))"); + Address ctr=ctx.getResult(); + + assertTrue(evalB(ctx,"(actor? ctr)")); + + assertTrue(evalB(ctx,"(actor? (address ctr))")); + + // hero address is not an Actor + assertFalse(evalB(ctx,"(actor? *address*)")); + + // Not an Actor Address, even though the given values string refer to one. + assertFalse(evalB(ctx,"(actor? \""+ctr.toHexString()+"\")")); + assertFalse(evalB(ctx,"(actor? 8)")); + + // Above are OK if cast to addresses explicitly + assertTrue(evalB(ctx,"(actor? (address \""+ctr.toHexString()+"\"))")); + assertTrue(evalB(ctx,"(actor? (address 8))")); + + assertFalse(evalB(ctx,"(actor? :foo)")); + assertFalse(evalB(ctx,"(actor? nil)")); + assertFalse(evalB(ctx,"(actor? [ctr])")); + assertFalse(evalB(ctx,"(actor? 'ctr)")); + + // non-existant account is not an actor + assertFalse(evalB(ctx,"(actor? (address 99999999))")); + assertFalse(evalB(ctx,"(actor? #4512)")); + assertFalse(evalB(ctx,"(actor? -1234)")); + + assertArityError(step("(actor?)")); + assertArityError(step("(actor? :foo :bar)")); // ARITY before CAST + + } + + @Test + public void testAccountQ() { + // a new Actor is an account + Context
ctx = step("(def ctr (deploy '(fn [] :foo :bar)))"); + assertTrue(evalB(ctx,"(account? ctr)")); + + // standard actors are accounts + assertTrue(evalB(ctx,"(account? *registry*)")); + + // standard actors are accounts + assertTrue(evalB(ctx,"(account? "+HERO+")")); + + // a fake address + assertFalse(evalB(ctx,"(account? 77777777)")); + + // String with and without hex. See Issue #90 + assertFalse(evalB(ctx,"(account? \"deadbeef\")")); + assertFalse(evalB(ctx,"(account? \"zzz\")")); + + // a blob that is wrong length for an address. See Issue #90 + assertFalse(evalB(ctx,"(account? 0x1234)")); + + // a blob that actually refers to a valid account. But it isn't an Address... + assertFalse(evalB(ctx,"(account? 0x0000000000000008)")); + + // current hero address is an account + assertTrue(evalB(ctx,"(account? *address*)")); + + assertFalse(evalB("(account? :foo)")); + assertFalse(evalB("(account? nil)")); + assertFalse(evalB("(account? [])")); + assertFalse(evalB("(account? 'foo)")); + + assertArityError(step("(account?)")); + assertArityError(step("(account? 1 2)")); // ARITY before CAST + } + + @Test + public void testAccount() { + // a new Actor is an account + Context
ctx = step("(def ctr (deploy '(fn [] :foo :bar)))"); + AccountStatus as=eval(ctx,"(account ctr)"); + assertNotNull(as); + + // standard actors are accounts + assertTrue(eval("(account *registry*)") instanceof AccountStatus); + + // a non-existent address returns null + assertNull(eval(ctx,"(account #77777777)")); + + // current address is an account, and its balance is correct + assertTrue(evalB("(= *balance* (:balance (account *address*)))")); + + // invalid addresses + assertCastError(step("(account nil)")); + assertCastError(step("(account 8)")); + assertCastError(step("(account :foo)")); + assertCastError(step("(account [])")); + assertCastError(step("(account 'foo)")); + + assertArityError(step("(account)")); + assertArityError(step("(account 1 2)")); // ARITY before CAST + } + + @Test + public void testSetKey() { + Context ctx=context(); + + ctx=step(ctx,"(set-key 0x0000000000000000000000000000000000000000000000000000000000000000)"); + assertEquals(AccountKey.ZERO,ctx.getResult()); + assertEquals(AccountKey.ZERO,eval(ctx,"*key*")); + + ctx=step(ctx,"(set-key nil)"); + assertNull(ctx.getResult()); + assertNull(eval(ctx,"*key*")); + + ctx=step(ctx,"(set-key "+InitTest.HERO_KEY+")"); + assertEquals(InitTest.HERO_KEY,ctx.getResult()); + assertEquals(InitTest.HERO_KEY,eval(ctx,"*key*")); + + assertEquals(true, evalB("(do " + + " (def k 0x0000000000000000000000000000000000000000000000000000000000000000)" + + " (def a (deploy `(set-key ~k)))" + + " (= k (:key (account a))))")); + } + + @Test + public void testSetAllowance() { + + // zero price for unchanged allowance + assertEquals(0L, evalL("(set-memory *memory*)")); + + // sell whole allowance, should zero memory + assertEquals(0L, evalL("(do (set-memory 0) *memory*)")); + + // buy allowance reduces balance + assertTrue(evalL("(let [b *balance*] (set-memory (inc *memory*)) (- *balance* b))")<0); + + // sell allowance increases balance + assertTrue(evalL("(let [b *balance*] (set-memory (dec *memory*)) (- *balance* b))")>0); + + // trying to buy too much is a funds error + assertFundsError(step("(set-memory 1000000000000000000)")); + + // trying to set memory negative is an ARGUMENT error + assertArgumentError(step("(set-memory -1)")); + assertArgumentError(step("(set-memory -10000000)")); + assertArgumentError(step("(set-memory "+Long.MIN_VALUE+")")); + + assertCastError(step("(set-memory :foo)")); + assertCastError(step("(set-memory nil)")); + + assertArityError(step("(set-memory)")); + assertArityError(step("(set-memory 1 2)")); + + } + + @Test + public void testTransferMemory() { + long ALL=Constants.INITIAL_ACCOUNT_ALLOWANCE; + assertEquals(ALL, evalL(Symbols.STAR_MEMORY.toString())); + + { + Context ctx=step("(transfer-memory *address* 1337)"); + assertEquals(1337L, ctx.getResult().longValue()); + assertEquals(ALL, ctx.getAccountStatus(HERO).getMemory()); + } + + assertEquals(ALL-1337, step("(transfer-memory "+VILLAIN+" 1337)").getAccountStatus(HERO).getMemory()); + + assertEquals(0L, step("(transfer-memory "+VILLAIN+" "+ALL+")").getAccountStatus(HERO).getMemory()); + + assertArgumentError(step("(transfer-memory *address* -1000)")); + assertNobodyError(step("(transfer-memory #88888888 0)")); + + assertMemoryError(step("(transfer-memory *address* (+ 1 "+ALL+"))")); + + // check bad arg types + assertCastError(step("(transfer-memory -1000 1000)")); + assertCastError(step("(transfer-memory *address* :foo)")); + + // check bad arities + assertArityError(step("(transfer-memory -1000)")); + assertArityError(step("(transfer-memory)")); + assertArityError(step("(transfer-memory *address* 100 100)")); + + } + + @Test + public void testTransferToActor() { + // SECURITY: be careful with these tests + Address CORE=Init.CORE_ADDRESS; + + // should fail transferring to an account with no receive-coins export + assertStateError(step("(transfer "+CORE+" 1337)")); + + { // transfer to an Actor that accepts everything + Context ctx=step("(deploy '(do (defn receive-coin ^{:callable? true} [sender amount data] (accept amount))))"); + Address receiver=(Address) ctx.getResult(); + + ctx=step(ctx,"(transfer "+receiver.toString()+" 100)"); + assertCVMEquals(100L,ctx.getResult()); + assertCVMEquals(100L,ctx.getBalance(receiver)); + } + + { // transfer to an Actor that accepts nothing + Context ctx=step("(deploy '(do (defn receive-coin ^{:callable? true} [sender amount data] (accept 0))))"); + Address receiver=(Address) ctx.getResult(); + + ctx=step(ctx,"(transfer "+receiver.toString()+" 100)"); + assertCVMEquals(0L,ctx.getResult()); + assertCVMEquals(0L,ctx.getBalance(receiver)); + } + + { // transfer to an Actor that accepts half + Context ctx=step("(deploy '(do (defn receive-coin ^{:callable? true} [sender amount data] (accept (long (/ amount 2))))))"); + Address receiver=(Address) ctx.getResult(); + + // should be OK with a Blob Address + ctx=step(ctx,"(transfer "+receiver+" 100)"); + assertCVMEquals(50L,ctx.getResult()); + assertCVMEquals(50L,ctx.getBalance(receiver)); + } + + + } + + @Test + public void testTransfer() { + // SECURITY: don't mess with these tests + + // balance at start of transaction + long BAL = HERO_BALANCE; + + // transfer to self. Note juice already accounted for in context. + assertEquals(1337L, evalL("(transfer *address* 1337)")); // should return transfer amount + assertEquals(BAL, step("(transfer *address* 1337)").getBalance(HERO)); + + // transfers to an address that doesn't exist + { + Context nc1=step("(transfer (address 666666) 1337)"); + assertNobodyError(nc1); + } + + + // String representing a new User Address + Context
ctx=step("(create-account "+InitTest.HERO_KEYPAIR.getAccountKey()+")"); + Address naddr=ctx.getResult(); + + // transfers to a new address + { + Context nc1=step(ctx,"(transfer "+naddr+" 1337)"); + assertCVMEquals(1337L, nc1.getResult()); + assertEquals(BAL - 1337,nc1.getBalance(HERO)); + assertEquals(1337L, evalL(nc1,"(balance "+naddr+")")); + } + + assertTrue(() -> evalB(ctx,"(let [a "+naddr+"]" + + " (not (= *balance* (transfer a 1337))))")); + + // transfer it all! + assertEquals(0L,step(ctx,"(transfer "+naddr+" *balance*)").getBalance(HERO)); + + // Should never be possible to transfer negative amounts + assertArgumentError(step("(transfer *address* -1000)")); + assertArgumentError(step("(transfer "+naddr+" -1)")); + + // Long.MAX_VALUE is too big for an Amount + assertArgumentError(step("(transfer *address* 9223372036854775807)")); // Long.MAX_VALUE + + assertFundsError(step("(transfer *address* 999999999999999999)")); + + assertCastError(step("(transfer :foo 1)")); + assertCastError(step("(transfer *address* :foo)")); + + assertArityError(step("(transfer)")); + assertArityError(step("(transfer 1)")); + assertArityError(step("(transfer 1 2 3)")); + } + + @Test + public void testStake() { + Context ctx=step(context(),"(def my-peer 0x"+InitTest.FIRST_PEER_KEY.toHexString()+")"); + AccountKey MY_PEER=InitTest.FIRST_PEER_KEY; + long PS=ctx.getState().getPeer(InitTest.FIRST_PEER_KEY).getPeerStake(); + + { + // simple case of staking 1000000 on first peer of the realm + Context rc=step(ctx,"(stake my-peer 1000000)"); + assertNotError(rc); + assertEquals(PS+1000000,rc.getState().getPeer(MY_PEER).getTotalStake()); + assertEquals(1000000,rc.getState().getPeer(MY_PEER).getDelegatedStake()); + assertEquals(Constants.MAX_SUPPLY, rc.getState().computeTotalFunds()); + } + + // staking on an account key that isn't a peer + assertStateError(step(ctx,"(stake 0x1234567812345678123456781234567812345678123456781234567812345678 1234)")); + + // staking on an address + assertCastError(step(ctx,"(stake *address* 1234)")); + + // bad arg types + assertCastError(step(ctx,"(stake :foo 1234)")); + assertCastError(step(ctx,"(stake my-peer :foo)")); + assertCastError(step(ctx,"(stake my-peer nil)")); + + assertArityError(step(ctx,"(stake my-peer)")); + assertArityError(step(ctx,"(stake my-peer 1000 :foo)")); + } + + @Test + public void testSetPeerData() { + String newHostname = "new_hostname:1234"; + Context ctx=context(); + ctx=ctx.forkWithAddress(InitTest.FIRST_PEER_ADDRESS); + AccountKey peerKey=InitTest.FIRST_PEER_KEY; + ctx=step(ctx,"(def peer-key "+peerKey+")"); + { + // make sure we are using the FIRST_PEER address + ctx=step(ctx,"(set-peer-data peer-key {:url \"" + newHostname + "\"})"); + assertNotError(ctx); + assertEquals(newHostname,ctx.getState().getPeer(InitTest.FIRST_PEER_KEY).getHostname().toString()); + ctx=step(ctx,"(set-peer-data peer-key {})"); + assertNotError(ctx); + // no change to data + assertEquals(newHostname,ctx.getState().getPeer(InitTest.FIRST_PEER_KEY).getHostname().toString()); + } + + // Try to hijack with an account that isn't the first Peer + ctx=ctx.forkWithAddress(HERO.offset(2)); + { + newHostname = "set-key-hijack"; + ctx=step(ctx,"(do (set-key "+peerKey+") (set-peer-data "+peerKey+" {:url \"" + newHostname + "\"}))"); + assertStateError(ctx); + } + + ctx=ctx.forkWithAddress(InitTest.FIRST_PEER_ADDRESS); + assertCastError(step(ctx,"(set-peer-data peer-key 0x1234567812345678123456781234567812345678123456781234567812345678)")); + assertCastError(step(ctx,"(set-peer-data peer-key :bad-key)")); + assertCastError(step(ctx,"(set-peer-data 12 {})")); + assertCastError(step(ctx,"(set-peer-data nil {})")); + + assertArityError(step(ctx,"(set-peer-data)")); + assertArityError(step(ctx,"(set-peer-data peer-key)")); + assertArityError(step(ctx,"(set-peer-data peer-key {:url \"test\" :bad-key 1234} 2)")); + } + + @Test + public void testCreatePeer() { + // Kep Pair for new Peer + AKeyPair kp=AKeyPair.createSeeded(4583763); + + Context ctx=step(context(),"(def hero-peer 0x"+kp.getAccountKey().toHexString()+")"); + ctx=ctx.forkWithAddress(InitTest.HERO); + + Context peerCTX = step(ctx,"(create-peer hero-peer 1000)"); + // create a peer based on the HERO address and public key + assertNotError(peerCTX); + + // create a peer again on the same peer key and address + assertError(step(peerCTX,"(create-peer hero-peer 1000)")); + + // create a new peer with zero stake + assertError(step(ctx,"(create-peer hero-peer 0)")); + + // creating a peer on an account key that isn't the hero account key + // TODO: what should happen here? + //assertArgumentError(step(ctx,"(create-peer 0x1234567812345678123456781234567812345678123456781234567812345678 1234)")); + + // creating a peer with invalid account key + assertCastError(step(ctx,"(create-peer *address* 1234)")); + + // bad arg types + assertCastError(step(ctx,"(create-peer :foo 1234)")); + assertCastError(step(ctx,"(create-peer hero-peer :foo)")); + assertCastError(step(ctx,"(create-peer hero-peer nil)")); + + assertArityError(step(ctx,"(create-peer hero-peer)")); + assertArityError(step(ctx,"(create-peer hero-peer 1000 :foo)")); + } + + @Test + public void testCreatePeerRegression() { + assertNotError(step("(create-peer 0x42ae93b185bd2ba64fd9b0304fec81a4d4809221a5b68de4da041b48c85bcc2e (dec *balance*))")); + assertNotError(step("(create-peer 0x42ae93b185bd2ba64fd9b0304fec81a4d4809221a5b68de4da041b48c85bcc2e *balance*)")); + assertFundsError(step("(create-peer 0x42ae93b185bd2ba64fd9b0304fec81a4d4809221a5b68de4da041b48c85bcc2e (inc *balance*))")); + + } + + @Test + public void testNumericComparisons() { + assertFalse(evalB("(== 1 2)")); + assertFalse(evalB("(== 1.0 2.0)")); + + assertTrue(evalB("(== 3 3)")); + assertTrue(evalB("(== 0.0 -0.0)")); // IEE754 defines as equals + assertFalse(evalB("(= 0.0 -0.0)")); // Not identical values + assertTrue(evalB("(== 7.0000 7.0)")); + assertTrue(evalB("(== -1.00E0 -1.0)")); + assertTrue(evalB("(== 7 7.0)")); + assertTrue(evalB("(== 7.0 7)")); + + assertTrue(evalB("(<)")); + assertTrue(evalB("(>)")); + assertTrue(evalB("(< 1 2)")); + assertTrue(evalB("(<= 1 2)")); + assertTrue(evalB("(<= 1 2 6)")); + assertFalse(evalB("(< 2 2)")); + assertFalse(evalB("(< 3 2)")); + assertTrue(evalB("(<= 2.0 2.0)")); + assertTrue(evalB("(>= 2.0 2.0)")); + assertFalse(evalB("(> 1 2)")); + assertFalse(evalB("(> 3.0 3.0)")); + assertFalse(evalB("(> 3.0 1.0 7.0)")); + assertTrue(evalB("(>= 3.0 3.0)")); + + // assertTrue(evalB("(>= \\b \\a)")); // TODO: do we want this to work? + + // juice should go down in order of evaluation + assertTrue(evalB("(> *juice* *juice* *juice*)")); + + assertCastError(step("(> :foo)")); + assertCastError(step("(> :foo :bar)")); + assertCastError(step("(> [] [1])")); + } + + @Test + public void testMin() { + assertEquals(1L, evalL("(min 1 2 3 4)")); + assertEquals(7L, evalL("(min 7)")); + assertEquals(2L, evalL("(min 4 3 2)")); + assertEquals(1L, evalL("(min 1 2)")); + + assertEquals(1L, evalL("("+Init.CORE_ADDRESS+"/min 1 2)")); + + assertEquals(1.0, evalD("(min 2.0 1.0 3.0)")); + assertEquals(CVMDouble.NaN, eval("(min 2.0 ##NaN -0.0 ##Inf)")); + assertEquals(CVMDouble.NaN, eval("(min ##NaN)")); + + // TODO: Figure out how this should behave. See issue https://github.com/Convex-Dev/convex/issues/99 + // assertEquals(CVMLong.ONE, eval("(min ##NaN 1 ##NaN)")); + + assertCastError(step("(min true)")); + assertCastError(step("(min \\c)")); + assertCastError(step("(min ##NaN true)")); + assertCastError(step("(min true ##NaN)")); + + + // #NaNs should get ignored + assertEquals(CVMDouble.NaN,eval("(min ##NaN 42)")); + assertEquals(CVMDouble.NaN,eval("(min 42 ##NaN)")); + + assertArityError(step("(min)")); + } + + @Test + public void testMax() { + assertEquals(4L, evalL("(max 1 2 3 4)")); + assertEquals(CVMDouble.NaN, eval("(max 1 ##-Inf 3 ##NaN 4)")); + assertEquals(7L, evalL("(max 7)")); + assertEquals(4.0, evalD("(max 4.0 3 2)")); + assertEquals(CVMDouble.NaN, eval("(max 1 2.5 ##NaN)")); + + assertArityError(step("(max)")); + } + + @Test + public void testPow() { + assertEquals(4.0, evalD("(pow 2 2)")); + + assertCastError(step("(pow :a 7)")); + assertCastError(step("(pow 7 :a)")); + + assertArityError(step("(pow)")); + assertArityError(step("(pow 1)")); + assertArityError(step("(pow 1 2 3)")); + } + + @Test + public void testQuot() { + assertEquals(0L, evalL("(quot 4 10)")); + assertEquals(2L, evalL("(quot 10 4)")); + assertEquals(-2L, evalL("(quot -10 4)")); + + assertCastError(step("(quot :a 7)")); + assertCastError(step("(quot 7 nil)")); + + assertArityError(step("(quot)")); + assertArityError(step("(quot 1)")); + assertArityError(step("(quot 1 2 3)")); + } + + @Test + public void testMod() { + assertEquals(4L, evalL("(mod 4 10)")); + assertEquals(4L, evalL("(mod 14 10)")); + assertEquals(6L, evalL("(mod -1 7)")); + assertEquals(0L, evalL("(mod 7 7)")); + assertEquals(0L, evalL("(mod 0 -1)")); + + assertEquals(6L, evalL("(mod -1 -7)")); + + assertArgumentError(step("(mod 10 0)")); + + assertCastError(step("(mod :a 7)")); + assertCastError(step("(mod 7 nil)")); + + assertArityError(step("(mod)")); + assertArityError(step("(mod 1)")); + assertArityError(step("(mod 1 2 3)")); + } + + @Test + public void testRem() { + assertEquals(4L, evalL("(rem 4 10)")); + assertEquals(4L, evalL("(rem 14 10)")); + assertEquals(-1L, evalL("(rem -1 7)")); + assertEquals(0L, evalL("(rem 7 7)")); + assertEquals(0L, evalL("(rem 0 -1)")); + + assertEquals(-1L, evalL("(rem -1 -7)")); + + assertArgumentError(step("(rem 10 0)")); + + assertCastError(step("(rem :a 7)")); + assertCastError(step("(rem 7 nil)")); + + assertArityError(step("(rem)")); + assertArityError(step("(rem 1)")); + assertArityError(step("(rem 1 2 3)")); + } + + @Test + public void testExp() { + assertEquals(1.0, evalD("(exp 0)")); + assertEquals(1.0, evalD("(exp -0)")); + assertEquals(StrictMath.exp(1.0), evalD("(exp 1)")); + assertEquals(0.0, evalD("(exp (/ -1 0))")); + assertEquals(Double.POSITIVE_INFINITY, evalD("(exp (/ 1 0))")); + + assertCastError(step("(exp :a)")); + assertCastError(step("(exp #3)")); + assertCastError(step("(exp nil)")); + + assertArityError(step("(exp)")); + assertArityError(step("(exp 1 2)")); + } + + @Test + public void testHash() { + assertEquals(Hash.fromHex("a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"),eval("(hash 0x)")); + + assertEquals(Hash.NULL_HASH, eval("(hash (encoding nil))")); + assertEquals(Hash.TRUE_HASH, eval("(hash (encoding true))")); + assertEquals(Maps.empty().getHash(), eval("(hash (encoding {}))")); + + assertTrue(evalB("(= (hash 0x12) (hash 0x12))")); + assertTrue(evalB("(blob? (hash (encoding 42)))")); // Should be a Blob + + assertArityError(step("(hash)")); + assertArityError(step("(hash nil nil)")); + } + + @Test + public void testCount() { + assertEquals(0L, evalL("(count nil)")); + assertEquals(0L, evalL("(count [])")); + assertEquals(0L, evalL("(count ())")); + assertEquals(0L, evalL("(count 0x)")); + assertEquals(0L, evalL("(count \"\")")); + assertEquals(2L, evalL("(count (list :foo :bar))")); + assertEquals(2L, evalL("(count #{1 2 2})")); + assertEquals(3L, evalL("(count [1 2 3])")); + assertEquals(4L, evalL("(count 0xcafebabe)")); + + // Count of a map is the number of entries + assertEquals(2L, evalL("(count {1 2 2 3})")); + + // non-countable things fail with CAST + assertCastError(step("(count 1)")); + assertCastError(step("(count :foo)")); + + assertArityError(step("(count)")); + assertArityError(step("(count 1 2)")); + } + + @Test + public void testCompile() { + assertEquals(Constant.of(1L), eval("(compile 1)")); + + assertEquals(Constant.of(1L), eval("(compile 1)")); + assertEquals(Constant.of(null), eval("(compile nil)")); + assertEquals(Invoke.class, eval("(compile '(+ 1 2))").getClass()); + assertEquals(Do.class, eval("(compile '(do a b))").getClass()); + + assertArityError(step("(compile)")); + assertArityError(step("(compile 1 2)")); + assertArityError(step("(if 1)")); + } + + private AVector ALL_PREDICATES = Vectors + .create(Core.ENVIRONMENT.filterValues(e -> e instanceof CorePred).values()); + private AVector ALL_CORE_DEFS = Vectors + .create(Core.ENVIRONMENT.filterValues(e -> e instanceof ICoreDef).values()); + + @Test + public void testPredArity() { + AVector pvals = ALL_PREDICATES; + assertFalse(pvals.isEmpty()); + Context C = context(); + ACell[] a0 = new ACell[0]; + ACell[] a1 = new ACell[1]; + ACell[] a2 = new ACell[2]; + for (ACell p : pvals) { + CorePred pred = (CorePred) p; + assertTrue(RT.isBoolean(pred.invoke(C, a1).getResult()), "Predicate: " + pred); + assertArityError(pred.invoke(C, a0)); + assertArityError(pred.invoke(C, a2)); + } + } + + @Test + public void testCoreDefSymbols() throws BadFormatException { + AVector vals = ALL_CORE_DEFS; + assertFalse(vals.isEmpty()); + for (ACell def : vals) { + Symbol sym = ((ICoreDef)def).getSymbol(); + ACell v=Core.ENVIRONMENT.get(sym); + assertSame(def, v); + + Blob b = Format.encodedBlob(def); + assertSame(def, Format.read(b)); + + AHashMap meta= Core.METADATA.get(sym); + assertNotNull(meta,"Missing metadata for core symbol: "+sym); + ACell dobj=meta.get(Keywords.DOC); + assertNotNull(dobj,"No documentation found for core definition: "+sym); + } + } + + @Test + public void testNilPred() { + assertTrue(evalB("(nil? nil)")); + assertFalse(evalB("(nil? 1)")); + assertFalse(evalB("(nil? [])")); + } + + @Test + public void testListPred() { + assertFalse(evalB("(list? nil)")); + assertFalse(evalB("(list? 1)")); + assertTrue(evalB("(list? '())")); + assertTrue(evalB("(list? '(3 4 5))")); + assertFalse(evalB("(list? [1 2 3])")); + assertFalse(evalB("(list? {1 2})")); + } + + @Test + public void testVectorPred() { + assertFalse(evalB("(vector? nil)")); + assertFalse(evalB("(vector? 1)")); + assertTrue(evalB("(vector? [])")); + assertFalse(evalB("(vector? '(3 4 5))")); + assertTrue(evalB("(vector? [1 2 3])")); + assertTrue(evalB("(vector? (first {1 2 3 4}))")); + assertFalse(evalB("(vector? {1 2})")); + } + + @Test + public void testSetPred() { + assertFalse(evalB("(set? nil)")); + assertFalse(evalB("(set? 1)")); + assertTrue(evalB("(set? #{})")); + assertFalse(evalB("(set? '(3 4 5))")); + assertTrue(evalB("(set? #{1 2 3})")); + assertFalse(evalB("(set? {1 2})")); + } + + @Test + public void testMapPred() { + assertFalse(evalB("(map? nil)")); + assertFalse(evalB("(map? 1)")); + assertTrue(evalB("(map? {})")); + assertFalse(evalB("(map? '(3 4 5))")); + assertTrue(evalB("(map? {1 2 3 4})")); + assertFalse(evalB("(map? #{1 2})")); + } + + @Test + public void testCollPred() { + assertFalse(evalB("(coll? nil)")); + assertFalse(evalB("(coll? 1)")); + assertFalse(evalB("(coll? :foo)")); + assertTrue(evalB("(coll? {})")); + assertTrue(evalB("(coll? [])")); + assertTrue(evalB("(coll? ())")); + assertTrue(evalB("(coll? '())")); + assertTrue(evalB("(coll? #{})")); + assertTrue(evalB("(coll? '(3 4 5))")); + assertTrue(evalB("(coll? [:foo :bar])")); + assertTrue(evalB("(coll? {1 2 3 4})")); + assertTrue(evalB("(coll? #{1 2})")); + } + + @Test + public void testEmptyPred() { + assertTrue(evalB("(empty? nil)")); + assertTrue(evalB("(empty? {})")); + assertTrue(evalB("(empty? [])")); + assertTrue(evalB("(empty? ())")); + assertTrue(evalB("(empty? #{})")); + assertFalse(evalB("(empty? {1 2})")); + assertFalse(evalB("(empty? [ 3])")); + assertFalse(evalB("(empty? '(foo))")); + assertFalse(evalB("(empty? #{[]})")); + } + + @Test + public void testSymbolPred() { + assertTrue(evalB("(symbol? 'foo)")); + assertTrue(evalB("(symbol? (symbol :bar))")); + assertFalse(evalB("(symbol? nil)")); + assertFalse(evalB("(symbol? 1)")); + assertFalse(evalB("(symbol? ['foo])")); + } + + @Test + public void testKeywordPred() { + assertTrue(evalB("(keyword? :foo)")); + assertTrue(evalB("(keyword? (keyword 'bar))")); + assertFalse(evalB("(keyword? nil)")); + assertFalse(evalB("(keyword? 1)")); + assertFalse(evalB("(keyword? [:foo])")); + } + + @Test + public void testAddressPred() { + assertTrue(evalB("(address? *origin*)")); + assertFalse(evalB("(address? nil)")); + assertFalse(evalB("(address? 1)")); + assertFalse(evalB("(address? \"0a1b2c3d\")")); + assertFalse(evalB("(address? (blob *origin*))")); + } + + @Test + public void testBlobPred() { + assertTrue(evalB("(blob? (blob *origin*))")); + assertTrue(evalB("(blob? 0xFF)")); + assertTrue(evalB("(blob? (blob 0x17))")); + assertTrue(evalB("(blob? (hash (encoding *state*)))")); // HAsh + assertTrue(evalB("(blob? *key*)")); // AccountKey + + assertFalse(evalB("(blob? 17)")); + assertFalse(evalB("(blob? nil)")); + assertFalse(evalB("(blob? *address*)")); + } + + @Test + public void testLongPred() { + assertTrue(evalB("(long? 1)")); + assertTrue(evalB("(long? (long *balance*))")); // TODO: is this sane? + assertFalse(evalB("(long? (byte 1))")); + assertFalse(evalB("(long? nil)")); + assertFalse(evalB("(long? 0xFF)")); + assertFalse(evalB("(long? [1 2])")); + } + + @Test + public void testStrPred() { + assertTrue(evalB("(str? (name :foo))")); + assertTrue(evalB("(str? (str :foo))")); + assertTrue(evalB("(str? (str nil))")); + assertFalse(evalB("(str? 1)")); + assertFalse(evalB("(str? nil)")); + } + + @Test + public void testNumberPred() { + assertTrue(evalB("(number? 0)")); + assertTrue(evalB("(number? (byte 0))")); + assertTrue(evalB("(number? 0.5)")); + assertTrue(evalB("(number? ##NaN)")); // Sane? Is numeric double type.... + + assertFalse(evalB("(number? nil)")); + assertFalse(evalB("(number? :foo)")); + assertFalse(evalB("(number? 0xFF)")); + assertFalse(evalB("(number? [1 2])")); + + assertFalse(evalB("(number? true)")); + + } + + @Test + public void testZeroPred() { + assertTrue(evalB("(zero? 0)")); + assertTrue(evalB("(zero? (byte 0))")); + assertTrue(evalB("(zero? 0.0)")); + assertFalse(evalB("(zero? 0.00005)")); + assertFalse(evalB("(zero? 0x00)")); // not numeric! + + assertFalse(0.0 > -0.0); // check we are living in a sane universe + assertTrue(evalB("(zero? -0.0)")); + + assertFalse(evalB("(zero? \\c)")); + + assertFalse(evalB("(zero? nil)")); + assertFalse(evalB("(zero? :foo)")); + assertFalse(evalB("(zero? [1 2])")); + } + + @Test + public void testFn() { + assertEquals(1L,evalL("((fn [] 1))")); + assertEquals(2L,evalL("((fn [x] 2) 1)")); + assertEquals(3L,evalL("((fn [x] 2 3) 1)")); + // TODO: more cases! + + // test closing over lexical scope + assertEquals(3L,evalL("(let [a 3 f (fn [x] a)] (f 0))")); + + // Bad arity fn execution + assertArityError(step("((fn [x] 0))")); + assertArityError(step("((fn [] 0) 1)")); + + // Bad fn forms + assertArityError(step("(fn)")); + assertCompileError(step("(fn 1)")); + assertCompileError(step("(fn {})")); + assertCompileError(step("(fn '())")); + + // fn printing + assertCVMEquals("(fn [x y] 0)",eval("(str (fn [x y] 0))")); + assertCVMEquals("(fn [x y] (do 0 1))",eval("(str (fn [x y] 0 1))")); + + } + + @Test + public void testFnMulti() { + assertEquals(1L,evalL("((fn ([] 1)))")); + assertEquals(2L,evalL("((fn ([x] 2)) 1)")); + + // dispatch by arity + assertEquals(1L,evalL("((fn ([x] 1) ([x y] 2)) 3)")); + assertEquals(2L,evalL("((fn ([x] 1) ([x y] 2)) 3 4)")); + + // first matching impl chosen + assertEquals(1L,evalL("((fn ([x] 1) ([x] 2)) 3)")); + + // variadic match + assertEquals(2L,evalL("((fn ([x] 1) ([x & more] 2)) 3 4 5 6)")); + assertEquals(2L,evalL("((fn ([x] 1) ([x y & more] 2)) 3 4)")); + + // MultiFn printing + assertCVMEquals("(fn ([] 0) ([x] 1))",eval("(str (fn ([]0) ([x] 1) ))")); + + // Issue #193 Error test + assertEquals(Vectors.of(1,2,3),eval("(do (defn f ([[a b] c] [a b c])) (f [1 2] 3))")); + + // arity errors + assertArityError(step("((fn ([x] 1) ([x & more] 2)))")); + assertArityError(step("((fn ([x] 1) ([x y] 2)))")); + assertArityError(step("((fn ([x] 1) ([x y z] 2)) 2 3)")); + assertArityError(step("((fn ([x] 1) ([x y z & more] 2)) 2 3)")); + } + + @Test + public void testFnMultiRecur() { + assertEquals(7L,evalL("((fn ([x] x) ([x y] (recur 7))) 1 2)")); + + assertArityError(step("((fn ([x] x) ([x y] (recur))) 1 2)")); + + assertJuiceError(step("((fn ([x] (recur 3 4)) ([x y] (recur 5))) 1 2)")); + } + + @Test + public void testFnPred() { + assertFalse(evalB("(fn? 0)")); + assertTrue(evalB("(fn? (fn[x] 0))")); + assertFalse(evalB("(fn? {})")); + assertTrue(evalB("(fn? count)")); + assertTrue(evalB("(fn? fn?)")); + assertTrue(evalB("(fn? if)")); + } + + @Test + public void testDef() { + // Def returns defined value + assertEquals(Keywords.FOO, eval("(def v :foo)")); + + // Def establishes mapping in environment + assertEquals(CVMLong.ONE, step("(def foo 1)").getEnvironment().get(Symbols.FOO)); + + // Def creates valid dynamic variables + assertEquals(Vectors.of(2L, 3L), eval("(do (def v [2 3]) v)")); + assertNull(eval("(do (def v nil) v)")); + + // def overwrites existing bindings + assertEquals(Vectors.of(2L, 3L), eval("(do (def v nil) (def v [2 3]) v)")); + assertEquals(Vectors.of(2L, 3L), eval("(do (def count [2 3]) count)")); // overwriting core + + // TODO: are these error types logical? + assertCompileError(step("(def)")); + assertCompileError(step("(def a b c)")); + + assertUndeclaredError(step("(def a b)")); + + assertUndeclaredError(step("(def a a)")); + } + + @Test + public void testDefMeta() { + AHashMap FOOMAP = Maps.of(Keywords.FOO, CVMBool.TRUE); + AHashMap BARMAP = Maps.of(Keywords.BAR, CVMBool.TRUE); + AHashMap FOOBARMAP=FOOMAP.merge(BARMAP); + + // def of simple symbol has empty meta + assertEquals(Maps.empty(), eval("(do (def v 1) (lookup-meta 'v))")); + + // def with a keyword tag + assertEquals(FOOMAP, eval("(do (def ^:foo v 1) (lookup-meta 'v))")); + + // def with a keyword tag on value + assertEquals(FOOMAP, eval("(do (def v ^:foo 1) (lookup-meta 'v))")); + + // def with constructed syntax object shouldn't set metadata + assertEquals(Maps.empty(), eval("(do (def v (syntax 1 {:foo true})) (lookup-meta 'v))")); + + // def with syntax object constructed for symbol inline + // assertEquals(FOOMAP, eval("(do (def ~(syntax 'v {:foo true})) (lookup-meta 'v))")); + + // def without metadata on symbol shouldn't change metadata + assertEquals(FOOMAP, eval("(do (def ^:foo v 1) (def v 2) (lookup-meta 'v))")); + + // def with new metadata should overwrite + assertEquals(BARMAP, eval("(do (def ^:foo v 1) (def ^:bar v 2) (lookup-meta 'v))")); + + // def with metadata on both symbol and value should merge + assertEquals(FOOBARMAP, eval("(do (def ^:foo v ^{:bar true} 1) (lookup-meta 'v))")); + + } + + @Test + public void testDefinedQ() { + assertFalse(evalB("(defined? foobar)")); + + assertTrue(evalB("(do (def foobar [2 3]) (defined? foobar))")); + assertTrue(evalB("(defined? count)")); + + // invalid names + assertCastError(step("(defined? :count)")); // not a Symbol + assertCastError(step("(defined? \"count\")")); // not a Symbol + assertCastError(step("(defined? nil)")); + assertCastError(step("(defined? 1)")); + assertCastError(step("(defined? 0x)")); + + assertArityError(step("(defined?)")); + assertArityError(step("(defined? foo bar)")); + } + + @Test + public void testUndef() { + assertNull(eval("(undef count)")); + assertNull(eval("(undef foo)")); + assertNull(eval("(undef *balance*)")); + assertNull(eval("(undef bar)")); + + assertEquals(Vectors.of(1L, 2L), eval("(do (def a 1) (def v [a 2]) (undef a) v)")); + + assertFalse(evalB("(do (def a 1) (undef a) (defined? a))")); + + assertUndeclaredError(step("(do (def a 1) (undef a) a)")); + + assertArityError(step("(undef a b)")); + assertArityError(step("(undef)")); + } + + @Test + public void testUnquote() { + assertEquals(Vectors.of(1L, 2L), eval("`[1 (unquote (+ 1 1))]")); + assertEquals(Constant.create(CVMLong.create(3L)), comp("(unquote (+ 1 2))")); + + assertCompileError(step("(unquote)")); + assertCompileError(step("(unquote 1 2)")); + } + + @Test + public void testUnquoteError() { + assertUndeclaredError(step("~~foo")); + } + + @Test + public void testDefn() { + assertTrue(evalB("(do (defn f [a] a) (fn? f))")); + assertEquals(Vectors.of(2L, 3L), eval("(do (defn f [a & more] more) (f 1 2 3))")); + + // multiple expressions in body + assertEquals(2L,evalL("(do (defn f [a] 1 2) (f 3))")); + + // arity problems + assertArityError(step("(defn)")); + assertArityError(step("(defn f)")); + + // bad function construction + assertCompileError(step("(defn f b)")); + + } + + @Test + public void testDefnMulti() { + assertEquals(2L,evalL("(do (defn f ([a] 1 2)) (f 3))")); + assertEquals(2L,evalL("(do (defn f ([] 4) ([a] 1 2)) (f 3))")); + + assertArityError(step("(do (defn f ([] nil)) (f 3))")); + } + + @Test + public void testDefExpander() { + Context ctx=step("(defexpander expand-once [x e] (expand x (fn [x e] (syntax x))))"); + + assertEquals(Syntax.of(42L),eval(ctx,"(expand 42 expand-once)")); + } + + @Test + public void testSetBang() { + // set! fails on undeclared values + assertCompileError(step("(set! a 13)")); + assertCompileError(step("(do (set! a 13) a)")); + + // set! works in a function body + assertEquals(35L,evalL("(let [a 13 f (fn [x] (set! a 25) (+ x a))] (f 10))")); + + // set! only works in the scope of the immediate surrounding binding expression + assertEquals(10L,evalL("(let [a 10] (let [] (set! a 13)) a)")); + + // set! binding does not escape current form, still undeclared in enclosing local context + assertUndeclaredError(step("(do (let [a 10] (set! a 20)) a)")); + + // set! cannot alter value across closure boundary + { + assertEquals(5L,evalL("(let [a 5] ((fn [] (set! a 666))) a)")); + } + + // set! cannot alter value within query + assertEquals(5L,evalL("(let [a 5] (query (set! a 6)) a)")); + + // TODO: reconsider this + // set! doesn't work outside eval boundary? + assertCompileError(step ("(let [a 5] (eval `(set! a 7)) a)")); + } + + @Test + public void testEval() { + assertEquals("foo", evalS("(eval (list 'str \\f \\o \\o))")); + assertNull(eval("(eval 'nil)")); + assertEquals(10L, evalL("(eval '(+ 3 7))")); + assertEquals(40L, evalL("(eval `(* 2 4 5))")); + + assertArityError(step("(eval)")); + assertArityError(step("(eval 1 2)")); + } + + @Test + public void testEvalAs() { + assertEquals("foo", evalS("(eval-as *address* (list 'str \\f \\o \\o))")); + + assertTrustError(step("(eval-as *registry* '1)")); + + assertCastError(step("(eval-as :foo 2)")); + assertArityError(step("(eval-as 1)")); // arity > cast + assertArityError(step("(eval-as 1 2 3)")); + } + + @Test + public void testEvalAsTrustedUser() { + Context ctx=step("(set-controller "+VILLAIN+")"); + ctx=ctx.forkWithAddress(VILLAIN); + ctx=step(ctx,"(def hero "+HERO+")"); + + assertEquals(3L, evalL(ctx,"(eval-as hero '(+ 1 2))")); + assertEquals(HERO, eval(ctx,"(eval-as hero '*address*)")); + assertEquals(VILLAIN, eval(ctx,"(eval-as hero '*caller*)")); + assertEquals(Keywords.FOO, eval(ctx,"(eval-as hero '(return :foo))")); + assertEquals(Keywords.FOO, eval(ctx,"(eval-as hero '(halt :foo))")); + assertEquals(Keywords.FOO, eval(ctx,"(eval-as hero '(rollback :foo))")); + + assertAssertError(step(ctx,"(eval-as hero '(assert false))")); + } + + @Test + public void testEvalAsUntrustedUser() { + Context ctx=step("(set-controller nil)"); + ctx=ctx.forkWithAddress(VILLAIN); + ctx=step(ctx,"(def hero "+HERO+")"); + + assertTrustError(step(ctx,"(eval-as hero '(+ 1 2))")); + assertTrustError(step(ctx,"(eval-as (address hero) '(+ 1 2))")); + } + + @Test + public void testEvalAsWhitelistedUser() { + // create trust monitor that allows VILLAIN + Context ctx=step("(deploy '(do (defn check-trusted? ^{:callable? true} [s a o] (= s (address "+VILLAIN+")))))"); + Address monitor = (Address) ctx.getResult(); + ctx=step(ctx,"(set-controller "+monitor+")"); + + ctx=ctx.forkWithAddress(VILLAIN); + ctx=step(ctx,"(def hero "+HERO+")"); + + assertEquals(3L, evalL(ctx,"(eval-as hero '(+ 1 2))")); + } + + @Test + public void testQuery() { + Context> ctx=step("(query (def a 10) [*address* *origin* *caller* 10])"); + assertEquals(Vectors.of(HERO,HERO,null,10L), ctx.getResult()); + + // shouldn't be possible to mutate surrounding environment in query + assertEquals(10L,evalL("(let [a 3] (+ (query (set! a 5) (+ a 2)) a) )")); + + // shouldn't be any def in the environment + assertSame(INITIAL,ctx.getState()); + + // some juice should be consumed + assertTrue(context().getJuice()>ctx.getJuice()); + } + + @Test + public void testQueryError() { + Context ctx=step("(query (fail :FOO))"); + assertAssertError(ctx); + + // some juice should be consumed + assertTrue(context().getJuice()>ctx.getJuice()); + } + +// TODO: probably needs Op level support? +// @Test +// public void testQueryAs() { +// Context> ctx=step("(query-as "+Init.VILLAIN+" '(do (def a 10) [*address* *origin* *caller* 10]))"); +// assertEquals(Vectors.of(Init.VILLAIN,Init.VILLAIN,null,10L), ctx.getResult()); +// +// // shouldn't be any def in the environment +// assertSame(INITIAL,ctx.getState()); +// assertSame(INITIAL_CONTEXT.getLocalBindings(),ctx.getLocalBindings()); +// +// // some juice should be consumed +// assertTrue(INITIAL_CONTEXT.getJuice()>ctx.getJuice()); +// } + + @Test + public void testEvalAsNotWhitelistedUser() { + // create trust monitor that allows HERO only + Context ctx=step("(deploy '(do (defn check-trusted? ^{:callable? true} [s a o] (= s (address "+HERO+")))))"); + Address monitor = (Address) ctx.getResult(); + ctx=step(ctx,"(set-controller "+monitor+")"); + + ctx=ctx.forkWithAddress(VILLAIN); + ctx=step(ctx,"(def hero "+HERO+")"); + + assertTrustError(step(ctx,"(eval-as hero '(+ 1 2))")); + } + + @Test + public void testSetController() { + // set-controller returns new controller + assertEquals(VILLAIN, eval("(set-controller "+VILLAIN+")")); + assertEquals(VILLAIN, eval("(set-controller (address "+VILLAIN+"))")); + assertEquals(null, (Address)eval("(set-controller nil)")); + + assertNobodyError(step("(set-controller #666666)")); // non-existent account + + assertCastError(step("(set-controller :foo)")); + assertCastError(step("(set-controller (address nil))")); // Address cast fails + + assertArityError(step("(set-controller)")); + assertArityError(step("(set-controller 1 2)")); // arity > cast + } + + @Test + public void testScheduleFailures() { + assertArityError(step("(schedule)")); + assertArityError(step("(schedule 1)")); + assertArityError(step("(schedule 1 2 3)")); + assertArityError(step("(schedule :foo 2 3)")); // ARITY error before CAST + + assertCastError(step("(schedule :foo (def a 2))")); + assertCastError(step("(schedule nil (def a 2))")); + } + + @Test + public void testScheduleExecution() throws BadSignatureException { + long expectedTS = INITIAL.getTimeStamp().longValue() + 1000; + Context ctx = step("(schedule (+ *timestamp* 1000) (def a 2))"); + assertCVMEquals(expectedTS, ctx.getResult()); + State s = ctx.getState(); + BlobMap> sched = s.getSchedule(); + assertEquals(1L, sched.count()); + assertEquals(expectedTS, sched.entryAt(0).getKey().longValue()); + + assertTrue(step(ctx, "(do a)").isExceptional()); + + Block b = Block.of(expectedTS,InitTest.FIRST_PEER_KEY); + BlockResult br = s.applyBlock(b); + State s2 = br.getState(); + + Context ctx2 = Context.createInitial(s2, HERO, INITIAL_JUICE); + assertEquals(2L, evalL(ctx2, "a")); + } + + @Test + public void testExpand() { + assertEquals(Strings.create("foo"), eval("(expand (name :foo) (fn [x e] x))")); + assertEquals(CVMLong.create(3), eval("(expand '[1 2 3] (fn [x e] (nth x 2)))")); + + assertNull(Syntax.unwrap(eval("(expand nil)"))); + + assertCastError(step("(expand 1 :foo)")); + assertCastError(step("(expand { 888 227 723 560} [75 561 258 833])")); + + + assertArityError(step("(expand)")); + assertArityError(step("(expand 1 (fn [x e] x) :blah :blah)")); + + // arity error calling expander function + assertArityError(step("(expand 1 (fn [x] x))")); + + // arity error in expansion execution + assertArityError(step("(expand 1 (fn [x e] (count)))")); + } + + @Test + public void testExpandEdgeCases() { + // BAd functions + assertCastError(step("(expand 123 #0 :foo)")); + assertCastError(step("(expand 123 #0)")); + + // psuedo-function application, not valid for expand + assertCastError(step("(expand 'foo 'bar 'baz)")); + assertCastError(step("(expand {} :foo)")); + assertCastError(step("(expand {:bar 1 :bax 2} :bar :baz)")); + assertCastError(step("(expand {:foo 1 :bax 2} :bar :baz)")); + } + + @Test + public void testExpandOnce() { + // an expander that does nothing except wrap as syntax. + Context c=step("(def identity-expand (fn [x e] x))"); + assertEquals(Keywords.FOO,eval(c,"(identity-expand :foo nil)")); + + // function that expands once with initial-expander, then with identity + c=step(c,"(defn expand-once [x] (*initial-expander* x identity-expand))"); + // Should expand the outermost macro only + assertEquals(read("(cond (if 1 2) 3 4)"),Syntax.unwrapAll(eval(c,"(expand-once '(if (if 1 2) 3 4))"))); + + // Should be idempotent + assertEquals(eval(c,"(expand '(if (if 1 2) 3 4))"),eval(c,"(expand (expand-once '(if (if 1 2) 3 4)))")); + } + + + @Test + public void testMacro() { + Context c=step("(defmacro foo [] :foo)"); + assertEquals(Keywords.FOO,eval(c,"(foo)")); + } + + @Test + public void testQuote() { + assertEquals(Vectors.of(1,2,3),eval("(quote [1 2 3])")); + assertEquals(Sets.of(42),eval("(quote #{42})")); // See Issue #109 + assertFalse(evalB("(= (quote #{42}) (quote #{(syntax 42)}))")); // See Issue #109 + + assertEquals(Vectors.of(1,Lists.of(Symbols.IF,4,7),3),eval("(quote [1 (if 4 7) 3])")); + } + + @Test + public void testSyntax() { + assertEquals(Syntax.of(null), eval("(syntax nil)")); + assertEquals(Syntax.of(10L), eval("(syntax 10)")); + + // TODO: check if this is sensible + // Syntax should be idempotent and wrap one level only + assertCVMEquals(eval("(syntax 10)"), eval("(syntax (syntax 10))")); + + // Syntax with null / empty metadata should equal basic syntax + assertCVMEquals(eval("(syntax 10)"), eval("(syntax 10 nil)")); + assertCVMEquals(eval("(syntax 10)"), eval("(syntax 10 {})")); + + assertCastError(step("(syntax 2 3)")); + + assertArityError(step("(syntax)")); + assertArityError(step("(syntax 2 3 4)")); + } + + @Test + public void testUnsyntax() { + assertNull(eval("(unsyntax (syntax nil))")); + assertNull(eval("(unsyntax nil)")); + assertEquals(10L, evalL("(unsyntax (syntax 10))")); + assertEquals(Keywords.FOO, eval("(unsyntax (expand :foo))")); + + assertArityError(step("(unsyntax)")); + assertArityError(step("(unsyntax 2 3)")); + } + + @Test + public void testMeta() { + assertEquals(Maps.empty(),eval("(meta (syntax nil))")); + assertNull(eval("(meta nil)")); + assertNull(eval("(meta 10)")); + assertEquals(Maps.of(1L,2L), eval("(meta (syntax 10 {1 2}))")); + + assertArityError(step("(meta)")); + assertArityError(step("(meta 2 3)")); + } + + @Test + public void testSyntaxQ() { + assertFalse(evalB("(syntax? nil)")); + assertTrue(evalB("(syntax? (syntax 10))")); + + assertArityError(step("(syntax?)")); + assertArityError(step("(syntax? 2 3)")); + } + + @Test + public void testInitialExpander() { + // bad continuation expanders + assertCastError(step("(*initial-expander* (list #0) #0)")); + + assertArityError(step("(*initial-expander* 1 2 3)")); + assertArityError(step("(*initial-expander* 1)")); + } + + @Test + public void testExportsQ() { + Context ctx = step("(def caddr (deploy '(do " + "(defn private [] :priv) " + "(defn public ^{:callable? true} [] :pub))))"); + + Address caddr = (Address) ctx.getResult(); + assertNotNull(caddr); + + assertTrue(evalB(ctx, "(callable? caddr 'public)")); // OK + assertFalse(evalB(ctx, "(callable? caddr 'private)")); // Defined, but not exported + assertFalse(evalB(ctx, "(callable? caddr 'random-symbol)")); // Doesn't exist + + assertCastError(step(ctx, "(callable? caddr :public)")); // not a Symbol + assertCastError(step(ctx, "(callable? caddr :random-name)")); + assertCastError(step(ctx, "(callable? caddr :private)")); + + assertArityError(step(ctx, "(callable? 1)")); + assertArityError(step(ctx, "(callable? 1 2 3)")); + + assertCastError(step(ctx, "(callable? :foo :foo)")); + assertCastError(step(ctx, "(callable? nil :foo)")); + assertCastError(step(ctx, "(callable? caddr nil)")); + assertCastError(step(ctx, "(callable? caddr 1)")); + } + + @Test + public void testDec() { + assertEquals(0L, evalL("(dec 1)")); + assertEquals(0L, evalL("(dec (byte 1))")); + assertEquals(-10L, evalL("(dec -9)")); + // assertEquals(96L,(long)eval("(dec \\a)")); // TODO: think about this + + assertCastError(step("(dec nil)")); + assertCastError(step("(dec :foo)")); + assertCastError(step("(dec [1])")); + assertCastError(step("(dec #666)")); + assertCastError(step("(dec 3.0)")); + + assertArityError(step("(dec)")); + assertArityError(step("(dec 1 2)")); + } + + @Test + public void testInc() { + assertEquals(2L, evalL("(inc 1)")); + assertEquals(2L, evalL("(inc (byte 1))")); + // assertEquals(98L,(long)eval("(inc \\a)")); // TODO: think about this + + assertCastError(step("(inc #42)")); // Issue #89 + assertCastError(step("(inc nil)")); + assertCastError(step("(inc \\c)")); // Issue #89 + assertCastError(step("(inc true)")); // Issue #89 + + assertArityError(step("(inc)")); + assertArityError(step("(inc 1 2)")); + } + + @Test + public void testOr() { + assertNull(eval("(or)")); + assertNull(eval("(or nil)")); + assertEquals(Keywords.FOO, eval("(or :foo)")); + assertEquals(Keywords.FOO, eval("(or nil :foo :bar)")); + assertEquals(Keywords.FOO, eval("(or :foo nil :bar)")); + + // ensure later branches never get executed + assertEquals(Keywords.FOO, eval("(or :foo (+ nil :bar))")); + + assertFalse(evalB("(or nil nil false)")); + assertTrue(evalB("(or nil nil true)")); + + // arity error if fails before first truth value + assertArityError(step("(or nil (count) true)")); + } + + @Test + public void testAnd() { + assertTrue(evalB("(and)")); + assertNull(eval("(and nil)")); + assertEquals(Keywords.FOO, eval("(and :foo)")); + assertEquals(Keywords.FOO, eval("(and :bar :foo)")); + assertNull(eval("(and :foo nil :bar)")); + + // ensure later branches never get executed + assertNull(eval("(and nil (+ nil :bar))")); + + assertFalse(evalB("(and 1 false 2)")); + assertTrue(evalB("(and 1 :foo true true)")); + + // arity error if fails before first falsey value + assertArityError(step("(and true (count) nil)")); + } + + @Test + public void testSpecialAddress() { + // Hero should be *address* initial context + assertEquals(InitTest.HERO, eval("*address*")); + + // *address* MUST return Actor address within actor call + Context ctx=step("(def act (deploy `(do (defn addr ^{:callable? true} [] *address*))))"); + Address act=(Address) ctx.getResult(); + assertEquals(act, eval(ctx,"(call act (addr))")); + + // *address* MUST be current address in library call + assertEquals(InitTest.HERO, eval(ctx,"(act/addr)")); + } + + @Test + public void testSpecialOrigin() { + // Hero should be *origin* in initial context + assertEquals(InitTest.HERO, eval("*origin*")); + + // *origin* MUST return original address within actor call + Context ctx=step("(def act (deploy `(do (defn origin ^{:callable? true} [] *origin*))))"); + assertEquals(InitTest.HERO, eval(ctx,"(call act (origin))")); + + // *origin* MUST be original address in library call + assertEquals(InitTest.HERO, eval(ctx,"(act/origin)")); + } + + @Test + public void testSpecialAllowance() { + // Should have initial allowance at start + assertEquals(Constants.INITIAL_ACCOUNT_ALLOWANCE, evalL("*memory*")); + + // Buy some memory + assertEquals(Constants.INITIAL_ACCOUNT_ALLOWANCE, evalL("*memory*")); + + } + + + @Test + public void testSpecialBalance() { + // balance should return exact balance of account after execution + Context ctx = step("(long *balance*)"); + Long bal=ctx.getAccountStatus(HERO).getBalance(); + assertCVMEquals(bal, ctx.getResult()); + + // throwing it all away.... + assertEquals(0L, evalL("(do (transfer "+VILLAIN+" *balance*) *balance*)")); + + // check balance as single expression + assertEquals(bal, evalL("*balance*")); + + // Local values override specials + assertNull(eval("(let [*balance* nil] *balance*)")); + + // TODO: reconsider this, special take priority over enviornment? + assertCVMEquals(ctx.getOffer(),eval("(do (def *offer* :foo) *offer*)")); + + // Alternative behaviour + //assertNull(eval("(let [*balance* nil] *balance*)")); + //assertEquals(Keywords.FOO,eval("(do (def *balance* :foo) *balance*)")); + } + + @Test + public void testSpecialCaller() { + assertNull(eval("*caller*")); + assertEquals(HERO, eval("(do (def c (deploy '(do (defn f ^{:callable? true} [] *caller*)))) (call c (f)))")); + } + + @Test + public void testSpecialResult() { + // initial context result should be null + assertNull(eval("*result*")); + + // Result should get value of last completed expression + assertEquals(Keywords.FOO, eval("(do :foo *result*)")); + assertNull(eval("(do 1 (do) *result*)")); + + // TODO: how should this behave? + // assertEquals(Keywords.FOO, eval("(let [a :foo] *result*)")); + + assertEquals(Keywords.FOO, eval("(do ((fn [] :foo)) *result*)")); + + // *result* should be cleared to nil in an Actor call. + assertNull(eval("(do (def c (deploy '(do (defn f ^{:callable? true} [] *result*)))) (call c (f)))")); + + } + + @Test + public void testSpecialState() { + assertSame(INITIAL, eval("*state*")); + assertSame(INITIAL.getAccounts(), eval("(:accounts *state*)")); + } + + @Test + public void testSpecialKey() { + assertEquals(InitTest.HERO_KEYPAIR.getAccountKey(), eval("*key*")); + } + + @Test + public void testSpecialJuice() { + // TODO: semantics of returning juice before lookup complete is OK? + // seems sensible, represents "juice left at this position". + assertCVMEquals(INITIAL_JUICE, eval(Special.forSymbol(Symbols.STAR_JUICE))); + + // juice gets consumed before returning a value + assertCVMEquals(INITIAL_JUICE-Juice.DO - Juice.CONSTANT, eval(comp("(do 1 *juice*)"))); + } + + + @Test + public void testSpecialEdgeCases() { + + // TODO: consider this + //assertEquals(Init.HERO,eval(Init.CORE_ADDRESS+"/*balance*")); + + // TODO: consider this + // Lookup in core environment of special returns the Symbol + assertEquals(Symbols.STAR_JUICE,eval("(lookup *juice*)")); + + assertEquals(Symbols.STAR_JUICE,eval(Lookup.create(Symbols.STAR_JUICE))); + } + + @Test public void testSpecialHoldings() { + assertSame(BlobMaps.empty(),eval("*holdings*")); + + // Test set-holding modifies *holdings* as expected + assertNull(eval("(get-holding *address*)")); + assertEquals(BlobMaps.of(HERO,1L),eval("(do (set-holding *address* 1) *holdings*)")); + + assertNull(eval("(*holdings* { :PuSg 650989 })")); + assertEquals(Keywords.FOO,eval("(*holdings* { :PuSg 650989 } :foo )")); + } + + @Test public void testHoldings() { + Context ctx = step("(def VILLAIN (address \""+VILLAIN.toHexString()+"\"))"); + assertTrue(eval(ctx,"VILLAIN") instanceof Address); + ctx=step(ctx,"(def NOONE (address 7777777))"); + + // Basic empty holding should match empty blobmap in account record. See #131 + assertTrue(evalB("(= *holdings* (:holdings (account *address*)) (blob-map))")); + + // initial holding behaviour + assertNull(eval(ctx,"(get-holding VILLAIN)")); + assertCastError(step(ctx,"(get-holding :foo)")); + assertCastError(step(ctx,"(get-holding nil)")); + assertNobodyError(step(ctx,"(get-holding NOONE)")); + + // OK to set holding for a real owner account + assertEquals(100L,evalL(ctx,"(set-holding VILLAIN 100)")); + + // error to set holding for a non-existent owner account + assertNobodyError(step(ctx,"(set-holding NOONE 200)")); + + // trying to set holding for the wrong type + assertCastError(step(ctx,"(set-holding :foo 300)")); + + { // test simple assign + Context c2 = step(ctx,"(set-holding VILLAIN 123)"); + assertEquals(123L,evalL(c2,"(get-holding VILLAIN)")); + + assertTrue(c2.getAccountStatus(VILLAIN).getHoldings().containsKey(HERO)); + assertCVMEquals(123L,c2.getAccountStatus(VILLAIN).getHolding(HERO)); + } + + { // test null assign + Context c2 = step(ctx,"(set-holding VILLAIN nil)"); + assertFalse(c2.getAccountStatus(VILLAIN).getHoldings().containsKey(HERO)); + } + } + + @Test + public void testSymbolFor() { + assertEquals(Symbols.COUNT, Core.symbolFor(Core.COUNT)); + assertThrows(Throwable.class, () -> Core.symbolFor(null)); + } + + @Test + public void testCoreFormatRoundTrip() throws BadFormatException { + { // a core function + ACell c = eval("count"); + Blob b = Format.encodedBlob(c); + assertSame(c, Format.read(b)); + } + + { // a core macro + ACell c = eval("*initial-expander*"); + Blob b = Format.encodedBlob(c); + assertSame(c, Format.read(b)); + } + + { // a basic lambda expression + ACell c = eval("(fn [x] x)"); + Blob b = Format.encodedBlob(c); + assertEquals(c, Format.read(b)); + } + } + +} diff --git a/convex-core/src/test/java/convex/core/lang/DataStructuresTest.java b/convex-core/src/test/java/convex/core/lang/DataStructuresTest.java new file mode 100644 index 000000000..3df2130a2 --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/DataStructuresTest.java @@ -0,0 +1,63 @@ +package convex.core.lang; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import convex.core.State; +import convex.core.data.ACell; +import convex.core.data.ASequence; +import convex.core.data.ASet; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMLong; +import convex.core.init.InitTest; +import convex.core.util.Utils; + +/** + * Tests for medium sized data structure operations. + * + */ +public class DataStructuresTest { + + private static final State INITIAL = TestState.STATE; + private static final long INITIAL_JUICE = 100000; + private static final Context INITIAL_CONTEXT; + + static { + try { + INITIAL_CONTEXT = Context.createInitial(INITIAL, InitTest.HERO, INITIAL_JUICE); + } catch (Throwable e) { + throw new Error(e); + } + } + + public T eval(String source) { + try { + Context c = INITIAL_CONTEXT; + AOp op = TestState.compile(c, source); + Context rc = c.execute(op); + return rc.getResult(); + } catch (Exception e) { + throw Utils.sneakyThrow(e); + } + } + + @Test + public void testSetRoundTripRegression() { + ASet a = eval("#{1,8,0,4,9,5,2,3,7,6}"); + assertEquals(10, a.count()); + ASequence b = RT.sequence(a); + assertEquals(10, b.count()); + ASet c = RT.castSet(b); + assertEquals(a, c); + } + + @Test + public void testRefCounts() { + assertEquals(0, Utils.totalRefCount(Vectors.empty())); + assertEquals(2, Utils.totalRefCount(Vectors.of(1, 2))); + assertEquals(1, Utils.totalRefCount(eval("(fn [a] a)"))); // 1 Ref in params [symbol] + assertEquals(6, Utils.totalRefCount(eval("[[1 2] [3 4]]"))); // 6 vector element Refs + } + +} diff --git a/convex-core/src/test/java/convex/core/lang/DocsTest.java b/convex-core/src/test/java/convex/core/lang/DocsTest.java new file mode 100644 index 000000000..cbfcf1e3b --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/DocsTest.java @@ -0,0 +1,58 @@ +package convex.core.lang; + +import static convex.core.lang.TestState.step; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Map.Entry; + +import org.junit.jupiter.api.Test; + +import convex.core.data.ACell; +import convex.core.data.AHashMap; +import convex.core.data.AVector; +import convex.core.data.Keywords; +import convex.core.data.Symbol; + +public class DocsTest { + public static final boolean PRINT_MISSING=true; + + @Test public void testDocs() { + for (Entry> me: Core.METADATA.entrySet()) { + Symbol sym=me.getKey(); + AHashMap meta = me.getValue(); + if (meta.isEmpty()) { + if (PRINT_MISSING) System.err.println("Empty metadata in Core: "+sym); + } else { + @SuppressWarnings("unchecked") + AHashMap doc=(AHashMap) meta.get(Keywords.DOC); + if (doc==null) { + if (PRINT_MISSING) System.err.println("No documentation in Core: "+sym); + } else { + doDocTest(sym,doc); + } + } + } + } + + public void doDocTest(Symbol sym,AHashMap doc) { + ACell desc = doc.get(Keywords.DESCRIPTION); + if (desc == null) { + if (PRINT_MISSING) System.err.println("No description on Core def: " + sym); + } + + @SuppressWarnings("unchecked") + AVector> examples=(AVector>) doc.get(Keywords.EXAMPLES); + if (examples!=null) { + for (AHashMap ex:examples) { + doExampleTest(sym,ex); + } + } + } + + private void doExampleTest(Symbol sym, AHashMap ex) { + String code=RT.jvm( ex.get(Keywords.CODE)); + + Context ctx=step(code); + assertNotNull(ctx); + } +} diff --git a/convex-core/src/test/java/convex/core/lang/GenTestCode.java b/convex-core/src/test/java/convex/core/lang/GenTestCode.java new file mode 100644 index 000000000..554684d4e --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/GenTestCode.java @@ -0,0 +1,63 @@ +package convex.core.lang; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Random; + +import com.pholser.junit.quickcheck.From; +import com.pholser.junit.quickcheck.Property; + +import convex.core.data.ACell; +import convex.core.data.Syntax; +import convex.core.exceptions.ParseException; +import convex.core.init.InitTest; +import convex.test.generators.FormGen; + +public class GenTestCode { + + @Property + public void testExpand(@From(FormGen.class) ACell form) { + Context ctx = Context.createFake(TestState.STATE, InitTest.HERO); + ctx = ctx.expand(form); + + if (!ctx.isExceptional()) { + ACell expObject=ctx.getResult(); + assertTrue(expObject instanceof Syntax); + + ctx=ctx.compile((Syntax) expObject); + + if (!ctx.isExceptional()) { + Object compObject=ctx.getResult(); + assertTrue(compObject instanceof AOp); + + ctx=ctx.execute((AOp) compObject); + } + } + + String s=RT.str(form); + doMutateTest(s); + } + + + @SuppressWarnings("unused") + public void doMutateTest(String original) { + StringBuffer sb=new StringBuffer(original); + Random r=new Random(original.hashCode()); + + int n=r.nextInt(3); + switch (n) { + case 0: sb.deleteCharAt(r.nextInt(sb.length())); break; + case 1: sb.insert(r.nextInt(sb.length()+1),sb.charAt(r.nextInt(sb.length()))); break; + case 2: sb.setCharAt(r.nextInt(sb.length()),sb.charAt(r.nextInt(sb.length()))); break; + default: + } + + try { + String source=sb.toString(); + ACell newForm=Reader.read(source); + Syntax newSyntax=Reader.readSyntax(source); + } catch (ParseException p) { + // OK, we broken the string + } + } +} diff --git a/convex-core/src/test/java/convex/core/lang/GenTestCore.java b/convex-core/src/test/java/convex/core/lang/GenTestCore.java new file mode 100644 index 000000000..5f7819e69 --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/GenTestCore.java @@ -0,0 +1,220 @@ +package convex.core.lang; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import static convex.test.Assertions.*; + +import org.junit.runner.RunWith; + +import com.pholser.junit.quickcheck.From; +import com.pholser.junit.quickcheck.Property; +import com.pholser.junit.quickcheck.generator.java.lang.LongGenerator; +import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; + +import convex.core.data.ACell; +import convex.core.data.ADataStructure; +import convex.core.data.AList; +import convex.core.data.ASequence; +import convex.core.data.ASet; +import convex.core.data.AString; +import convex.core.data.AVector; +import convex.core.data.Address; +import convex.core.data.Blob; +import convex.core.data.BlobsTest; +import convex.core.data.Lists; +import convex.core.data.MapEntry; +import convex.core.data.Sets; +import convex.core.data.Strings; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMBool; +import convex.core.data.prim.CVMByte; +import convex.core.data.prim.CVMLong; +import convex.core.util.Utils; +import convex.test.generators.AddressGen; +import convex.test.generators.ListGen; +import convex.test.generators.SetGen; +import convex.test.generators.VectorGen; + +/** + * Set of generative tests for Runtime functions + * + * Generally grouped according to generated input types. The idea is to generate random sets of paramter + * values and check that RT / core functions behave as expected. + */ +@RunWith(JUnitQuickcheck.class) +public class GenTestCore { + + private void doDataStructureTests(ADataStructure a) { + long n=RT.count(a); + + assertTrue(RT.bool(a)); + + ASet uniqueVals=RT.castSet(a); + long ucount=RT.count(uniqueVals); + assertTrue(ucount<=n); + + if (n>0) { + assertTrue(ucount>0); + } else { + assertSame(a.empty(),a); + } + } + + private void doSequenceTests(ASequence a) { + doDataStructureTests(a); + + long n=RT.count(a); + + assertSame(a,RT.sequence(a)); + + // bounds exceptions + assertThrows(IndexOutOfBoundsException.class,()->RT.nth(a,n)); + assertThrows(IndexOutOfBoundsException.class,()->RT.nth(a,-1)); + } + + /** + * Tests for objects that can be coerced into sequences + * @param a + */ + private void doSequenceableTests(ACell a) { + ASequence seq=RT.sequence(a); + doSequenceTests(seq); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Property + public void testListFunctions(@From(ListGen.class) AList a) { + doSequenceTests(a); + + assertSame(Lists.empty(),a.empty()); + + AString foos=Strings.create("foo"); + ASequence ca=RT.cons(foos, a); + assertEquals(foos,ca.get(0)); + // assertEquals(a,RT.next(ca)); // TODO BUG: broken for big lists / vectors + assertEquals(ca,a.conj(foos)); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Property + public void testVectorFunctions(@From(VectorGen.class) AVector a) { + doSequenceTests(a); + + if (!(a instanceof MapEntry)) { + // only true for regular vectors + assertSame(a,RT.vec(a)); + } + assertSame(Vectors.empty(),a.empty()); + + AString foos=Strings.create("foo"); + long n=RT.count(a); + AVector ca=a.conj(foos); + assertEquals(foos,RT.nth(ca,n)); + } + + private void doAddressTests(Address a) { + assertSame(a,RT.castAddress(a)); + long n=RT.count(a); + + Blob b=a.toBlob(); + assertEquals(b,RT.castBlob(a)); + assertEquals(a.toHexString(),b.toHexString()); + + // Check a byte in the Address + assertSame(CVMByte.create(a.byteAt(6)),RT.nth(a, 6)); + + assertThrows(IndexOutOfBoundsException.class,()->RT.nth(a,-1)); + assertThrows(IndexOutOfBoundsException.class,()->RT.nth(a,n)); + + BlobsTest.doBlobTests(a); + } + + @Property + public void testAddressFunctions(@From(AddressGen.class) Address a) { + doAddressTests(a); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Property + public void testSetFunctions(@From(SetGen.class) ASet a) { + doSequenceableTests(a); + + assertSame(a,RT.castSet(a)); + assertSame(Sets.empty(),a.empty()); + + assertEquals(a,RT.castSet(RT.sequence(a))); + + long n=RT.count(a); + AString key=Strings.create("newkey1"); + // loop until we have a key not in set + while(a.contains(key)) { + key=Strings.create("newkey"+(Integer.parseInt(key.toString().substring(6))+1)); + } + + ASet ca=a.conj(key); + assertSame(CVMBool.TRUE,RT.get(ca, key)); + assertEquals(n+1, RT.count(ca)); + assertFalse(a.containsKey(key)); + assertTrue(ca.containsKey(key)); + } + + @Property + public void testLongFunctions(@From(LongGenerator.class) Long a) { + CVMLong ca=CVMLong.create(a); + + long v=a; + assertEquals(Long.toString(v),RT.str(ca).toString()); + assertSame(CVMByte.create(v),RT.castByte(ca)); + assertCVMEquals((char)v,RT.toCharacter(ca)); + assertCVMEquals(v+1,RT.inc(ca)); + assertCVMEquals(v-1,RT.dec(ca)); + assertCVMEquals(0,RT.compare(a,(Long)v)); + assertCVMEquals(-1,RT.compare((long)a,v+10)); + assertCVMEquals(1,RT.compare((long)a,v-10)); + + CVMLong[] args=new CVMLong[] {ca}; + assertEquals(-v,RT.minus(args).longValue()); + assertEquals(v,RT.plus(args).longValue()); + assertEquals(v,RT.times(args).longValue()); + assertEquals(1.0/v,RT.divide(args).doubleValue()); + + assertTrue(RT.lt(args)); + assertTrue(RT.gt(args)); + assertTrue(RT.le(args)); + assertTrue(RT.ge(args)); + assertTrue(RT.eq(args)); + + assertTrue(Utils.bool(a)); // longs are always truthy + + assertNull(RT.count(a)); + assertNull(RT.vec(a)); + } + + @Property + public void testLongMaths(@From(LongGenerator.class) Long a, @From(LongGenerator.class) Long b) { + assertEquals(RT.compare(a, b),-RT.compare(b,a)); + + CVMLong ca=CVMLong.create(a); + CVMLong cb=CVMLong.create(b); + + + CVMLong[] args=new CVMLong[] {ca,cb}; + assertEquals(a+b,RT.plus(args).longValue()); + assertEquals(a*b,RT.times(args).longValue()); + assertEquals(a-b,RT.minus(args).longValue()); + assertEquals(((double)a)/((double)b),RT.divide(args).doubleValue()); + + assertEquals(ab,RT.gt(args)); + assertEquals(a<=b,RT.le(args)); + assertEquals(a>=b,RT.ge(args)); + assertEquals(a==b,RT.eq(args)); + // assertEquals(a!=b,RT.ne(args)); // TODO: do we need this? + } + +} diff --git a/convex-core/src/test/java/convex/core/lang/GenTestRT.java b/convex-core/src/test/java/convex/core/lang/GenTestRT.java new file mode 100644 index 000000000..3dd636c62 --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/GenTestRT.java @@ -0,0 +1,44 @@ +package convex.core.lang; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.junit.runner.RunWith; + +import com.pholser.junit.quickcheck.From; +import com.pholser.junit.quickcheck.Property; +import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; + +import convex.core.data.ACell; +import convex.core.data.ACollection; +import convex.core.data.ASet; +import convex.test.generators.CollectionGen; +import convex.test.generators.ValueGen; + +@RunWith(JUnitQuickcheck.class) +public class GenTestRT { + + @SuppressWarnings("rawtypes") + @Property + public void setConversion(@From(CollectionGen.class) ACollection a) { + long ac = a.count(); + ASet set = RT.castSet(a); + assertTrue(set.count() <= ac); + for (Object o : a) { + assertTrue(set.contains(o)); + } + } + + @Property + public void strTest(@From(ValueGen.class) ACell b) { + String s = RT.str(b); + assertNotNull(s); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Property + public void conjTest(@From(CollectionGen.class) ACollection a, @From(ValueGen.class) ACell b) { + ACollection ac = a.conj(b); + assertTrue(ac.contains(b)); + } +} diff --git a/convex-core/src/test/java/convex/core/lang/JuiceTest.java b/convex-core/src/test/java/convex/core/lang/JuiceTest.java new file mode 100644 index 000000000..421eb4579 --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/JuiceTest.java @@ -0,0 +1,173 @@ +package convex.core.lang; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import convex.core.data.ACell; + +/** + * Tests for expected juice costs + * + * These are not your regular example based tests. These are handcrafted, + * artisinal juice tests. + */ +public class JuiceTest extends ACVMTest { + + public JuiceTest() { + super(TestState.STATE); + } + + private long JUICE = context().getJuice(); + + /** + * Compute the precise juice consumed by executing the compiled source code + * (i.e. this excludes the code of expansion+compilation). + * + * @param source + * @return Juice consumed + */ + public long juice(String source) { + ACell form = Reader.read(source); + AOp op = context().expandCompile(form).getResult(); + Context jctx = context().execute(op); + return JUICE - jctx.getJuice(); + } + + /** + * Compute the precise juice consumed by compiling the source code (i.e. the + * cost of expand+compilation). + * + * @param source + * @return Juice consumed + */ + public long compileJuice(String source) { + ACell form = Reader.read(source); + Context jctx = context().expandCompile(form); + return JUICE - jctx.getJuice(); + } + + /** + * Compute the precise juice consumed by expanding the source code (i.e. the + * cost of initial expander execution). + * + * @param source + * @return Juice consumed + */ + public long expandJuice(String source) { + ACell form = Reader.read(source); + Context jctx = context().invoke(Core.INITIAL_EXPANDER,form, Core.INITIAL_EXPANDER); + return JUICE - jctx.getJuice(); + } + + /** + * Returns the difference in juice consumed between two sources + * + * @param a + * @param b + * @return Difference in juice consumed + */ + public long juiceDiff(String a, String b) { + return juice(b) - juice(a); + } + + @Test + public void testSimpleValues() { + assertEquals(Juice.CONSTANT, juice("1")); + assertEquals(Juice.LOOKUP_SYM, juice("count")); + assertEquals(Juice.DO, juice("(do)")); + } + + @Test + public void testFunctionCalls() { + assertEquals(Juice.LOOKUP_SYM + Juice.EQUALS, juice("(=)")); + } + + @Test + public void testCompileJuice() { + assertEquals(Juice.EXPAND_CONSTANT + Juice.COMPILE_CONSTANT, compileJuice("1")); + assertEquals(Juice.EXPAND_CONSTANT + Juice.COMPILE_CONSTANT, compileJuice("[]")); + + assertEquals(Juice.EXPAND_CONSTANT + Juice.COMPILE_LOOKUP, compileJuice("foobar")); + } + + @Test + public void testExpandJuice() { + assertEquals(Juice.EXPAND_CONSTANT, expandJuice("1")); + assertEquals(Juice.EXPAND_CONSTANT, expandJuice("[]")); + assertEquals(Juice.EXPAND_SEQUENCE + Juice.EXPAND_CONSTANT * 4, expandJuice("(= 1 2 3)")); + assertEquals(Juice.EXPAND_SEQUENCE + Juice.EXPAND_CONSTANT * 3, expandJuice("[1 2 3]")); // [1 2 3] -> (vector 1 + // 2 3) + } + + @Test + public void testEval() { + {// eval for a single constant + long j = juice("(eval 1)"); + assertEquals((Juice.EVAL + Juice.LOOKUP_SYM + Juice.CONSTANT) + Juice.EXPAND_CONSTANT + Juice.COMPILE_CONSTANT + + Juice.CONSTANT, j); + + // expand list with symbol and number literal + long je = expandJuice("(eval 1)"); + assertEquals((Juice.EXPAND_SEQUENCE + Juice.EXPAND_CONSTANT * 2), je); + + // compile node with constant and symbol lookup + long jc = compileJuice("(eval 1)"); + assertEquals(je + (Juice.COMPILE_NODE + Juice.COMPILE_CONSTANT + Juice.COMPILE_LOOKUP), jc); + } + + // Calculate cost of executing op to build a single element vector, need this + // later + long oneElemVectorJuice = juice("[1]"); + // (vector 1), where vector is a constant core function. + assertEquals((Juice.CONSTANT + Juice.BUILD_DATA + Juice.BUILD_PER_ELEMENT + Juice.CONSTANT), + oneElemVectorJuice); + + {// eval for a small vector + long j = juice("(eval [1])"); + long exParams = (Juice.LOOKUP_SYM + oneElemVectorJuice); // prepare call (lookup 'eval', build 1-vector arg) + long exCompile = compileJuice("[1]"); // cost of compiling [1] + long exInvoke = (Juice.EVAL + oneElemVectorJuice); // cost of eval plus cost of running [1] + assertEquals(exParams + exCompile + exInvoke, j); + } + + { + long jdiffSimple = juiceDiff("[1]", "[1 2]"); + assertEquals(Juice.BUILD_PER_ELEMENT + Juice.CONSTANT, jdiffSimple); // extra cost per element in execution + + long jdiff = juiceDiff("(eval [1])", "(eval [1 2])"); + + // we pay +1 simple cost preparing args eval call, and +1 in ecexution phase. + // One extra constant in expand and compile phase. + assertEquals(Juice.EXPAND_CONSTANT + Juice.COMPILE_CONSTANT + jdiffSimple * 2, jdiff); + } + } + + @Test + public void testDef() { + assertEquals(Juice.DEF + Juice.CONSTANT, juice("(def a 1)")); + } + + @Test + public void testReturn() { + assertEquals(Juice.RETURN + Juice.CONSTANT + Juice.LOOKUP_SYM, juice("(return :foo)")); + } + + @Test + public void testHalt() { + assertEquals(Juice.RETURN + Juice.CONSTANT + Juice.LOOKUP_SYM, juice("(halt 123)")); + } + + @Test + public void testRollback() { + assertEquals(Juice.RETURN + Juice.CONSTANT + Juice.LOOKUP_SYM, juice("(rollback 123)")); + } + + @Test + public void testLoopIteration() { + long j1 = juice("(loop [i 2] (cond (> i 0) (recur (dec i)) :end))"); + long j2 = juice("(loop [i 3] (cond (> i 0) (recur (dec i)) :end))"); + assertEquals(Juice.COND_OP + (Juice.LOOKUP_SYM * 3) + ((Juice.LOOKUP)*2) + Juice.CONSTANT * 1 + Juice.ARITHMETIC + Juice.NUMERIC_COMPARE + + Juice.RECUR, j2 - j1); + } +} diff --git a/convex-core/src/test/java/convex/core/lang/NumericsTest.java b/convex-core/src/test/java/convex/core/lang/NumericsTest.java new file mode 100644 index 000000000..249e6b7ac --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/NumericsTest.java @@ -0,0 +1,307 @@ +package convex.core.lang; + +import static convex.core.lang.TestState.eval; +import static convex.core.lang.TestState.evalB; +import static convex.core.lang.TestState.evalD; +import static convex.core.lang.TestState.evalL; +import static convex.core.lang.TestState.step; +import static convex.test.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import convex.core.data.prim.CVMByte; +import convex.core.data.prim.CVMDouble; + +/** + * + * "You know, people think mathematics is complicated. Mathematics is the simple + * bit. Its the stuff we can understand. Its cats that are complicated. I mean, + * what is it in those little molecules and stuff that make one cat behave + * differently than another, or that make a cat? And how do you define a cat? I + * have no idea." + * + * - John Conway + */ +public class NumericsTest { + + @Test + public void testIncDec() { + assertEquals(1L, evalL("(inc (byte 256))")); + assertEquals(2L, evalL("(inc 1)")); + assertEquals(3L, evalL("(dec 4)")); + assertEquals(4L, evalL("(inc (dec 4))")); + + assertCastError(step("(inc nil)")); + assertCastError(step("(dec nil)")); + assertCastError(step("(inc :foo)")); + + } + + @Test + public void testPlus() { + assertEquals(0L, evalL("(+)")); + assertEquals(1L, evalL("(+ 1)")); + assertEquals(2L, evalL("(+ 3 -1)")); + assertEquals(3L, evalL("(+ 1 2)")); + assertEquals(4L, evalL("(+ 0 1 2 1)")); + + // long wrap round 64-bit signed + assertEquals(-2, evalL("(+ 9223372036854775807 9223372036854775807)")); + assertEquals(0, evalL("(+ -9223372036854775808 -9223372036854775808)")); + + } + + @Test + public void testPlusDouble() { + assertEquals(1.0, evalD("(+ 1.0)")); + assertEquals(2.0, evalD("(+ 3 -1.0)")); + assertEquals(3.0, evalD("(+ 1.0 2)")); + assertEquals(4.0, evalD("(+ 0 1 2.0 1)")); + + assertCastError(step("(+ nil)")); + assertCastError(step("(+ 1.0 :foo)")); + assertCastError(step("(+ 1.0 nil 2)")); + } + + @Test + public void testMinus() { + assertEquals(1L, evalL("(- -1)")); + assertEquals(2L, evalL("(- 3 1)")); + assertEquals(3L, evalL("(- 6 1 2)")); + assertEquals(4L, evalL("(- 10 1 -2 7)")); + + assertCastError(step("(- nil)")); + assertCastError(step("(- 1 [])")); + } + + @Test + public void testDivisionConsistency() { + // Check consistency of quot and rem + assertNotError(step("(map (fn [[a b]] (or (== a (+ (* b (quot a b)) (rem a b) ) ) (fail [a b])) ) [[10 3] [-10 3] [10 -3] [-10 -3] [10000000 1] [1 10000000]])")); + + // Check modular behaviour (mod a b) == (mod (mod a b) b) + assertNotError(step("(map (fn [[a b]] (or (== (mod a b) (mod (mod a b) b)) (fail [a b])) ) [[10 3] [-10 3] [10 -3] [-10 -3] [10000000 1] [1 10000000]])")); + } + + @Test + public void testMinusDouble() { + assertEquals(1.0, evalD("(- -1.0)")); + assertEquals(2.0, evalD("(- 3 1.0)")); + assertEquals(3.0, evalD("(- 6.0 1 2)")); + assertEquals(4.0, evalD("(- 10 1.0 -2 7)")); + assertEquals(5.0, evalD("(- 7.5 2.5)")); + } + + @Test + public void testMinusNoArgs() { + assertArityError(step("(-)")); + } + + @Test + public void testTimes() { + assertEquals(0L, evalL("(* 0 10)")); + assertEquals(1L, evalL("(*)")); + assertEquals(20L, evalL("(* 2 10)")); + assertEquals(120L, evalL("(* 1 2 3 4 5)")); + + // long wrap round 64 bits + assertEquals(0L, evalL("(* 65536 65536 65536 65536)")); + assertEquals(Long.MIN_VALUE, evalL("(* 32768 65536 65536 65536)")); + + assertCastError(step("(* nil)")); + assertCastError(step("(* :foo)")); + } + + @Test + public void testTimesDouble() { + assertEquals(0.0, evalD("(* 0 10.0)")); + assertEquals(5.0, evalD("(* 0.5 10)")); + + assertEquals(50.0, evalD("(* 10 2.5 2)")); + + assertEquals(2.25, evalD("(* -1.5 -1.5)")); + } + + @Test + public void testDouble() { + assertEquals(1.0, evalD("1.0")); + assertEquals(1.0, evalD("(double 1)")); + assertEquals(-1.0, evalD("-1.0")); + assertEquals(Double.NaN, evalD("(double ##NaN)")); + } + + @Test + public void testDivide() { + assertEquals(0.5, evalD("(/ 2.0)"), 0); + assertEquals(0.5, evalD("(/ 1 2.0)"), 0); + assertEquals(0.25, evalD("(/ 1 2 2)"), 0); + assertEquals(-4.0, evalD("(/ 2 -0.5)"), 0); + assertEquals(0.5, evalD("(/ 2)"), 0); + + assertEquals(Double.NaN, evalD("(/ 0.0 0.0)"), 0); + assertEquals(Double.POSITIVE_INFINITY, evalD("(/ 2.0 0.0)"), 0); + assertEquals(Double.NEGATIVE_INFINITY, evalD("(/ -2.0 0.0)"), 0); + + assertCastError(step("(/ nil)")); + assertCastError(step("(/ 1 :foo)")); + assertCastError(step("(/ #7 #0)")); + assertCastError(step("(/ 'foo 1)")); + + assertArityError(step("(/)")); + } + + @Test + public void testNaNPropagation() { + assertEquals(Double.NaN, evalD("##NaN"), 0); + assertEquals(Double.NaN, evalD("(+ 1 ##NaN)"), 0); + assertEquals(Double.NaN, evalD("(/ ##NaN 2)"), 0); + assertEquals(Double.NaN, evalD("(* 1 ##NaN 3.0)"), 0); + } + + @Test + public void testNaNBehaviour() { + assertEquals(Double.NaN, evalD("##NaN"), 0); + assertTrue(evalB("(= ##NaN ##NaN)")); + + // match Java primitives for equality as IEE754. All NaN comparisons should be false + assertFalse(Double.NaN==Double.NaN); + assertFalse(evalB("(== ##NaN ##NaN)")); + assertFalse(evalB("(< ##NaN ##NaN)")); + assertFalse(evalB("(>= ##NaN ##NaN)")); + assertFalse(evalB("(>= ##NaN 1.0)")); + assertFalse(evalB("(< 1 ##NaN)")); + + // TODO: should this be in core? NaN is not equal to itself + // assertTrue(evalB("(!= ##NaN ##NaN)")); + } + + @Test + public void testInfinity() { + assertEquals(CVMDouble.POSITIVE_INFINITY, eval("(/ 1 0)")); + assertEquals(CVMDouble.NEGATIVE_INFINITY, eval("(/ -1 0)")); + + assertEquals(CVMDouble.NEGATIVE_INFINITY, eval("(- ##Inf)")); + assertEquals(CVMDouble.POSITIVE_INFINITY, eval("(- ##-Inf)")); + + assertTrue(evalB("(== ##Inf ##Inf)")); + assertFalse(evalB("(== ##Inf ##-Inf)")); + } + + @Test + public void testZero() { + assertTrue(evalB("(== 0 -0)")); + assertTrue(evalB("(== 0.0 -0.0)")); + assertTrue(evalB("(== 0 -0.0)")); + assertTrue(evalB("(<= 0 -0)")); + assertTrue(evalB("(>= 0 0)")); + + } + + @Test + public void testSignum() { + assertEquals(CVMDouble.NEGATIVE_ZERO,eval("(signum -0.0)")); + assertEquals(CVMDouble.ZERO,eval("(signum 0.0)")); + assertEquals(CVMDouble.ONE,eval("(signum 13.3)")); + assertEquals(CVMDouble.MINUS_ONE,eval("(signum (/ -1 0))")); + assertEquals(CVMDouble.ONE,eval("(signum ##Inf)")); + assertEquals(CVMDouble.NaN,eval("(signum (sqrt -1))")); + } + + + @Test + public void testSqrt() { + assertEquals(2.0, evalD("(sqrt 4.0)"), 0); + assertEquals(0.0, evalD("(sqrt 0.0)"), 0); + assertEquals(Double.NaN, evalD("(sqrt -3)"), 0); + assertEquals(Double.NaN, evalD("(sqrt ##NaN)"), 0); + + assertArityError(step("(sqrt)")); + assertArityError(step("(sqrt :foo :bar)")); // arity before cast error + + assertCastError(step("(sqrt :foo)")); + assertCastError(step("(sqrt nil)")); + + } + + @Test + public void testExp() { + assertEquals(1.0, evalD("(exp 0.0)"), 0); + + assertEquals(StrictMath.exp(1.0), evalD("(exp 1.0)")); + assertEquals(Math.E, evalD("(exp 1.0)"), 0.000001); + assertEquals(0.0, evalD("(exp -100000000.0)"), 0); + assertEquals(Double.POSITIVE_INFINITY, evalD("(exp 100000000)"), 0); + + assertArityError(step("(exp)")); + assertArityError(step("(exp 1 2)")); + } + + @Test + public void testPow() { + assertEquals(1.0, evalD("(pow 1.0 1.0)"), 0); + assertEquals(1.0, evalD("(pow 3.0 0.0)"), 0); + assertEquals(2.0, evalD("(pow 4 0.5)"), 0); + + assertEquals(StrictMath.pow(1.2, 3.5), evalD("(pow 1.2,3.5)")); + assertEquals(0.0, evalD("(pow 2 -100000000.0)"), 0); + assertEquals(Double.POSITIVE_INFINITY,evalD("(pow 3 100000000)"), 0); + + assertEquals(Double.NaN, evalD("(pow -1.0 1.5)"), 0); + assertEquals(-1.0, evalD("(pow -1.0 7)"), 0); + + assertArityError(step("(pow)")); + assertArityError(step("(pow 1)")); + assertArityError(step("(pow 1 2 3)")); + } + + @Test + public void testApply() { + assertEquals(6L, evalL("(apply + [1 2 3])"), 0); + assertEquals(2L, evalL("(apply inc [1])"), 0); + assertEquals(1L, evalL("(apply * [])"), 0); + assertEquals(0L, evalL("(apply + nil)"), 0); + } + + @Test + public void testHexCasts() { + assertEquals(3L, evalL("(+ (long 0x01) (byte 0x02))")); + + // byte cast wraps over + assertSame(CVMByte.create(-1), eval("(byte 0xFF)")); + + // check we are treating blobs as unsigned values + assertEquals(510L, evalL("(+ (long 0xFF) (long 0xFF))")); + assertEquals(-2L, evalL("(+ (long 0xFFFFFFFFFFFFFFFF) (long 0xFFFFFFFFFFFFFFFF))")); + + // take low order bytes of big long + assertEquals(-1L, evalL("(long 0x0000000000000000FFFFFFFFFFFFFFFF)")); + } + + @Test + public void testBadArgs() { + // Regression check for issue #89 + assertCastError(step("(+ 1 #42)")); + assertCastError(step("(+ #42 1.0)")); + } + + @Test + public void testCasts() { + assertEquals(0L, evalL("(long (byte 256))")); + assertEquals(13L, evalL("(long #13)")); + assertEquals(255L, evalL("(long 0xff)")); + assertEquals(1L, evalL("(long 1)")); + assertCVMEquals('a', eval("(char 97)")); + assertEquals(97L, evalL("(long \\a)")); + assertSame(CVMByte.create(1), eval("(byte 1)")); + } + + @Test + public void testBadStringCast() { + assertCastError(step("(inc (str 1))")); + } + +} diff --git a/convex-core/src/test/java/convex/core/lang/OpsTest.java b/convex-core/src/test/java/convex/core/lang/OpsTest.java new file mode 100644 index 000000000..772819e59 --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/OpsTest.java @@ -0,0 +1,302 @@ +package convex.core.lang; + +import static convex.test.Assertions.assertJuiceError; +import static convex.test.Assertions.assertUndeclaredError; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import convex.core.State; +import convex.core.data.ACell; +import convex.core.data.AMap; +import convex.core.data.AString; +import convex.core.data.Address; +import convex.core.data.ObjectsTest; +import convex.core.data.Symbol; +import convex.core.data.Syntax; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.InvalidDataException; +import convex.core.init.Init; +import convex.core.init.InitTest; +import convex.core.lang.impl.AClosure; +import convex.core.lang.impl.Fn; +import convex.core.lang.ops.Cond; +import convex.core.lang.ops.Constant; +import convex.core.lang.ops.Def; +import convex.core.lang.ops.Do; +import convex.core.lang.ops.Invoke; +import convex.core.lang.ops.Lambda; +import convex.core.lang.ops.Let; +import convex.core.lang.ops.Local; +import convex.core.lang.ops.Lookup; +import convex.core.lang.ops.Special; +import convex.core.util.Utils; + +/** + * Tests for ops functionality. + * + * In general, focused on unit testing special op capabilities. General on-chain + * behaviour should be covered elsewhere. + */ +public class OpsTest extends ACVMTest { + + protected OpsTest() { + super(InitTest.BASE); + } + + private final long INITIAL_JUICE = context().getJuice(); + + @Test + public void testConstant() { + Context c = context(); + + {// simple long constant + AOp op = Constant.of(10L); + Context c2 = c.fork().execute(op); + + assertEquals(INITIAL_JUICE - Juice.CONSTANT, c2.getJuice()); + assertEquals(CVMLong.create(10L), c2.getResult()); + doOpTest(op); + } + + {// null constant + AOp op = Constant.nil(); + Context c2 = c.fork().execute(op); + + assertEquals(INITIAL_JUICE - Juice.CONSTANT, c2.getJuice()); + assertNull(c2.getResult()); + doOpTest(op); + } + } + + @Test + public void testOutOfJuice() { + long JUICE = Juice.CONSTANT - 1; // insufficient juice to run operation + Context c = Context.createInitial(INITIAL, InitTest.HERO, JUICE); + + AOp op = Constant.of(10L); + assertJuiceError(c.execute(op)); + + doOpTest(op); + } + + @Test + public void testDef() { + Context c1 = context(); + + Symbol fooSym = Symbol.create("foo"); + AOp op = Def.create(Syntax.create(fooSym), Constant.createString("bar")); + + AMap env1 = c1.getEnvironment(); + Context c2 = c1.execute(op); + AMap env2 = c2.getEnvironment(); + + assertNotEquals(env1, env2); + + assertNull(env1.get(fooSym)); // initially no entry + assertEquals("bar", env2.get(fooSym).toString()); + + long expectedJuice = INITIAL_JUICE - Juice.CONSTANT - Juice.DEF; + assertEquals(expectedJuice, c2.getJuice()); + assertEquals("bar", c2.getResult().toString()); + + AOp lookupOp = Lookup.create(Symbol.create("foo")); + Context c3 = c2.execute(lookupOp); + expectedJuice -= Juice.LOOKUP_DYNAMIC; + assertEquals(expectedJuice, c3.getJuice()); + assertEquals("bar", c3.getResult().toString()); + + doOpTest(op); + doOpTest(lookupOp); + } + + @Test + public void testUndeclaredLookup() { + Context c = context(); + AOp op = Lookup.create("missing-symbol"); + assertUndeclaredError(c.execute(op)); + + doOpTest(op); + } + + @Test + public void testDo() { + Context c = context(); + + AOp op = Do.create(Def.create("foo", Constant.createString("bar")), Lookup.create("foo")); + + Context c2 = c.execute(op); + long expectedJuice = INITIAL_JUICE - (Juice.CONSTANT + Juice.DEF + Juice.LOOKUP_DYNAMIC + Juice.DO); + assertEquals(expectedJuice, c2.getJuice()); + assertEquals("bar", c2.getResult().toString()); + + doOpTest(op); + } + + @Test + public void testSpecial() { + Context c = context(); + + AOp
op = Special.forSymbol(Symbols.STAR_ADDRESS); + assertEquals(op,Special.forSymbol(Symbol.create("*address*"))); // double check lookup in hash map + + Context
c2 = c.execute(op); + assertEquals(c2.getAddress(), c2.getResult()); + + doOpTest(op); + } + + @Test + public void testLet() { + Context c = context(); + AOp op = Let.create(Vectors.of(Symbols.FOO), + Vectors.of(Constant.createString("bar"), Local.create(0)), false); + Context c2 = c.execute(op); + assertEquals("bar", c2.getResult().toString()); + + doOpTest(op); + } + + @Test + public void testCondTrue() { + Context c = context(); + + AOp op = Cond.create(Constant.of(true), Constant.createString("trueResult"), + Constant.createString("falseResult")); + + Context c2 = c.execute(op); + + assertEquals("trueResult", c2.getResult().toString()); + long expectedJuice = INITIAL_JUICE - (Juice.COND_OP + Juice.CONSTANT + Juice.CONSTANT); + assertEquals(expectedJuice, c2.getJuice()); + + doOpTest(op); + } + + @Test + public void testCondFalse() { + Context c = context(); + + AOp op = Cond.create(Constant.of(false), Constant.createString("trueResult"), + Constant.createString("falseResult")); + + Context c2 = c.execute(op); + + assertEquals("falseResult", c2.getResult().toString()); + long expectedJuice = INITIAL_JUICE - (Juice.COND_OP + Juice.CONSTANT + Juice.CONSTANT); + assertEquals(expectedJuice, c2.getJuice()); + + doOpTest(op); + } + + @Test + public void testCondNoResult() { + Context c = context(); + + AOp op = Cond.create(Constant.of(false), Constant.createString("trueResult")); + + Context c2 = c.execute(op); + + assertNull(c2.getResult()); + long expectedJuice = INITIAL_JUICE - (Juice.COND_OP + Juice.CONSTANT); + assertEquals(expectedJuice, c2.getJuice()); + + doOpTest(op); + } + + @Test + public void testCondEnvironmentChange() { + Context c = context(); + + Symbol sym = Symbol.create("val"); + + AOp op = Cond.create(Do.create(Def.create(sym, Constant.of(false)), Constant.of(false)), + Constant.createString("1"), Lookup.create(sym), Constant.of("2"), + Do.create(Def.create(sym, Constant.of(true)), Constant.of(false)), Constant.of("3"), + Lookup.create(sym), Constant.of("4"), Constant.of("5")); + + Context c2 = c.execute(op); + assertEquals("4", c2.getResult().toString()); + + doOpTest(op); + } + + @Test + public void testInvoke() { + Context c = context(); + + Symbol sym = Symbol.create("arg0"); + + Invoke op = Invoke.create(Lambda.create(Vectors.of(sym), Local.create(0)), + Constant.createString("bar")); + + Context c2 = c.execute(op); + assertEquals("bar", c2.getResult().toString()); + + doOpTest(op); + } + + @Test + public void testLookup() throws InvalidDataException { + Lookup l1=Lookup.create("foo"); + assertNull(l1.getAddress()); + doOpTest(l1); + + Lookup l2=Lookup.create(Constant.of(Init.CORE_ADDRESS),"count"); + assertEquals(Constant.of(Init.CORE_ADDRESS),l2.getAddress()); + doOpTest(l2); + } + + @Test + public void testLocal() throws InvalidDataException { + Context c=Context.createFake(State.EMPTY); + c=c.withLocalBindings(Vectors.of(1337L)); + + Local op=Local.create(0); + c=c.execute(op); + assertEquals(RT.cvm(1337),c.getResult()); + + doOpTest(op); + } + + @Test + public void testLambda() { + Context c = context(); + + Symbol sym = Symbol.create("arg0"); + + Lambda lam = Lambda.create(Vectors.of(Syntax.create(sym)), Lookup.create(sym)); + + Context> c2 = c.execute(lam); + AClosure fn = c2.getResult(); + assertTrue(fn.hasArity(1)); + assertFalse(fn.hasArity(2)); + + doOpTest(lam); + } + + @Test + public void testLambdaString() { + Fn fn = Fn.create(Vectors.empty(), Constant.nil()); + assertEquals("(fn [] nil)",fn.toString()); + } + + public void doOpTest(AOp op) { + // Executing any Op should not throw + context().execute(op); + + try { + op.validate(); + } catch (InvalidDataException e) { + throw Utils.sneakyThrow(e); + } + + ObjectsTest.doAnyValueTests(op); + } + +} diff --git a/convex-core/src/test/java/convex/core/lang/ParamTestCasts.java b/convex-core/src/test/java/convex/core/lang/ParamTestCasts.java new file mode 100644 index 000000000..c47f3f308 --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/ParamTestCasts.java @@ -0,0 +1,64 @@ +package convex.core.lang; + +import static convex.core.lang.TestState.eval; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import convex.core.ErrorCodes; +import convex.core.data.ACell; +import convex.core.data.Sets; +import convex.core.data.Strings; +import convex.core.data.prim.CVMDouble; +import convex.core.data.prim.CVMLong; +import convex.test.Samples; + +public class ParamTestCasts { + + public final ACell[] values = new ACell[] { + CVMLong.ZERO, + CVMLong.ONE, + CVMLong.MAX_VALUE, + CVMLong.MIN_VALUE, + CVMDouble.NaN, + CVMDouble.POSITIVE_INFINITY, + CVMDouble.NEGATIVE_INFINITY, + CVMDouble.ZERO, + Strings.EMPTY, + Strings.create("foobar"), + Samples.ACCOUNT_KEY, + Samples.BAD_HASH, + Samples.INT_SET_300, + Samples.INT_VECTOR_23, + Samples.LONG_MAP_100, + null + }; + + + @SuppressWarnings("unchecked") + @ParameterizedTest + @ValueSource(strings = { "long","boolean","double","byte","char","blob","str","vec","set","blob","address"}) + public void testIdempotent(String name) { + ACell namedFn=eval(name); + + assertTrue(namedFn instanceof AFn); + AFn fn=(AFn)namedFn; + + Context ctx=TestState.CONTEXT.fork(); + + for (ACell x: values) { + ACell[] args= new ACell[] {x}; + Context r = ctx.fork().invoke(fn, args); + if (r.isExceptional()) { + ACell code=r.getExceptional().getCode(); + assertTrue(Sets.of(ErrorCodes.CAST,ErrorCodes.ARGUMENT).contains(code)); + } else { + ACell result=r.getResult(); + ACell result2=r.invoke(fn, args).getResult(); + assertEquals(result,result2); + } + } + } +} diff --git a/convex-core/src/test/java/convex/core/lang/ParamTestEvals.java b/convex-core/src/test/java/convex/core/lang/ParamTestEvals.java new file mode 100644 index 000000000..9ed7f95f1 --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/ParamTestEvals.java @@ -0,0 +1,122 @@ +package convex.core.lang; + +import static convex.test.Assertions.assertCVMEquals; +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.data.Blob; +import convex.core.data.Format; +import convex.core.data.Keyword; +import convex.core.exceptions.BadFormatException; +import convex.core.init.InitTest; +import convex.core.util.Utils; + +@RunWith(Parameterized.class) +public class ParamTestEvals { + + private static long INITIAL_JUICE = TestState.INITIAL_JUICE; + private static final Context INITIAL_CONTEXT = TestState.CONTEXT.fork(); + + private static final Address TEST_CONTRACT = TestState.CONTRACTS[0]; + + @Parameterized.Parameters(name = "{index}: {0}") + public static Collection dataExamples() { + return Arrays.asList(new Object[][] { + { "(do)", null }, + { "(do (do :foo))", Keyword.create("foo") }, + { "(do 1 2)", 2L }, + { "(do 1 *result*)", 1L }, + { "(do (do :foo) (do))", null }, + { "*result*", null }, + { "*origin*", InitTest.HERO }, + { "*caller*", null }, + { "*address*", InitTest.HERO }, + { "(do 1 *result*)", 1L }, + + { "(call " + TEST_CONTRACT + " (my-address))", TEST_CONTRACT }, + { "(call " + TEST_CONTRACT + " (foo))", Keyword.create("bar") }, + + { "(let [a (address " + TEST_CONTRACT + ")]" + "(call a (write :bar))" + "(call a (read)))", + Keyword.create("bar") }, + + { "*depth*", 0L }, // *depth* + { "(do *depth*)", 1L }, // do, *depth* + { "(let [a *depth*] a)", 1L }, // let, *depth* + { "(let [f (fn [] *depth*)] (f))", 2L }, // let, invoke, *depth* + + { "(let [])", null }, { "(let [a 1])", null }, { "(let [a 1] a)", 1L }, + { "(do (def a 2) (let [a 13] a))", 13L }, { "*juice*", INITIAL_JUICE }, + { "(- *juice* *juice*)", Juice.SPECIAL }, + { "((fn [a] a) 4)", 4L }, { "(do (def a 3) a)", 3L }, + { "(do (let [a 1] (def f (fn [] a))) (f))", 1L }, { "1", 1L }, { "(not true)", false }, + { "(= true true)", true } }); + } + + private String source; + private Object expectedResult; + + public ParamTestEvals(String source, Object expectedResult) { + this.source = source; + this.expectedResult = expectedResult; + } + + public AOp compile(String source) { + try { + Context c = INITIAL_CONTEXT.fork(); + AOp op = TestState.compile(c, source); + return op; + } catch (Exception e) { + throw Utils.sneakyThrow(e); + } + } + + public Context eval(AOp op) { + try { + Context c = INITIAL_CONTEXT.fork(); + Context rc = c.execute(op); + return rc; + } catch (Exception e) { + throw Utils.sneakyThrow(e); + } + } + + public Context eval(String source) { + try { + Context c = INITIAL_CONTEXT.fork(); + AOp op = TestState.compile(c, source); + return eval(op); + } catch (Exception e) { + throw Utils.sneakyThrow(e); + } + } + + @Test + public void testOpRoundTrip() throws BadFormatException { + AOp op = compile(source); + Blob b = Format.encodedBlob(op); + ACell.createPersisted(op); // persist to allow re-creation + + AOp op2 = Format.read(b); + Blob b2 = Format.encodedBlob(op2); + assertEquals(b, b2); + + ACell result = eval(op2).getResult(); + assertCVMEquals(expectedResult, result); + } + + @Test + public void testResultAndJuice() { + Context c = eval(source); + + Object result = c.getResult(); + assertCVMEquals(expectedResult, result); + } +} diff --git a/convex-core/src/test/java/convex/core/lang/ParamTestJuice.java b/convex-core/src/test/java/convex/core/lang/ParamTestJuice.java new file mode 100644 index 000000000..53cbe5247 --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/ParamTestJuice.java @@ -0,0 +1,126 @@ +package convex.core.lang; + +import static convex.test.Assertions.assertCVMEquals; +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import convex.core.State; +import convex.core.data.ACell; +import convex.core.data.Blob; +import convex.core.data.Format; +import convex.core.data.Keyword; +import convex.core.data.Lists; +import convex.core.data.Maps; +import convex.core.exceptions.BadFormatException; +import convex.core.init.InitTest; +import convex.core.util.Utils; + +@RunWith(Parameterized.class) +public class ParamTestJuice { + + private static final long JUICE_SYM_LOOKUP = Juice.LOOKUP_SYM; + private static final long JUICE_EMPTY_MAP = (Juice.BUILD_DATA + JUICE_SYM_LOOKUP); // consider: (hash-map) + private static final long JUICE_IDENTITY_FN = (Juice.LAMBDA); + + @Parameterized.Parameters(name = "{index}: {0}") + public static Collection dataExamples() { + return Arrays.asList(new Object[][] { + { "3", 3L, Juice.CONSTANT }, + { "'()", Lists.empty(), Juice.CONSTANT }, + { "{}", Maps.empty(), JUICE_EMPTY_MAP }, // (hash-map) + { "(hash-map)", Maps.empty(), JUICE_EMPTY_MAP }, // (hash-map) + { "(eval 1)", 1L, + (Juice.EVAL + JUICE_SYM_LOOKUP + Juice.CONSTANT) + Juice.EXPAND_CONSTANT + Juice.COMPILE_CONSTANT + + Juice.CONSTANT }, + { "(do)", null, Juice.DO }, { "({} 0 1)", 1L, JUICE_EMPTY_MAP + Juice.CONSTANT * 2 }, + { "(do (do :foo))", Keyword.create("foo"), Juice.DO * 2 + Juice.CONSTANT }, + { "(let [])", null, Juice.LET }, { "(cond)", null, Juice.COND_OP }, + { "(if 1 2 3)", 2L, Juice.COND_OP + 2 * Juice.CONSTANT }, + { "(fn [x] x)", eval("(fn [x] x)").getResult(), JUICE_IDENTITY_FN }, + { "(do (def a 3) a)", 3L, Juice.DO + Juice.CONSTANT + JUICE_SYM_LOOKUP + Juice.DEF }, + { "(do (let [a 1] (def f (fn [] a))) (f))", 1L, + Juice.DO + Juice.LET + Juice.CONSTANT * 1 + JUICE_SYM_LOOKUP + Juice.LOOKUP + JUICE_IDENTITY_FN + + Juice.DEF }, + { "(let [a 1] a)", 1L, Juice.LET + Juice.LOOKUP + Juice.CONSTANT }, { "~(+ 1 2)", 3L, Juice.CONSTANT }, // compiler + // executes + // + + // in + // advance, + // so + // this + // is + // constant + { "*depth*", 0L, Juice.SPECIAL }, + { "(= true true)", true, (1 * JUICE_SYM_LOOKUP) + (2 * Juice.CONSTANT) + Juice.EQUALS } }); + } + + private String source; + private long expectedJuice; + private Object expectedResult; + + public ParamTestJuice(String source, Object expectedResult, Long expectedJuice) { + this.source = source; + this.expectedJuice = expectedJuice; + this.expectedResult = expectedResult; + } + + private static final State INITIAL = TestState.STATE; + private static final long INITIAL_JUICE = 10000; + private static final Context INITIAL_CONTEXT; + + static { + try { + INITIAL_CONTEXT = Context.createInitial(INITIAL, InitTest.HERO, INITIAL_JUICE); + } catch (Throwable e) { + throw new Error(e); + } + } + + public static AOp compile(String source) { + try { + Context c = INITIAL_CONTEXT.fork(); + AOp op = TestState.compile(c, source); + return op; + } catch (Exception e) { + throw Utils.sneakyThrow(e); + } + } + + public static Context eval(String source) { + try { + Context c = INITIAL_CONTEXT.fork(); + AOp op = TestState.compile(c, source); + Context rc = c.fork().execute(op); + return rc; + } catch (Exception e) { + throw Utils.sneakyThrow(e); + } + } + + @Test + public void testOpRoundTrip() throws BadFormatException { + AOp op = compile(source); + Blob b = Format.encodedBlob(op); + AOp op2 = Format.read(b); + Blob b2 = Format.encodedBlob(op2); + assertEquals(b, b2); + } + + @Test + public void testResultAndJuice() { + Context c = eval(source); + + Object result = c.getResult(); + assertCVMEquals(expectedResult, result); + + long juiceUsed = INITIAL_JUICE - c.getJuice(); + assertEquals(expectedJuice, juiceUsed); + + } +} diff --git a/convex-core/src/test/java/convex/core/lang/ParamTestRTSequences.java b/convex-core/src/test/java/convex/core/lang/ParamTestRTSequences.java new file mode 100644 index 000000000..a16eaa63c --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/ParamTestRTSequences.java @@ -0,0 +1,82 @@ +package convex.core.lang; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static convex.test.Assertions.*; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import convex.core.data.ACell; +import convex.core.data.ACollection; +import convex.core.data.ASequence; +import convex.core.data.AVector; +import convex.core.data.List; +import convex.core.data.Lists; +import convex.core.data.MapEntry; +import convex.core.data.Maps; +import convex.core.data.Sets; +import convex.core.data.Vectors; + +/** + * Set of test for objects that can be treated as sequences + * + */ +@RunWith(Parameterized.class) +public class ParamTestRTSequences { + @Parameterized.Parameters(name = "{index}: {1}") + public static Collection dataExamples() { + return Arrays.asList(new Object[][] { { 0, null }, { 0, Vectors.empty() }, { 0, Lists.empty() }, + { 2, MapEntry.of(1L, 2L) }, { 2, MapEntry.of(Maps.of(1L, 2L), 2L) }, + { 2, MapEntry.of(null, 2L) }, { 3, Vectors.of(1L, 2L, 3L) }, { 2, List.of("foo", "bar") }, + { 3, Sets.of(null, 1L, 1.0) } }); + } + + private ACollection data; + private long expectedCount; + + public ParamTestRTSequences(int expectedCount, Object data) { + this.expectedCount = expectedCount; + this.data = (ACollection)data; + } + + @Test + public void testCount() { + assertEquals(expectedCount, RT.count(data)); + } + + @Test + public void testSeq() { + assertEquals(expectedCount, RT.count(RT.sequence(data))); + } + + @Test + public void testVec() { + AVector v = RT.vec(data); + assertEquals(expectedCount, v.count()); + if (expectedCount > 0) { + assertEquals(data.get(0), v.get(0)); + } + } + + @Test + public void testCons() { + ASequence a = RT.cons(RT.cvm("foo"), RT.sequence(data)); + assertCVMEquals("foo", a.get(0)); + assertCVMEquals("foo", RT.nth(a, 0)); + } + + @Test + public void testFirst() { + if (expectedCount > 0) { + ACell fst = data.get(0); + assertEquals(RT.nth(data, 0), fst); + } else { + if (data!=null) assertThrows(IndexOutOfBoundsException.class, () -> data.get(0)); + } + } +} diff --git a/convex-core/src/test/java/convex/core/lang/RTTest.java b/convex-core/src/test/java/convex/core/lang/RTTest.java new file mode 100644 index 000000000..66599b13b --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/RTTest.java @@ -0,0 +1,88 @@ +package convex.core.lang; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.junit.jupiter.api.Test; + +import convex.core.data.AList; +import convex.core.data.AVector; +import convex.core.data.Address; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.Lists; +import convex.core.data.Strings; +import convex.core.data.Symbol; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMDouble; +import convex.core.data.prim.CVMLong; + +/** + * Tests for RT functions. + * + * Normally should prefer testing via executing core environment functions, but + * these are useful for testing utility functions, edge cases and internal + * behaviour of the RT class itself. + */ +public class RTTest { + + @Test + public void testName() { + assertEquals("foo", RT.name(Symbol.create("foo")).toString()); + assertEquals("foo", RT.name(Keyword.create("foo")).toString()); + assertEquals("foo", RT.name(Strings.create("foo")).toString()); + + assertNull(RT.name(null)); + } + + @Test + public void testAddress() { + Address za = Address.create(0x7777777); + assertEquals(za, RT.castAddress(za.toBlob())); + assertSame(za, RT.castAddress(za)); + + // reading a hex address + assertEquals(Address.create(18),RT.castAddress(Strings.create("0000000000000012"))); // OK, hex string + assertNull(RT.castAddress(Strings.create("0012"))); // too short + + // Check null return values for invalid addresses + assertNull(RT.castAddress(null)); // null not allowed + assertNull(RT.castAddress(CVMLong.create(-1))); // negative ints not allowed + assertNull(RT.castAddress(Strings.create("xyz2030405060708090a0b0c0d0e0f1011121314"))); // bad format + } + + @Test + public void testSequence() { + AVector v = Vectors.of(1L, 2L, 3L); + AList l = Lists.of(1L, 2L, 3L); + assertSame(v, RT.sequence(v)); + assertSame(l, RT.sequence(l)); + assertSame(Vectors.empty(), RT.sequence(null)); + + // null return values if cast fails + assertNull(RT.sequence(Keywords.FOO)); // keywords not allowed + } + + @Test + public void testVec() { + AVector v = Vectors.of(1L, 2L, 3L); + AList l = Lists.of(1L, 2L, 3L); + assertEquals(Vectors.of(1L, 2L,3L), RT.vec(l.toCellArray())); + assertEquals(v, RT.vec(new java.util.ArrayList<>(v))); + + assertNull(RT.vec(1)); // ints not allowed + } + + @Test + public void testCVMCasts() { + assertEquals(CVMLong.create(1L),RT.cvm(1L)); + assertEquals(CVMDouble.create(0.17),RT.cvm(0.17)); + assertEquals(Strings.create("foo"),RT.cvm("foo")); + + // CVM objects shouldn't change + Keyword k=Keyword.create("test-key"); + assertSame(k,RT.cvm(k)); + + } +} diff --git a/convex-core/src/test/java/convex/core/lang/ReaderTest.java b/convex-core/src/test/java/convex/core/lang/ReaderTest.java new file mode 100644 index 000000000..759eb08e2 --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/ReaderTest.java @@ -0,0 +1,273 @@ +package convex.core.lang; + +import static convex.test.Assertions.assertCVMEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import convex.core.data.ACell; +import convex.core.data.AList; +import convex.core.data.Address; +import convex.core.data.Blob; +import convex.core.data.Blobs; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.Lists; +import convex.core.data.Maps; +import convex.core.data.Strings; +import convex.core.data.Symbol; +import convex.core.data.Syntax; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMBool; +import convex.core.data.prim.CVMDouble; +import convex.core.exceptions.ParseException; +import convex.test.Samples; + +public class ReaderTest { + + @Test + public void testVectors() { + assertSame(Vectors.empty(), Reader.read("[]")); + assertSame(Vectors.empty(), Reader.read(" [ ] ")); + + assertEquals(Vectors.of(1L,-2L), Reader.read("[1 -2]")); + + assertEquals(Vectors.of(Samples.FOO), Reader.read(" [ :foo ] ")); + assertEquals(Vectors.of(Vectors.empty()), Reader.read(" [ [] ] ")); + } + + @Test + public void testKeywords() { + assertEquals(Samples.FOO, Reader.read(":foo")); + assertEquals(Keyword.create("foo.bar"), Reader.read(":foo.bar")); + + // : is currently a valid symbol character + assertEquals(Keyword.create("foo:bar"), Reader.read(":foo:bar")); + + } + + @Test + public void testBadKeywords() { + assertThrows(Error.class, () -> Reader.read(":")); + } + + @Test + public void testComment() { + assertCVMEquals(1L, Reader.read(";this is a comment\n 1 \n")); + assertCVMEquals(Vectors.of(2L), Reader.read("[#_foo 2]")); + assertCVMEquals(Vectors.of(3L), Reader.read("[3 #_foo]")); + } + + @Test + public void testSymbols() { + assertEquals(Symbols.FOO, Reader.read("foo")); + assertEquals(Lists.of(Symbols.LOOKUP,Address.create(666),Symbols.FOO), Reader.read("#666/foo")); + + assertEquals(Lists.of(Symbol.create("+"), 1L), Reader.read("(+ 1)")); + assertEquals(Lists.of(Symbol.create("+a")), Reader.read("( +a )")); + assertEquals(Lists.of(Symbol.create("/")), Reader.read("(/)")); + assertEquals(Lists.of(Symbols.LOOKUP,Symbols.FOO,Symbols.BAR), Reader.read("foo/bar")); + assertEquals(Symbol.create("a*+!-_?<>=!"), Reader.read("a*+!-_?<>=!")); + assertEquals(Symbol.create("foo.bar"), Reader.read("foo.bar")); + assertEquals(Symbol.create(".bar"), Reader.read(".bar")); + + // Interpret leading dot as symbols always. Addresses Issue #65 + assertEquals(Symbol.create(".56"), Reader.read(".56")); + + // TODO: maybe this should be possible? + // namespaces cannot themselves be qualified + //assertThrows(ParseException.class,()->Reader.read("a/b/c")); + + // Bad address parsing + assertThrows(ParseException.class,()->Reader.read("#-1/foo")); + + // too long symbol names + assertThrows(ParseException.class,()->Reader.read("abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmnop")); + assertThrows(ParseException.class,()->Reader.read("abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmnop/a")); + assertThrows(ParseException.class,()->Reader.read("a/abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmnop")); + + } + + @Test + public void testSymbolPath() { + ACell form=Reader.read("foo/bar/baz"); + assertEquals(Lists.of(Symbols.LOOKUP,Lists.of(Symbols.LOOKUP,Symbols.FOO,Symbols.BAR),Symbols.BAZ),form) ; + } + + @Test + public void testSymbolsRegressionCases() { + assertEquals(Symbol.create("nils"), Reader.read("nils")); + + // symbol starting with a boolean value + assertEquals(Symbol.create("falsey"), Reader.read("falsey")); + assertEquals(Symbol.create("true-exp"), Reader.read("true-exp")); + } + + @Test + public void testChar() { + assertCVMEquals('A', Reader.read("\\A")); + assertCVMEquals('a', Reader.read("\\u0061")); + assertCVMEquals(' ', Reader.read("\\space")); + assertCVMEquals('\t', Reader.read("\\tab")); + assertCVMEquals('\n', Reader.read("\\newline")); + assertCVMEquals('\f', Reader.read("\\formfeed")); + assertCVMEquals('\b', Reader.read("\\backspace")); + assertCVMEquals('\r', Reader.read("\\return")); + } + + @Test + public void testNumbers() { + assertCVMEquals(1L, Reader.read("1")); + assertCVMEquals(2.0, Reader.read("2.0")); + + // scientific notation + assertCVMEquals(2.0, Reader.read("2.0e0")); + assertCVMEquals(20.0, Reader.read("2.0e1")); + assertCVMEquals(0.2, Reader.read("2.0e-1")); + assertCVMEquals(12.0, Reader.read("12e0")); + + assertThrows(Error.class, () -> { + Reader.read("2.0e0.1234"); + }); + // assertNull( Reader.read("[2.0e0.1234]")); + // TODO: do we want this? + //assertThrows(Error.class, () -> Reader.read("[2.0e0.1234]")); // Issue #70 + + // metadata ignored + assertEquals(Syntax.create(RT.cvm(3.23),Maps.of(Keywords.FOO, CVMBool.TRUE)), Reader.read("^:foo 3.23")); + } + + @Test + public void testSpecialNumbers() { + assertEquals(CVMDouble.NaN, Reader.read("##NaN")); + assertEquals(CVMDouble.POSITIVE_INFINITY, Reader.read("##Inf ")); + assertEquals(CVMDouble.NEGATIVE_INFINITY, Reader.read(" ##-Inf")); + } + + @Test + public void testHexBlobs() { + assertEquals(Blobs.fromHex("cafebabe"), Reader.read("0xcafebabe")); + assertEquals(Blobs.fromHex("0aA1"), Reader.read("0x0Aa1")); + assertEquals(Blob.EMPTY, Reader.read("0x")); + + // TODO: figure out the edge case + assertThrows(Error.class, () -> Reader.read("0x1")); + //assertThrows(Error.class, () -> Reader.read("[0x1]")); // odd number of hex digits + + assertThrows(Error.class, () -> Reader.read("0x123")); // odd number of hex digits + } + + @Test + public void testNil() { + assertNull(Reader.read("nil")); + + // metadata on null + assertEquals(Syntax.create(null),Reader.read("^{} nil")); + } + + @Test + public void testStrings() { + assertSame(Strings.empty(), Reader.read("\"\"")); + assertEquals(Strings.create("bar"), Reader.read("\"bar\"")); + assertEquals(Vectors.of(Strings.create("bar")), Reader.read("[\"bar\"]")); + assertEquals(Strings.create("\"bar\""), Reader.read("\"\\\"bar\\\"\"")); + + } + + @Test + public void testList() { + assertSame(Lists.empty(), Reader.read(" ()")); + assertEquals(Lists.of(1L, 2L), Reader.read("(1 2)")); + assertEquals(Lists.of(Vectors.empty()), Reader.read(" ([] )")); + } + + @Test + public void testNoWhiteSpace() { + assertEquals(Lists.of(Vectors.empty(), Vectors.empty()), Reader.read("([][])")); + assertEquals(Lists.of(Vectors.empty(), 13L), Reader.read("([]13)")); + assertEquals(Lists.of(Symbols.SET, Vectors.empty()), Reader.read("(set[])")); + } + + @Test + public void testMaps() { + assertSame(Maps.empty(), Reader.read("{}")); + assertEquals(Maps.of(1L, 2L), Reader.read("{1,2}")); + assertEquals(Maps.of(Samples.FOO, Samples.BAR), Reader.read("{:foo :bar}")); + } + + @Test + public void testMapError() { + assertThrows(ParseException.class,()->Reader.read("{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}")); + } + + @Test + public void testQuote() { + assertEquals(Lists.of(Symbols.QUOTE, 1L), Reader.read("'1")); + assertEquals(Lists.of(Symbols.QUOTE, Lists.of(Symbols.QUOTE, Vectors.empty())), Reader.read("''[]")); + + assertEquals(Lists.of(Symbols.QUOTE,Lists.of(Symbols.UNQUOTE,Symbols.FOO)),Reader.read("'~foo")); + + } + + + @Test + public void testTooManyClosingParens() { + // See #244 + assertThrows(ParseException.class, () -> Reader.read("(42))))")); + } + + + @Test + public void testWrongSizeMaps() { + assertThrows(ParseException.class, () -> Reader.read("{:foobar}")); + } + + @Test + public void testParsingNothing() { + assertThrows(ParseException.class, () -> Reader.read(" ")); + } + + @Test + public void testSyntaxReader() { + assertEquals(Syntax.class, Reader.readSyntax("nil").getClass()); + assertEquals(Syntax.create(RT.cvm(1L)), Reader.readSyntax("1").withoutMeta()); + assertEquals(Syntax.create(Symbols.FOO), Reader.readSyntax("foo").withoutMeta()); + assertEquals(Syntax.create(Keywords.FOO), Reader.readSyntax(":foo").withoutMeta()); + } + + @Test + public void testReadMetadata() { + assertEquals(Syntax.create(Keywords.FOO),Reader.read("^{} :foo")); + } + + @SuppressWarnings("unchecked") + @Test + public void testMetadata() { + assertCVMEquals(Boolean.TRUE, Reader.readSyntax("^:foo a").getMeta().get(Keywords.FOO)); + + { + AList def=(AList) Reader.readAll("(def ^{:foo 2} a 1)").get(0); + Syntax form=(Syntax) def.get(1); + + assertCVMEquals(2L, form.getMeta().get(Keywords.FOO)); + } + + } + + @Test public void testIdempotentPrint() { + doIdempotencyTest(Address.create(12345)); + doIdempotencyTest(Samples.LONG_MAP_10); + doIdempotencyTest(Samples.BAD_HASH); + doIdempotencyTest(Samples.MAX_EMBEDDED_STRING); + doIdempotencyTest(Reader.readAll("(def ^{:foo 2} a 1)")); + doIdempotencyTest(Reader.readAll("(fn ^{:foo 2} [] bar/baz)")); + } + + public void doIdempotencyTest(ACell cell) { + String s=cell.print(); + assertEquals(s,Reader.read(s).print()); + } +} diff --git a/convex-core/src/test/java/convex/core/lang/SyntaxTest.java b/convex-core/src/test/java/convex/core/lang/SyntaxTest.java new file mode 100644 index 000000000..5f8e1dc45 --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/SyntaxTest.java @@ -0,0 +1,19 @@ +package convex.core.lang; + +import static convex.test.Assertions.assertCVMEquals; + +import org.junit.jupiter.api.Test; + +import convex.core.data.ObjectsTest; +import convex.core.data.Syntax; + +public class SyntaxTest { + + @Test + public void testSyntaxConstructor() { + Syntax s = Syntax.create(RT.cvm(1L)); + assertCVMEquals(1L, s.getValue()); + + ObjectsTest.doAnyValueTests(s); + } +} diff --git a/convex-core/src/test/java/convex/core/lang/TestState.java b/convex-core/src/test/java/convex/core/lang/TestState.java new file mode 100644 index 000000000..d85281193 --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/TestState.java @@ -0,0 +1,252 @@ +package convex.core.lang; + +import static convex.test.Assertions.assertCVMEquals; +import static convex.test.Assertions.assertStateError; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import convex.core.Constants; +import convex.core.State; +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.data.Keyword; +import convex.core.data.prim.CVMBool; +import convex.core.data.prim.CVMDouble; +import convex.core.data.prim.CVMLong; +import convex.core.init.InitTest; +import convex.core.util.Utils; + +/** + * Class for building and testing a State for the unit test suite. + * + * Includes example smart contracts. + */ +public class TestState { + public static final int NUM_CONTRACTS = 5; + + public static final Address[] CONTRACTS = new Address[NUM_CONTRACTS]; + + /** + * A test state set up with a few accounts + */ + public static final State STATE; + + + static { + + try { + + State s = InitTest.STATE; + Context ctx = Context.createFake(s, InitTest.HERO); + for (int i = 0; i < NUM_CONTRACTS; i++) { + // Construct code for each contract + ACell contractCode = Reader.read( + "(do " + "(def my-data nil)" + "(defn write ^{:callable? true} [x] (def my-data x)) " + + "(defn read ^{:callable? true} [] my-data)" + "(defn who-called-me ^{:callable? true} [] *caller*)" + + "(defn my-address ^{:callable? true} [] *address*)" + "(defn my-number ^{:callable? true} [] "+i+")" + "(defn foo ^{:callable? true} [] :bar))"); + + ctx = ctx.deployActor(contractCode); + CONTRACTS[i] = (Address) ctx.getResult(); + } + + s= ctx.getState(); + STATE = s; + CONTEXT = Context.createFake(STATE, InitTest.HERO); + } catch (Throwable e) { + e.printStackTrace(); + throw new Error(e); + } + } + + /** + * Initial juice for TestState.INITIAL_CONTEXT + */ + public static final long INITIAL_JUICE = Constants.MAX_TRANSACTION_JUICE; + + /** + * Initial juice price + */ + public static final CVMLong JUICE_PRICE = STATE.getJuicePrice(); + + /** + * A test context set up with a few accounts + */ + public static final Context CONTEXT; + + + + /** + * Total funds in the test state, minus those subtracted for juice in the + * initial context + */ + public static final Long TOTAL_FUNDS = Constants.MAX_SUPPLY; + + + + + @SuppressWarnings("unchecked") + static AOp compile(Context c, String source) { + c=c.fork(); + try { + ACell form = Reader.read(source); + AOp op = (AOp) c.expandCompile(form).getResult(); + return op; + } catch (Exception e) { + throw Utils.sneakyThrow(e); + } + } + + public static T eval(Context c, String source) { + c=c.fork(); + try { + AOp op = compile(c, source); + Context rc = c.run(op); + return rc.getResult(); + } catch (Exception e) { + throw Utils.sneakyThrow(e); + } + } + + // Deploy actor code directly into a Context + public static Context deploy(Context ctx,String actorResource) { + String source; + try { + source = Utils.readResourceAsString(actorResource); + ACell contractCode=Reader.read(source); + ctx=ctx.deployActor(contractCode); + } catch (IOException e) { + e.printStackTrace(); + } + return ctx; + } + + @Test + public void testInitial() { + Context ctx = Context.createFake(STATE,InitTest.HERO); + State s = ctx.getState(); + assertEquals(STATE, s); + assertSame(Core.COUNT, ctx.lookup(Symbols.COUNT).getResult()); + + assertCVMEquals(Symbols.STAR_TIMESTAMP, ctx.lookup(Symbols.STAR_TIMESTAMP).getResult()); + + assertCVMEquals(Constants.INITIAL_TIMESTAMP, s.getTimeStamp()); + } + + @Test + public void testContractCall() { + Context ctx0 = Context.createFake(STATE, InitTest.HERO); + Address TARGET = CONTRACTS[0]; + ctx0 = ctx0.execute(compile(ctx0, "(def target (address \"" + TARGET.toHexString() + "\"))")); + ctx0 = ctx0.execute(compile(ctx0, "(def hero *address*)")); + final Context ctx = ctx0; + + assertEquals(InitTest.HERO, ctx.lookup(Symbols.HERO).getResult()); + assertEquals(Keyword.create("bar"), eval(ctx, "(call target (foo))")); + assertEquals(InitTest.HERO, eval(ctx, "(call target (who-called-me))")); + assertEquals(TARGET, eval(ctx, "(call target (my-address))")); + + assertEquals(0L, evalL(ctx, "(call target (my-number))")); + + assertStateError(TestState.step(ctx, "(call target (missing-function))")); + } + + public static boolean evalB(String source) { + return ((CVMBool)eval(source)).booleanValue(); + } + + public static boolean evalB(Context ctx, String source) { + return ((CVMBool)eval(ctx, source)).booleanValue(); + } + + public static double evalD(Context ctx, String source) { + ACell result=eval(ctx,source); + CVMDouble d=RT.castDouble(result); + if (d==null) throw new ClassCastException("Expected Double, but got: "+RT.getType(result)); + return d.doubleValue(); + } + + public static double evalD(String source) { + return evalD(CONTEXT,source); + } + + public static long evalL(Context ctx, String source) { + ACell result=eval(ctx,source); + CVMLong d=RT.castLong(result); + if (d==null) throw new ClassCastException("Expected Long, but got: "+RT.getType(result)); + return d.longValue(); + } + + public static long evalL(String source) { + return evalL(CONTEXT,source); + } + + public static String evalS(String source) { + return eval(source).toString(); + } + + @SuppressWarnings("unchecked") + public static T eval(String source) { + return (T) step(source).getResult(); + } + + public static Context step(String source) { + return step(CONTEXT, source); + } + + /** + * Steps execution in a new forked Context + * @param + * @param ctx Initial context to fork + * @param source + * @return New forked context containing step result + */ + @SuppressWarnings("unchecked") + public static Context step(Context ctx, String source) { + // Compile form in forked context + Context> cctx=ctx.fork(); + ACell form = Reader.read(source); + cctx = cctx.expandCompile(form); + if (cctx.isExceptional()) return (Context) cctx; + AOp op = cctx.getResult(); + + // Run form in separate forked context to get result context + Context rctx = ctx.fork(); + rctx=(Context) rctx.run(op); + assert(rctx.getDepth()==0):"Invalid depth after step: "+rctx.getDepth(); + return rctx; + } + + /** + * Runs an execution step as a different address. Returns value after restoring + * the original address. + * @param address Address to run as + * @param c Initial Context. Will not be modified. + * @param source Source form to execute + * @return Updated context + */ + @SuppressWarnings("unchecked") + public static Context stepAs(Address address, Context c, String source) { + Context rc = Context.createFake(c.getState(), address); + rc = step(rc, source); + return (Context) Context.createFake(rc.getState(), c.getAddress()).withValue(rc.getValue()); + } + + @Test public void testStateSetup() { + assertEquals(0,CONTEXT.getDepth()); + assertFalse(CONTEXT.isExceptional()); + assertNull(CONTEXT.getResult()); + assertEquals(TestState.TOTAL_FUNDS, STATE.computeTotalFunds()); + + } + + public static void main(String[] args) { + System.out.println(Utils.print(STATE)); + } + +} diff --git a/convex-core/src/test/java/convex/core/lang/reader/ANTLRTest.java b/convex-core/src/test/java/convex/core/lang/reader/ANTLRTest.java new file mode 100644 index 000000000..e00e5a495 --- /dev/null +++ b/convex-core/src/test/java/convex/core/lang/reader/ANTLRTest.java @@ -0,0 +1,144 @@ +package convex.core.lang.reader; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.data.Blob; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.Lists; +import convex.core.data.Maps; +import convex.core.data.Sets; +import convex.core.data.Strings; +import convex.core.data.Symbol; +import convex.core.data.Syntax; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMBool; +import convex.core.data.prim.CVMChar; +import convex.core.data.prim.CVMDouble; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.ParseException; +import convex.core.lang.Symbols; + +public class ANTLRTest { + + @SuppressWarnings("unchecked") + private R read(String s) { + return (R) AntlrReader.read(s); + } + + @SuppressWarnings("unchecked") + private R readAll(String s) { + return (R) AntlrReader.readAll(s); + } + + @Test public void testNil() { + assertNull(read("nil")); + } + + @Test public void testPrimitives () { + assertSame(CVMBool.TRUE,read("true")); + assertSame(CVMBool.FALSE,read("false")); + assertEquals(CVMLong.create(17),read("17")); + assertEquals(CVMLong.create(-2),read("-2")); + assertEquals(CVMLong.ZERO,read("0")); + } + + @Test public void testDataStructures() { + // basic data structures + assertEquals(Vectors.of(1,2),read("[1 2]")); + assertEquals(Lists.of(1,2),read("(1 2)")); + assertEquals(Sets.of(1,2),read("#{1 2}")); + assertEquals(Maps.of(1,2),read("{1 2}")); + + // empty structures + assertSame(Sets.empty(),read("#{}")); + assertSame(Lists.empty(),read("()")); + assertSame(Vectors.empty(),read("[]")); + assertSame(Maps.empty(),read("{}")); + } + + @Test public void testSymbols() { + assertEquals(Symbols.FOO,read("foo")); + assertEquals(Symbol.create("/"),read("/")); + } + + @Test public void testKeywords() { + assertEquals(Keywords.FOO,read(":foo")); + assertEquals(Keyword.create("/"),read(":/")); + } + + + @Test public void testBlobs() { + // Blobs + assertEquals(Blob.EMPTY,read("0x")); + assertEquals(Blob.fromHex("cafebabe"),read("0xcaFEBAbe")); + } + + @Test public void testAddress() { + // Address + assertEquals(Address.create(17),read("#17")); + } + + @Test public void testSyntax() { + assertEquals(Syntax.create(CVMLong.ONE,Maps.empty()), read("^{} 1")); + } + + @Test public void testQuoting() { + assertEquals(Lists.of(Symbols.QUOTE,CVMLong.ZERO), read("'0")); + assertEquals(Lists.of(Symbols.QUOTE,Symbols.FOO), read("'foo")); + assertEquals(Lists.of(Symbols.QUOTE,Vectors.empty()), read("'[]")); + } + + + @Test public void testChars() { + assertEquals(CVMChar.create('a'), read("\\a")); + assertEquals(CVMChar.create('\t'), read("\\tab")); + } + + @Test public void testSpecial() { + assertEquals(CVMDouble.NaN, read("##NaN")); + } + + + @Test public void testDouble() { + assertEquals(CVMDouble.ONE, read("1.0")); + assertEquals(CVMDouble.ONE, read("1.0e0")); + assertEquals(CVMDouble.create(-17.0), read("-17.0")); + assertEquals(CVMDouble.create(-17.0e2), read("-17.0E2")); + assertEquals(CVMDouble.create(1000), read("1e3")); + assertEquals(CVMDouble.create(0.001), read("1e-3")); + } + + @Test public void testStrings() { + assertEquals(Strings.create(""), read("\"\"")); + assertEquals(Strings.create("a"), read("\"a\"")); + assertEquals(Strings.create("bar"), read("\"bar\"")); + assertEquals(Strings.create("ba\nr"), read("\"ba\\\nr\"")); + } + + @Test public void testReadAll() { + assertSame(Lists.empty(),readAll("")); + assertEquals(Lists.of(1,2),readAll(" 1 2 ")); + + assertThrows(ParseException.class,()->readAll("1 2 (")); + + } + + @Test public void testPath() { + assertEquals(Lists.of(Symbols.LOOKUP,Address.ZERO,Symbols.FOO),AntlrReader.read("#0/foo")); + assertEquals(Lists.of(Symbols.LOOKUP,Address.ZERO,Symbols.DIVIDE),AntlrReader.read("#0//")); + } + + @Test public void testError() { + assertThrows(ParseException.class,()->read("1 2")); + assertThrows(ParseException.class,()->read("1.0e0.1234")); + } + +} diff --git a/convex-core/src/test/java/convex/core/util/EconomicsTest.java b/convex-core/src/test/java/convex/core/util/EconomicsTest.java new file mode 100644 index 000000000..fda2be3bb --- /dev/null +++ b/convex-core/src/test/java/convex/core/util/EconomicsTest.java @@ -0,0 +1,47 @@ +package convex.core.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class EconomicsTest { + + @Test + public void testPoolRate() { + + assertEquals(1.0, Economics.swapRate(100, 100)); + assertEquals(1.0, Economics.swapRate(Long.MAX_VALUE, Long.MAX_VALUE)); + assertEquals(2.0, Economics.swapRate(1, 2)); + + assertThrows(IllegalArgumentException.class, () -> Economics.swapRate(0, 0)); + assertThrows(IllegalArgumentException.class, () -> Economics.swapRate(1000, 0)); + assertThrows(IllegalArgumentException.class, () -> Economics.swapRate(0, 1000)); + assertThrows(IllegalArgumentException.class, () -> Economics.swapRate(-10, 10)); + assertThrows(IllegalArgumentException.class, () -> Economics.swapRate(10, -10)); + assertThrows(IllegalArgumentException.class, () -> Economics.swapRate(Long.MIN_VALUE, Long.MIN_VALUE)); + } + + @Test + public void testPoolPrice() { + + assertEquals(101, Economics.swapPrice(50, 100, 100)); + assertEquals(1, Economics.swapPrice(0, 100, 100)); + assertEquals(-33, Economics.swapPrice(-50, 100, 100)); + assertEquals(1, Economics.swapPrice(0, 1675, 117)); + assertEquals(1, Economics.swapPrice(0, 12, 1454517)); + assertEquals(1000000, Economics.swapPrice(999999, 1000000, 1)); + + // TODO: seem to be some instability issues doing things like this? + // assertEquals(Long.MAX_VALUE-1000000, Economics.swapPrice(Long.MAX_VALUE-1000000, Long.MAX_VALUE, 1000000)); + + // Fails because (double)(Long.MAX_VALUE-1) == (double)(Long.MAX_VALUE) + assertThrows(IllegalArgumentException.class, ()->Economics.swapPrice(Long.MAX_VALUE-1, Long.MAX_VALUE, 10)); + + assertThrows(IllegalArgumentException.class, () -> Economics.swapPrice(100, 100, 100)); + assertThrows(IllegalArgumentException.class, () -> Economics.swapPrice(100, 0, 100)); + assertThrows(IllegalArgumentException.class, () -> Economics.swapPrice(0, 0, 0)); + assertThrows(IllegalArgumentException.class, () -> Economics.swapPrice(100, 100, 0)); + assertThrows(IllegalArgumentException.class, () -> Economics.swapPrice(100, 50, 200)); + } +} diff --git a/convex-core/src/test/java/convex/core/util/GenTestEconomics.java b/convex-core/src/test/java/convex/core/util/GenTestEconomics.java new file mode 100644 index 000000000..7b6f64391 --- /dev/null +++ b/convex-core/src/test/java/convex/core/util/GenTestEconomics.java @@ -0,0 +1,31 @@ +package convex.core.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.runner.RunWith; + +import com.pholser.junit.quickcheck.Property; +import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; + +@RunWith(JUnitQuickcheck.class) +public class GenTestEconomics { + + @Property + public void alwaysARate(Long a, Long b) { + if ((a>0)&&(b>0)) { + double rate=Economics.swapRate(a, b); + assertTrue(rate>0); + } + } + + @Property + public void zeroCostsZero(Long a, Long b) { + if ((a>0)&&(b>0)) { + long price = Economics.swapPrice(0, a, b); + + // price should always be 1, since must spend minimum 1 unit to increase pool + assertEquals(1L,price); + } + } +} diff --git a/convex-core/src/test/java/convex/lib/AssetTest.java b/convex-core/src/test/java/convex/lib/AssetTest.java new file mode 100644 index 000000000..508816ea3 --- /dev/null +++ b/convex-core/src/test/java/convex/lib/AssetTest.java @@ -0,0 +1,106 @@ +package convex.lib; + +import static convex.core.lang.TestState.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import convex.core.crypto.AKeyPair; +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.lang.Context; +import convex.core.lang.TestState; + +/** + * + * Generic tests for ANY digital asset compatible with the convex.asset API + */ +public class AssetTest { + + static final AKeyPair TEST_KP=AKeyPair.generate(); + + static { + + } + /** + * Generic tests for an Asset + * + * Both users must have a non-empty balance. + * + * @param ctx Context to execute in + * @param asset Address of asset + * @param user1 First user + * @param user2 Second user + */ + public static void doAssetTests (Context ctx, Address asset, Address user1, Address user2) { + // Set up test user + ctx=ctx.createAccount(TEST_KP.getAccountKey()); + Address tester=(Address) ctx.getResult(); + ctx=ctx.forkWithAddress(tester); + ctx=step(ctx,"(import convex.asset :as asset)"); + ctx=step(ctx,"(def token "+asset+")"); + ctx=step(ctx,"(def user1 "+user1+")"); + ctx=step(ctx,"(def user2 "+user2+")"); + ctx = TestState.step(ctx,"(def actor (deploy '(set-controller "+tester+")))"); + Address actor = (Address) ctx.getResult(); + assertNotNull(actor); + + // Set up user imports + ctx=stepAs(user1,ctx,"(import convex.asset :as asset)"); + ctx=stepAs(user2,ctx,"(import convex.asset :as asset)"); + + + // Tester balance should be the empty value + ACell empty=eval(ctx,"(asset/balance token)"); + assertEquals(empty,eval(ctx,"(asset/quantity-zero token)")); + + // Get user balances and total balance, ensure they are not empty + ctx=step(ctx,"(def bal1 (asset/balance token user1))"); + ACell balance1=ctx.getResult(); + assertNotNull(balance1); + assertNotEquals(empty,balance1); + ctx=step(ctx,"(def bal2 (asset/balance token user2))"); + ACell balance2=ctx.getResult(); + assertNotNull(balance2); + assertNotEquals(empty,balance2); + ACell total=eval(ctx,"(asset/quantity-add token bal1 bal2)"); + assertNotNull(total); + assertNotEquals(empty,total); + + // Tests for each user + doUserAssetTests(ctx,asset,user1,balance1); + doUserAssetTests(ctx,asset,user2,balance2); + + // Test transferring everything to tester + { + Context c=ctx.fork(); + c=stepAs(user1,c,"(asset/transfer "+tester+" ["+asset+" (asset/balance "+asset+")])"); + c=stepAs(user2,c,"(asset/transfer "+tester+" ["+asset+" (asset/balance "+asset+")])"); + + // user balances should now be empty + assertEquals(empty,eval(c,"(asset/balance token user1)")); + assertEquals(empty,eval(c,"(asset/balance token user2)")); + + // tester should own everything + assertEquals(total,eval(c,"(asset/balance token)")); + } + + } + + public static void doUserAssetTests (Context ctx, Address asset, Address user, ACell balance) { + ctx=ctx.forkWithAddress(user); + ctx=step(ctx,"(def ast (address "+asset+"))"); + assertEquals(asset,ctx.getResult()); + + ctx=step(ctx,"(def bal (asset/balance "+asset+"))"); + assertEquals(balance,eval(ctx,"bal")); + assertEquals(balance,eval(ctx,"(asset/quantity-add ast bal nil)")); + assertEquals(balance,eval(ctx,"(asset/quantity-add ast nil bal)")); + assertEquals(eval(ctx,"(asset/quantity-zero ast)"),eval(ctx,"(asset/quantity-sub ast bal bal)")); + + assertTrue(evalB(ctx,"(asset/owns? *address* [ast bal])")); + assertTrue(evalB(ctx,"(asset/owns? *address* [ast nil])")); + } + +} diff --git a/convex-core/src/test/java/convex/lib/FungibleTest.java b/convex-core/src/test/java/convex/lib/FungibleTest.java new file mode 100644 index 000000000..819362ac2 --- /dev/null +++ b/convex-core/src/test/java/convex/lib/FungibleTest.java @@ -0,0 +1,244 @@ +package convex.lib; + +import static convex.core.lang.TestState.eval; +import static convex.core.lang.TestState.evalB; +import static convex.core.lang.TestState.evalL; +import static convex.core.lang.TestState.step; +import static convex.test.Assertions.assertAssertError; +import static convex.test.Assertions.assertError; +import static convex.test.Assertions.assertNotError; +import static convex.test.Assertions.assertTrustError; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import convex.core.crypto.AKeyPair; +import convex.core.data.AMap; +import convex.core.data.Address; +import convex.core.init.InitTest; +import convex.core.lang.Context; +import convex.core.lang.RT; +import convex.core.lang.TestState; + +public class FungibleTest { + + static final AKeyPair TEST_KEYPAIR=AKeyPair.generate(); + + private static final Address VILLAIN=InitTest.VILLAIN; + + private static final Context CTX; + private static final Address fungible; + + static { + Context ctx=TestState.CONTEXT.fork(); + String importS="(import convex.fungible :as fungible)"; + ctx=step(ctx,importS); + assertNotError(ctx); + fungible = (Address)ctx.getResult(); + ctx=step(ctx,"(import convex.asset :as asset)"); + assertFalse(ctx.isExceptional()); + CTX = ctx; + } + + @Test public void testAssetAPI() { + Context ctx = CTX.fork(); + ctx=step(ctx,"(def token (deploy (fungible/build-token {:supply 1000000})))"); + Address token = (Address) ctx.getResult(); + assertNotNull(token); + + // generic tests + doFungibleTests(ctx,token,ctx.getAddress()); + + assertEquals(1000000L,evalL(ctx,"(asset/balance token *address*)")); + assertEquals(0L,evalL(ctx,"(asset/balance token *registry*)")); + + ctx=step(ctx,"(asset/offer "+VILLAIN+" [token 1000])"); + assertNotError(ctx); + + ctx=step(ctx,"(asset/transfer "+VILLAIN+" [token 2000])"); + assertNotError(ctx); + + assertEquals(998000L,evalL(ctx,"(asset/balance token *address*)")); + assertEquals(2000L,evalL(ctx,"(asset/balance token "+VILLAIN+")")); + + assertEquals(0L,evalL(ctx,"(asset/quantity-zero token)")); + assertEquals(110L,evalL(ctx,"(asset/quantity-add token 100 10)")); + assertEquals(110L,evalL(ctx,"(asset/quantity-sub token 120 10)")); + assertEquals(110L,evalL(ctx,"(asset/quantity-sub token 110 nil)")); + assertEquals(0L,evalL(ctx,"(asset/quantity-sub token 100 1000)")); + + assertTrue(evalB(ctx,"(asset/quantity-contains? [token 110] [token 100])")); + assertTrue(evalB(ctx,"(asset/quantity-contains? [token 110] nil)")); + assertTrue(evalB(ctx,"(asset/quantity-contains? token 1000 999)")); + assertFalse(evalB(ctx,"(asset/quantity-contains? [token 110] [token 300])")); + + + + assertTrue(evalB(ctx,"(asset/owns? "+VILLAIN+" [token 1000])")); + assertTrue(evalB(ctx,"(asset/owns? "+VILLAIN+" [token 2000])")); + assertFalse(evalB(ctx,"(asset/owns? "+VILLAIN+" [token 2001])")); + + // transfer using map argument + ctx=step(ctx,"(asset/transfer "+VILLAIN+" {token 100})"); + assertTrue(ctx.getResult() instanceof AMap); + assertTrue(evalB(ctx,"(asset/owns? "+VILLAIN+" [token 2100])")); + + // test offer + ctx=step(ctx,"(asset/offer "+VILLAIN+" [token 1337])"); + assertEquals(1337L,evalL(ctx,"(asset/get-offer token *address* "+VILLAIN+")")); + } + + @Test public void testBuildToken() { + // check our alias is right + Context ctx = CTX.fork(); + assertEquals(fungible,eval(ctx,"fungible")); + + // deploy a token with default config + ctx=step(ctx,"(def token (deploy (fungible/build-token {})))"); + Address token = (Address) ctx.getResult(); + assertTrue(ctx.getAccountStatus(token)!=null); + ctx=step(ctx,"(def token (address "+token+"))"); + + // GEnric tests + doFungibleTests(ctx,token,ctx.getAddress()); + + // check our balance is positive as initial holder + long bal=evalL(ctx,"(fungible/balance token *address*)"); + assertTrue(bal>0); + + // transfer to the Villain scenario + { + Context tctx=step(ctx,"(fungible/transfer token "+VILLAIN+" 100)"); + assertEquals(bal-100,evalL(tctx,"(fungible/balance token *address*)")); + assertEquals(100,evalL(tctx,"(fungible/balance token "+VILLAIN+")")); + } + + // acceptable transfers + assertNotError(step(ctx,"(fungible/transfer token *address* 0)")); + assertNotError(step(ctx,"(fungible/transfer token *address* "+bal+")")); + + // bad transfers + assertAssertError(step(ctx,"(fungible/transfer token *address* -1)")); + assertAssertError(step(ctx,"(fungible/transfer token *address* "+(bal+1)+")")); + } + + @Test public void testMint() { + // check our alias is right + Context ctx = CTX.fork(); + + // deploy a token with default config + ctx=step(ctx,"(def token (deploy [(fungible/build-token {:supply 100}) (fungible/add-mint {:max-supply 1000})]))"); + Address token = (Address) ctx.getResult(); + assertTrue(ctx.getAccountStatus(token)!=null); + + // do Generic Tests + doFungibleTests(ctx,token,ctx.getAddress()); + + // check our balance is positive as initial holder + Long bal=evalL(ctx,"(fungible/balance token *address*)"); + assertEquals(100L,bal); + + // Mint up to max and back down to zero + { + Context c=step(ctx,"(fungible/mint token 900)"); + assertEquals(1000L,evalL(c,"(fungible/balance token *address*)")); + + c=step(c,"(fungible/mint token -900)"); + assertEquals(bal,evalL(c,"(fungible/balance token *address*)")); + + c=step(c,"(fungible/mint token -100)"); + assertEquals(0L,evalL(c,"(fungible/balance token *address*)")); + } + + // Mint up to max and burn down to zero + { + Context c=step(ctx,"(fungible/mint token 900)"); + assertEquals(1000L,evalL(c,"(fungible/balance token *address*)")); + + c=step(c,"(fungible/burn token 900)"); + assertEquals(100L,evalL(c,"(fungible/balance token *address*)")); + + assertAssertError(step(c,"(fungible/burn token 101)")); // Fails, not held + + c=step(c,"(fungible/burn token 100)"); + assertEquals(0L,evalL(c,"(fungible/balance token *address*)")); + + assertAssertError(step(c,"(fungible/burn token 1)")); // Fails, not held + } + + + // Shouldn't be possible to burn tokens in supply but not held + { + Context c=step(ctx,"(fungible/mint token 900)"); + assertEquals(1000L,evalL(c,"(fungible/balance token *address*)")); + + c=step(c,"(fungible/transfer token "+VILLAIN+" 800)"); + assertEquals(200L,evalL(c,"(fungible/balance token *address*)")); + + assertAssertError(step(c,"(fungible/burn token 201)")); // Fails, not held + assertNotError(step(c,"(fungible/burn token 200)")); // OK since held + } + + // Illegal Minting amounts + { + assertError(step(ctx,"(fungible/mint token 901)")); // too much (exceeds max supply) + assertError(step(ctx,"(fungible/mint token -101)")); // too little + } + + // Villain shouldn't be able to mint or burn + { + Context c=ctx.forkWithAddress(VILLAIN); + c=step(c,"(def token "+token+")"); + c=step(c,"(import convex.fungible :as fungible)"); + + assertTrustError(step(c,"(fungible/mint token 100)")); + assertTrustError(step(c,"(fungible/mint token 10000)")); // trust before amount checks + + assertTrustError(step(c,"(fungible/burn token 100)")); + } + } + + /** + * Generic tests for a fungible token. User account should have some of fungible token and sufficient coins. + * @param ctx Initial Context. Will be forked. + * @param token Fungible token Address + * @param user User Address + */ + public static void doFungibleTests (Context ctx, Address token, Address user) { + ctx=ctx.forkWithAddress(user); + ctx=step(ctx,"(import convex.asset :as asset)"); + ctx=step(ctx,"(import convex.fungible :as fungible)"); + ctx=step(ctx,"(def token "+token+")"); + ctx = TestState.step(ctx,"(def actor (deploy '(set-controller "+user+")))"); + Address actor = (Address) ctx.getResult(); + assertNotNull(actor); + + Long BAL=evalL(ctx,"(asset/balance token *address*)"); + assertEquals(0L, evalL(ctx,"(asset/balance token actor)")); + assertTrue(BAL>0,"Should provide a user account with positive balance!"); + + // transfer all to self, should not affect balance + ctx=step(ctx,"(asset/transfer *address* [token "+BAL+"])"); + assertEquals(BAL,RT.jvm(ctx.getResult())); + assertEquals(BAL, evalL(ctx,"(asset/balance token *address*)")); + + // transfer nothing to self, should not affect balance + ctx=step(ctx,"(asset/transfer *address* [token nil])"); + assertEquals(0L,(long)RT.jvm(ctx.getResult())); + assertEquals(BAL, evalL(ctx,"(asset/balance token *address*)")); + + // Run generic asset tests, giving 1/3 the balance to a new user account + { + Context c=ctx.fork(); + c=c.createAccount(TEST_KEYPAIR.getAccountKey()); + Address user2=(Address) c.getResult(); + Long smallBal=BAL/3; + c=step(c,"(asset/transfer "+user2+" [token "+smallBal+"])"); + + AssetTest.doAssetTests(c, token, user, user2); + } + } +} diff --git a/convex-core/src/test/java/convex/lib/SimpleNFTTest.java b/convex-core/src/test/java/convex/lib/SimpleNFTTest.java new file mode 100644 index 000000000..8051767e4 --- /dev/null +++ b/convex-core/src/test/java/convex/lib/SimpleNFTTest.java @@ -0,0 +1,69 @@ +package convex.lib; + +import static convex.core.lang.TestState.eval; +import static convex.core.lang.TestState.step; +import static convex.test.Assertions.assertNotError; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +import convex.core.crypto.AKeyPair; +import convex.core.data.AVector; +import convex.core.data.Address; +import convex.core.data.Sets; +import convex.core.data.prim.CVMLong; +import convex.core.lang.Context; +import convex.core.lang.TestState; +import convex.test.Testing; + +public class SimpleNFTTest { + + static final AKeyPair KP1=AKeyPair.generate(); + static final AKeyPair KP2=AKeyPair.generate(); + + static final Address NFT; + + private static final Context CTX; + + static { + Context ctx=TestState.CONTEXT.fork(); + String importS = "(import asset.nft.simple :as nft)"; + ctx=step(ctx,importS); + NFT=(Address)ctx.getResult(); + assertNotNull(NFT); + ctx=step(ctx,"(import convex.asset :as asset)"); + CTX=ctx; + } + + @Test public void testScript1() { + Context c=Testing.runTests(CTX,"contracts/nft/simple-nft-test.con"); + assertNotError(c); + } + + + @SuppressWarnings("unchecked") + @Test public void testAssetAPI() { + Context ctx=CTX.fork(); + ctx=step(ctx,"(def total (map (fn [v] (call nft (create))) [1 2 3 4]))"); + AVector v=(AVector) ctx.getResult(); + assertEquals(4,v.count()); + CVMLong b1=v.get(0); + + // Test balance + assertEquals(Sets.of(v.toCellArray()),eval(ctx,"(asset/balance nft)")); + + // Create test Users + ctx=ctx.createAccount(KP1.getAccountKey()); + Address user1=(Address) ctx.getResult(); + ctx=ctx.createAccount(KP2.getAccountKey()); + Address user2=(Address) ctx.getResult(); + + ctx=step(ctx,"(asset/transfer "+user1+" [nft (set (next total))])"); + ctx=step(ctx,"(asset/transfer "+user2+" [nft #{"+b1+"}])"); + assertEquals(Sets.of(b1),ctx.getResult()); + + AssetTest.doAssetTests(ctx, NFT, user1, user2); + + } +} diff --git a/convex-core/src/test/java/convex/lib/TrustTest.java b/convex-core/src/test/java/convex/lib/TrustTest.java new file mode 100644 index 000000000..3861e3afb --- /dev/null +++ b/convex-core/src/test/java/convex/lib/TrustTest.java @@ -0,0 +1,237 @@ +package convex.lib; + +import static convex.test.Assertions.assertCastError; +import static convex.test.Assertions.assertNotError; +import static convex.test.Assertions.assertStateError; +import static convex.test.Assertions.assertTrustError; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import convex.core.data.Address; +import convex.core.data.Keywords; +import convex.core.init.InitTest; +import convex.core.lang.ACVMTest; +import convex.core.lang.Context; +import convex.test.Samples; + +public class TrustTest extends ACVMTest { + private Address trusted=null; + + private Context CONTEXT; + protected TrustTest() throws IOException { + super(InitTest.BASE); + Context ctx = context(); + + String importS = "(import convex.trust :as trust)"; + ctx = step(ctx, importS); + assertNotError(ctx); + trusted = (Address)ctx.getResult(); + + CONTEXT=ctx.fork(); + INITIAL=ctx.getState(); + } + + /** + * Test that re-deployment of Fungible matches what is expected + */ + @Test + public void testLibraryProperties() { + assertTrue(CONTEXT.getAccountStatus(trusted).isActor()); + + // check alias is set up correctly + assertEquals(trusted, eval(CONTEXT, "trust")); + } + + @Test + public void testSelfTrust() { + Context ctx = CONTEXT.fork(); + + assertTrue(evalB(ctx, "(trust/trusted? *address* *address*)")); + assertFalse(evalB(ctx, "(trust/trusted? *address* nil)")); + assertFalse(evalB(ctx, "(trust/trusted? *address* :foo)")); + assertFalse(evalB(ctx, + "(trust/trusted? *address* (address 666666))")); + } + + @Test + public void testUpgradeWhitelist() { + Context ctx = CONTEXT.fork(); + + // deploy a whitelist with default config and upgradable capability + ctx = step(ctx, "(def wlist (deploy [(trust/build-whitelist nil) (trust/add-trusted-upgrade nil)]))"); + Address wl = (Address) ctx.getResult(); + assertNotNull(wl); + + assertTrue(evalB(ctx, "(trust/trusted? wlist *address*)")); + + // do an upgrade that blanks the whitelist + ctx = step(ctx, "(call wlist (upgrade '(do (def whitelist #{}))))"); + + { + // check our villain cannot upgrade the actor! + Address a1 = VILLAIN; + + Context c = ctx.forkWithAddress(a1); + c = step(c, "(do (import " + trusted + " :as trust) (def wlist " + wl + "))"); + + assertTrustError(step(c, "(call wlist (upgrade '(do :foo)))")); + } + + // check that our edit has updated actor + assertFalse(evalB(ctx, "(trust/trusted? wlist *address*)")); + + // check we can permanently remove upgradability + ctx = step(ctx, "(trust/remove-upgradability! wlist)"); + assertNotError(ctx); + assertStateError(step(ctx, "(call wlist (upgrade '(do :foo)))")); + + // actor functionality should still work otherwise + assertFalse(evalB(ctx, "(trust/trusted? wlist *address*)")); + } + + @Test + public void testWhitelist() { + // check our alias is right + Context ctx = CONTEXT.fork(); + + // deploy a whitelist with default config + ctx = step(ctx, "(def wlist (deploy (trust/build-whitelist nil)))"); + Address wl = (Address) ctx.getResult(); + assertNotNull(wl); + + // initial creator should be on whitelist + assertTrue(evalB(ctx, "(trust/trusted? wlist *address*)")); + + assertCastError(step(ctx, "(trust/trusted? wlist nil)")); + assertCastError(step(ctx, "(trust/trusted? wlist [])")); + + assertCastError(step(ctx, "(trust/trusted? nil *address*)")); + assertCastError(step(ctx, "(trust/trusted? [] *address*)")); + + { // check adding and removing to whitelist + Address a1 = Samples.BAD_ADDRESS; + Context c = ctx; + + // Check not initially on whitelist + assertFalse(evalB(c, "(trust/trusted? wlist " + a1 + ")")); + + // Add address to whitelist, shouldn't matter if it exists or not + c = step(c, "(call wlist (set-trusted " + a1 + " true))"); + assertNotError(c); + assertTrue(evalB(c, "(trust/trusted? wlist " + a1 + ")")); + + // Check removal from whitelist + c = step(c, "(call wlist (set-trusted " + a1 + " false))"); + assertNotError(c); + assertFalse(evalB(c, "(trust/trusted? wlist " + a1 + ")")); + } + + { // check the villain is excluded + Address a1 = VILLAIN; + Address a2 = HERO; + + Context c = ctx.forkWithAddress(a1); + c = step(c, "(do (import " + trusted + " :as trust) (def wlist (address " + wl + ")))"); + assertNotError(c); + + // villain can still check monitor + assertFalse(evalB(c, "(trust/trusted? wlist " + a1 + ")")); + assertTrue(evalB(c, "(trust/trusted? wlist " + a2 + ")")); + + // villain can't change whitelist + assertTrustError(step(c, "(call wlist (set-trusted " + a1 + " true))")); + assertTrustError(step(c, "(call wlist (set-trusted " + a2 + " false))")); + } + } + + @Test + public void testBlacklist() { + Context ctx = CONTEXT.fork(); + + // deploy a blacklist with default config + ctx = step(ctx, "(def blist (deploy (trust/build-blacklist {:blacklist [" + VILLAIN + "]})))"); + Address wl = (Address) ctx.getResult(); + assertNotNull(wl); + + // initial creator should not be on blacklist + assertTrue(evalB(ctx, "(trust/trusted? blist *address*)")); + + // our villain should be on the blacklist + assertFalse(evalB(ctx, "(trust/trusted? blist " + VILLAIN + ")")); + + assertCastError(step(ctx, "(trust/trusted? blist nil)")); + assertCastError(step(ctx, "(trust/trusted? blist [])")); + + assertCastError(step(ctx, "(trust/trusted? nil *address*)")); + assertCastError(step(ctx, "(trust/trusted? [] *address*)")); + + { // check adding and removing to blacklist + Address a1 = Samples.BAD_ADDRESS; + Context c = ctx; + + // Check not initially on blacklist + assertTrue(evalB(c, "(trust/trusted? blist " + a1 + ")")); + + // Add address to blacklist, shouldn't matter if it exists or not + c = step(c, "(call blist (set-trusted " + a1 + " false))"); + assertNotError(c); + assertFalse(evalB(c, "(trust/trusted? blist " + a1 + ")")); + + // Check removal from blacklist + c = step(c, "(call blist (set-trusted " + a1 + " true))"); + assertNotError(c); + assertTrue(evalB(c, "(trust/trusted? blist " + a1 + ")")); + } + + { // check the villain is excluded + Address a1 = VILLAIN; + Address a2 = HERO; + + Context c = ctx.forkWithAddress(a1); + c = step(c, "(do (import " + trusted + " :as trust) (def blist (address " + wl + ")))"); + assertNotError(c); + + // villain can still check monitor + assertFalse(evalB(c, "(trust/trusted? blist " + a1 + ")")); + assertTrue(evalB(c, "(trust/trusted? blist " + a2 + ")")); + + // villain can't change whitelist + assertTrustError(step(c, "(call blist (set-trusted " + a1 + " true))")); + assertTrustError(step(c, "(call blist (set-trusted " + a2 + " false))")); + } + } + + @Test + public void testWhitelistController() { + Context ctx = CONTEXT.fork(); + + // deploy an initially empty whitelist + ctx = step(ctx, "(def wlist (deploy (trust/build-whitelist {:whitelist []})))"); + + // deploy two actors + ctx = step(ctx, "(def alice (deploy `(set-controller ~*address*)))"); + ctx = step(ctx, "(def bob (deploy `(set-controller ~wlist)))"); + + // check initial trust + assertEquals(Keywords.FOO, eval(ctx, "(eval-as alice :foo)")); + assertTrustError(step(ctx, "(eval-as bob :foo)")); + + // add alice to the whitelist + ctx = step(ctx, "(call wlist (set-trusted alice true))"); + + // eval-as should work from alice to bob + assertEquals(eval(ctx, "bob"), (Object) eval(ctx, "(eval-as alice `(eval-as ~bob '*address*))")); + + // remove alice from the whitelist + ctx = step(ctx, "(call wlist (set-trusted alice false))"); + + // eval-as should now fail + assertTrustError(step(ctx, "(eval-as alice `(eval-as ~bob :foo))")); + } +} diff --git a/convex-core/src/test/java/convex/store/EtchInitTest.java b/convex-core/src/test/java/convex/store/EtchInitTest.java new file mode 100644 index 000000000..a246fa74e --- /dev/null +++ b/convex-core/src/test/java/convex/store/EtchInitTest.java @@ -0,0 +1,35 @@ +package convex.store; + +import org.junit.jupiter.api.Test; + +import convex.core.State; +import convex.core.data.ACell; +import convex.core.data.Hash; +import convex.core.data.Ref; +import convex.core.exceptions.InvalidDataException; +import convex.core.init.InitTest; +import convex.core.store.AStore; +import convex.core.store.Stores; +import etch.EtchStore; + +public class EtchInitTest { + + @Test public void testInitState() throws InvalidDataException { + AStore temp=Stores.current(); + try { + Stores.setCurrent(EtchStore.createTemp()); + + // Use fresh State + State s=InitTest.createState(); + Ref sr=ACell.createPersisted(s); + + Hash hash=sr.getHash(); + + Ref sr2=Ref.forHash(hash); + State s2=sr2.getValue(); + s2.validate(); + } finally { + Stores.setCurrent(temp); + } + } +} diff --git a/convex-core/src/test/java/convex/store/EtchStoreTest.java b/convex-core/src/test/java/convex/store/EtchStoreTest.java new file mode 100644 index 000000000..efcab849e --- /dev/null +++ b/convex-core/src/test/java/convex/store/EtchStoreTest.java @@ -0,0 +1,240 @@ +package convex.store; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +import org.junit.Test; + +import convex.core.Belief; +import convex.core.Block; +import convex.core.Order; +import convex.core.crypto.AKeyPair; +import convex.core.data.ACell; +import convex.core.data.AMap; +import convex.core.data.AVector; +import convex.core.data.Address; +import convex.core.data.Blob; +import convex.core.data.Format; +import convex.core.data.Hash; +import convex.core.data.Keywords; +import convex.core.data.Lists; +import convex.core.data.Maps; +import convex.core.data.Ref; +import convex.core.data.Vectors; +import convex.core.exceptions.BadFormatException; +import convex.core.init.InitTest; +import convex.core.lang.Symbols; +import convex.core.store.AStore; +import convex.core.store.Stores; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Invoke; +import convex.core.transactions.Transfer; +import convex.core.util.Utils; +import convex.test.Samples; +import etch.EtchStore; + +public class EtchStoreTest { + + private static final Hash BAD_HASH = Samples.BAD_HASH; + private EtchStore store = EtchStore.createTemp(); + + @Test + public void testEmptyStore() { + AStore oldStore = Stores.current(); + try { + Stores.setCurrent(store); + assertTrue(oldStore != store); + assertEquals(store, Stores.current()); + + assertNull(store.refForHash(BAD_HASH)); + + AMap data = Maps.of(Keywords.FOO,Symbols.FOO); + Ref> goodRef = data.getRef(); + Hash goodHash = goodRef.getHash(); + assertNull(store.refForHash(goodHash)); + + goodRef.persist(); + + if (!data.isEmbedded()) { + Ref> recRef = store.refForHash(goodHash); + assertNotNull(recRef); + + assertEquals(data, recRef.getValue()); + } + } finally { + Stores.setCurrent(oldStore); + } + } + + @Test + public void testPersistedStatus() throws BadFormatException { + AStore oldStore = Stores.current(); + try { + Stores.setCurrent(store); + + // generate Hash of unique secure random bytes to test - should not already be + // in store + Blob randomBlob = Blob.createRandom(new Random(), Format.MAX_EMBEDDED_LENGTH+1); + Hash hash = randomBlob.getHash(); + assertNotEquals(hash, randomBlob); + + Ref initialRef = randomBlob.getRef(); + assertEquals(Ref.UNKNOWN, initialRef.getStatus()); + assertNull(Stores.current().refForHash(hash)); + + // shallow persistence first + Ref refShallow=initialRef.persistShallow(); + assertEquals(Ref.STORED, refShallow.getStatus()); + + Ref ref = initialRef.persist(); + assertEquals(Ref.PERSISTED, ref.getStatus()); + assertTrue(ref.isPersisted()); + + Ref newRef = Stores.current().refForHash(hash); + assertEquals(initialRef, newRef); + assertEquals(randomBlob, newRef.getValue()); + } finally { + Stores.setCurrent(oldStore); + } + } + + @Test + public void testBeliefAnnounce() { + AStore oldStore = Stores.current(); + AtomicLong counter=new AtomicLong(0L); + + AKeyPair kp=InitTest.HERO_KEYPAIR; + try { + Stores.setCurrent(store); + + ATransaction t1=Invoke.create(InitTest.HERO,0, Lists.of(Symbols.PLUS, Symbols.STAR_BALANCE, 1000L)); + ATransaction t2=Transfer.create(InitTest.HERO,1, InitTest.VILLAIN,1000000); + Block b=Block.of(Utils.getCurrentTimestamp(),InitTest.FIRST_PEER_KEY,kp.signData(t1),kp.signData(t2)); + assertNotNull(b.getPeer()); + + Order ord=Order.create().propose(b); + + Belief belief=Belief.create(kp,ord); + + Ref rb=belief.getRef(); + Ref rt=t1.getRef(); + assertEquals(Ref.UNKNOWN,rb.getStatus()); + assertEquals(Ref.UNKNOWN,rt.getStatus()); + + assertEquals(3,Utils.refCount(t1)); + assertEquals(0,Utils.refCount(t2)); + assertEquals(11,Utils.totalRefCount(belief)); + + + Consumer> noveltyHandler=r-> { + counter.incrementAndGet(); + }; + + // First try shallow persistence + counter.set(0L); + Ref srb=rb.persistShallow(noveltyHandler); + assertEquals(Ref.STORED,srb.getStatus()); + assertEquals(1L,counter.get()); // One cell persisted + + // assertEquals(srb,store.refForHash(rb.getHash())); + assertNull(store.refForHash(t1.getRef().getHash())); + + // Persist belief + counter.set(0L); + Ref prb=srb.persist(noveltyHandler); + assertEquals(3L,counter.get()); + + // Persist again. Should be no new novelty + counter.set(0L); + Ref prb2=srb.persist(noveltyHandler); + assertEquals(prb2,prb); + assertEquals(0L,counter.get()); // Nothing new persisted + + // Announce belief + counter.set(0L); + Ref arb=belief.announce(noveltyHandler).getRef(); + assertEquals(srb,arb); + assertEquals(4L,counter.get()); + + // Announce again. Should be no new novelty + counter.set(0L); + Ref arb2=belief.announce(noveltyHandler).getRef(); + assertEquals(srb,arb2); + assertEquals(0L,counter.get()); // Nothing new announced + + // Check re-stored ref has correct status + counter.set(0L); + Ref arb3=srb.persistShallow(noveltyHandler); + assertEquals(0L,counter.get()); // Nothing new persisted + assertTrue(Ref.STORED<=arb3.getStatus()); + + // Recover Belief from store. Should be top level stored + Belief recb=(Belief) store.refForHash(belief.getHash()).getValue(); + assertEquals(belief,recb); + } finally { + Stores.setCurrent(oldStore); + } + } + + @Test + public void testNoveltyHandler() { + AStore oldStore = Stores.current(); + ArrayList> al = new ArrayList<>(); + try { + Stores.setCurrent(store); + // create a random item that shouldn't already be in the store + AVector data = Vectors.of(Blob.createRandom(new Random(), 100),Blob.createRandom(new Random(), 100)); + + // handler that records added refs + Consumer> handler = r -> al.add(r); + + Ref> dataRef = data.getRef(); + Hash dataHash = dataRef.getHash(); + assertNull(store.refForHash(dataHash)); + + ACell.createPersisted(data,handler); + int num=al.size(); // number of novel cells persisted + assertTrue(num>0); // got new novelty + assertEquals(data, al.get(num-1).getValue()); + + data.getRef().persist(); + assertEquals(num, al.size()); // no new novelty transmitted + } finally { + Stores.setCurrent(oldStore); + } + } + + @Test public void testDecodeCache() throws BadFormatException { + Address a1=Address.create(12345678); + ACell cell=store.decode(a1.getEncoding()); + assertNotSame(cell,a1); + assertEquals(cell,a1); + + // decoding again should get same value with very high probability + ACell cell2=store.decode(a1.getEncoding()); + assertSame(cell,cell2); + } + + @Test + public void testReopen() throws IOException { + File file=File.createTempFile("etch",null); + EtchStore es=EtchStore.create(file); + es.setRootHash(Hash.NULL_HASH); + es.close(); + + EtchStore es2=EtchStore.create(file); + assertEquals(Hash.NULL_HASH,es2.getRootHash()); + } +} diff --git a/convex-core/src/test/java/convex/store/MemoryStoreTest.java b/convex-core/src/test/java/convex/store/MemoryStoreTest.java new file mode 100644 index 000000000..bd35117a2 --- /dev/null +++ b/convex-core/src/test/java/convex/store/MemoryStoreTest.java @@ -0,0 +1,114 @@ +package convex.store; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Random; +import java.util.function.Consumer; + +import org.junit.Test; + +import convex.core.data.ACell; +import convex.core.data.AMap; +import convex.core.data.AVector; +import convex.core.data.Address; +import convex.core.data.Blob; +import convex.core.data.Format; +import convex.core.data.Hash; +import convex.core.data.Keywords; +import convex.core.data.Maps; +import convex.core.data.Ref; +import convex.core.data.Sets; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.store.AStore; +import convex.core.store.MemoryStore; +import convex.core.store.Stores; +import convex.test.Samples; + +public class MemoryStoreTest { + + private static final Hash BAD_HASH = Samples.BAD_HASH; + + @Test + public void testEmptyStore() { + AStore oldStore = Stores.current(); + MemoryStore ms = new MemoryStore(); + try { + Stores.setCurrent(ms); + assertTrue(oldStore != ms); + assertEquals(ms, Stores.current()); + + assertNull(ms.refForHash(BAD_HASH)); + + AMap data = Maps.of(Keywords.CODE,Address.ZERO); + Ref> goodRef = data.getRef(); + Hash goodHash = goodRef.getHash(); + assertNull(ms.refForHash(goodHash)); + + goodRef.persist(); + + if (!(data.isEmbedded())) { + Ref> recRef = ms.refForHash(goodHash); + assertNotNull(recRef); + assertEquals(data, recRef.getValue()); + } + + } finally { + Stores.setCurrent(oldStore); + } + } + + @Test + public void testPersistedStatus() throws BadFormatException { + // generate Hash of unique secure random bytes to test - should not already be + // in store + Blob value = Blob.createRandom(new Random(), Format.MAX_EMBEDDED_LENGTH); + Hash hash = value.getHash(); + assertNotEquals(hash, value); + + Ref initialRef = value.getRef(); + assertEquals(Ref.UNKNOWN, initialRef.getStatus()); + assertNull(Stores.current().refForHash(hash)); + Ref ref = initialRef.persist(); + assertEquals(Ref.PERSISTED, ref.getStatus()); + assertTrue(ref.isPersisted()); + + if (!(value.isEmbedded())) { + Ref newRef = Stores.current().refForHash(hash); + assertEquals(initialRef, newRef); + assertEquals(value, newRef.getValue()); + } + } + + @Test + public void testNoveltyHandler() { + AStore oldStore = Stores.current(); + MemoryStore ms = new MemoryStore(); + ArrayList> al = new ArrayList<>(); + try { + Stores.setCurrent(ms); + ACell data = Sets.of(Samples.INT_SET_10,15685995L,Samples.INT_VECTOR_300,Samples.MAX_EMBEDDED_BLOB); // should be novel + + Consumer> handler = r -> al.add(r); + + Ref> dataRef = data.getRef(); + Hash dataHash = dataRef.getHash(); + assertNull(ms.refForHash(dataHash)); + + dataRef.persist(handler); + int num=al.size(); + assertTrue(num>0); + assertEquals(data, al.get(num-1).getValue()); + + data.getRef().persist(); + assertEquals(num, al.size()); // no new novelty transmitted + } finally { + Stores.setCurrent(oldStore); + } + } +} diff --git a/convex-core/src/test/java/convex/store/ParamTestStores.java b/convex-core/src/test/java/convex/store/ParamTestStores.java new file mode 100644 index 000000000..9f89043f8 --- /dev/null +++ b/convex-core/src/test/java/convex/store/ParamTestStores.java @@ -0,0 +1,5 @@ +package convex.store; + +public class ParamTestStores { + +} diff --git a/convex-core/src/test/java/convex/test/Assertions.java b/convex-core/src/test/java/convex/test/Assertions.java new file mode 100644 index 000000000..9fe9066a6 --- /dev/null +++ b/convex-core/src/test/java/convex/test/Assertions.java @@ -0,0 +1,115 @@ +package convex.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +import convex.core.ErrorCodes; +import convex.core.lang.Context; +import convex.core.lang.RT; +import convex.core.util.Utils; + +public class Assertions { + + public static void assertNotError(Context ctx) { + if(ctx.isExceptional()) { + fail("Expected no error but got: " + ctx.getValue()); + } + } + + public static void assertTotalRefCount(long expected, Object o) { + long count = Utils.totalRefCount(o); + assertEquals(expected, count, + () -> "Wrong number of Refs, expected " + expected + " but got " + o + " in object " + o); + } + + public static void assertCVMEquals(Object expected, Object result) { + assertEquals((Object)RT.cvm(expected),RT.cvm(result)); + } + + public static void assertError(Object et, Context ctx) { + Object cet = ctx.getErrorCode(); + assertEquals(et, cet, "Expected error type " + et + " but got result: " + ctx.getValue()); + } + + public static void assertError(Context ctx) { + Object cet = ctx.getErrorCode(); + assertNotNull(cet, "Expected an error but got result: " + ctx.getValue()); + } + + public static void assertArityError(Context ctx) { + Object cet = ctx.getErrorCode(); + assertEquals(ErrorCodes.ARITY, cet, "Expected ARITY error but got result: " + ctx.getValue()); + } + + public static void assertTrustError(Context ctx) { + Object cet = ctx.getErrorCode(); + assertEquals(ErrorCodes.TRUST, cet, "Expected TRUST error but got result: " + ctx.getValue()); + } + + public static void assertCompileError(Context ctx) { + Object cet = ctx.getErrorCode(); + assertEquals(ErrorCodes.COMPILE, cet, "Expected COMPILE error but got result: " + ctx.getValue()); + } + + public static void assertBoundsError(Context ctx) { + Object cet = ctx.getErrorCode(); + assertEquals(ErrorCodes.BOUNDS, cet, "Expected BOUNDS error but got result: " + ctx.getValue()); + } + + public static void assertCastError(Context ctx) { + Object cet = ctx.getErrorCode(); + assertEquals(ErrorCodes.CAST, cet, "Expected CAST error but got result: " + ctx.getValue()); + } + + public static void assertDepthError(Context ctx) { + Object cet = ctx.getErrorCode(); + assertEquals(ErrorCodes.DEPTH, cet, "Expected DEPTH error but got: " + ctx.getValue()); + } + + public static void assertJuiceError(Context ctx) { + Object cet = ctx.getErrorCode(); + assertEquals(ErrorCodes.JUICE, cet, "Expected JUICE error but got: " + ctx.getValue()); + } + + public static void assertUndeclaredError(Context ctx) { + Object cet = ctx.getErrorCode(); + assertEquals(ErrorCodes.UNDECLARED, cet, "Expected UNDECLARED error but got: " + ctx.getValue()); + } + + public static void assertStateError(Context ctx) { + Object cet = ctx.getErrorCode(); + assertEquals(ErrorCodes.STATE, cet, "Expected STATE error but got: " + ctx.getValue()); + } + + public static void assertArgumentError(Context ctx) { + Object cet = ctx.getErrorCode(); + assertEquals(ErrorCodes.ARGUMENT, cet, "Expected ARGUMENT error but got: " + ctx.getValue()); + } + + public static void assertMemoryError(Context ctx) { + Object cet = ctx.getErrorCode(); + assertEquals(ErrorCodes.MEMORY, cet, "Expected MEMORY error but got: " + ctx.getValue()); + } + + + public static void assertFundsError(Context ctx) { + Object cet = ctx.getErrorCode(); + assertEquals(ErrorCodes.FUNDS, cet, "Expected FUNDS error but got: " + ctx.getValue()); + } + + public static void assertNobodyError(Context ctx) { + Object cet = ctx.getErrorCode(); + assertEquals(ErrorCodes.NOBODY, cet, "Expected NOBODY error but got: " + ctx.getValue()); + } + + public static void assertSequenceError(Context ctx) { + Object cet = ctx.getErrorCode(); + assertEquals(ErrorCodes.SEQUENCE, cet, "Expected SEQUENCE error but got: " + ctx.getValue()); + } + + public static void assertAssertError(Context ctx) { + Object cet = ctx.getErrorCode(); + assertEquals(ErrorCodes.ASSERT, cet, "Expected ASSERT error but got: " + ctx.getValue()); + } +} diff --git a/convex-core/src/test/java/convex/test/Samples.java b/convex-core/src/test/java/convex/test/Samples.java new file mode 100644 index 000000000..5ce414f11 --- /dev/null +++ b/convex-core/src/test/java/convex/test/Samples.java @@ -0,0 +1,282 @@ +package convex.test; + +import static org.junit.Assert.assertTrue; + +import java.util.Random; +import java.util.stream.Stream; + +import org.junit.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +import convex.core.crypto.AKeyPair; +import convex.core.crypto.ASignature; +import convex.core.crypto.Ed25519KeyPair; +import convex.core.crypto.Ed25519Signature; +import convex.core.data.ACell; +import convex.core.data.ADataStructure; +import convex.core.data.AMap; +import convex.core.data.ASequence; +import convex.core.data.ASet; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.Address; +import convex.core.data.Blob; +import convex.core.data.BlobMap; +import convex.core.data.BlobMaps; +import convex.core.data.BlobTree; +import convex.core.data.Blobs; +import convex.core.data.Format; +import convex.core.data.Hash; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.List; +import convex.core.data.Lists; +import convex.core.data.LongBlob; +import convex.core.data.MapLeaf; +import convex.core.data.MapTree; +import convex.core.data.Maps; +import convex.core.data.Sets; +import convex.core.data.StringShort; +import convex.core.data.StringTree; +import convex.core.data.Strings; +import convex.core.data.Syntax; +import convex.core.data.VectorLeaf; +import convex.core.data.VectorTree; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMBool; +import convex.core.data.prim.CVMByte; +import convex.core.data.prim.CVMChar; +import convex.core.data.prim.CVMDouble; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.InvalidDataException; +import convex.core.exceptions.ValidationException; +import convex.core.init.Init; +import convex.core.lang.RT; +import convex.core.lang.Symbols; +import convex.core.lang.ops.Do; +import convex.core.transactions.Invoke; + +/** + * Miscellaneous value objects for testing purposes + * + */ +public class Samples { + + public static Hash BAD_HASH = Hash.fromHex("1234000012340000123400001234000012340000123400001234000012340000"); + + /** + * An Address which cannot be valid + */ + public static final Address BAD_ADDRESS = Address.create(7777777777L); + public static final AccountKey BAD_ACCOUNTKEY = AccountKey.dummy("bbbb"); + + public static final AKeyPair KEY_PAIR=Ed25519KeyPair.createSeeded(13371337L); + public static final AccountKey ACCOUNT_KEY = KEY_PAIR.getAccountKey(); + + public static final ASignature BAD_SIGNATURE = Ed25519Signature.wrap(Blobs.createRandom(64).getBytes()); + + + + public static final VectorLeaf INT_VECTOR_10 = createTestIntVector(10); + public static final VectorLeaf INT_VECTOR_16 = createTestIntVector(16); + public static final VectorLeaf INT_VECTOR_23 = createTestIntVector(23); + public static final VectorTree INT_VECTOR_32 = createTestIntVector(32); + public static final VectorTree INT_VECTOR_256 = createTestIntVector(256); + public static final VectorLeaf INT_VECTOR_300 = createTestIntVector(300); + + public static final AVector> VECTOR_OF_VECTORS = Vectors.of(INT_VECTOR_10, INT_VECTOR_16, + INT_VECTOR_23); + + public static final List INT_LIST_10 = Lists.create(INT_VECTOR_10); + public static final List INT_LIST_300 = Lists.create(INT_VECTOR_300); + + public static final ASet INT_SET_10 = Sets.create(INT_VECTOR_10); + public static final ASet INT_SET_300 = Sets.create(INT_VECTOR_300); + + + public static final MapLeaf LONG_MAP_5 = createTestLongMap(5); + public static final MapTree LONG_MAP_10 = createTestLongMap(10); + public static final MapTree LONG_MAP_100 = createTestLongMap(100); + + public static final BlobMap INT_BLOBMAP_7 = BlobMaps.of(Blob.fromHex(""), 0, Blob.fromHex("0001"), 1, + Blob.fromHex("01"), 2, Blob.fromHex("010000"), 3, Blob.fromHex("010001"), 4, Blob.fromHex("ff0000"), 5, + Blob.fromHex("ff0101"), 6); + + public static final ASet LONG_SET_5 = Sets.of(1,2,3,4,5); + public static final ASet LONG_SET_10 = Sets.create(INT_VECTOR_10); + public static final ASet LONG_SET_100 = Sets.create(INT_VECTOR_300); + + public static final Blob ONE_ZERO_BYTE_DATA = Blob.fromHex("00"); + + public static final Keyword FOO = Keyword.create("foo"); + public static final Keyword BAR = Keyword.create("bar"); + + public static final AVector DIABOLICAL_VECTOR_30_30; + public static final AVector DIABOLICAL_VECTOR_2_10000; + public static final AMap DIABOLICAL_MAP_30_30; + public static final AMap DIABOLICAL_MAP_2_10000; + + public static final Random rand = new Random(123); + + public static final long BIG_BLOB_LENGTH = 10000; + public static final BlobTree BIG_BLOB_TREE = Blobs.createRandom(Samples.rand, BIG_BLOB_LENGTH); + public static final Blob FULL_BLOB = Blobs.createRandom(Samples.rand, Blob.CHUNK_LENGTH); + + public static final ASignature FAKE_SIGNATURE = Ed25519Signature.wrap(new byte[Ed25519Signature.SIGNATURE_LENGTH]); + + public static final Blob MAX_EMBEDDED_BLOB = createTestBlob(Format.MAX_EMBEDDED_LENGTH-Format.getVLCLength(Format.MAX_EMBEDDED_LENGTH)-1); + public static final Blob NON_EMBEDDED_BLOB = createTestBlob(MAX_EMBEDDED_BLOB.count()+1); + + public static final StringShort MAX_EMBEDDED_STRING= StringShort.create("[0x1234567812345678123456781234567812345678123456781234567812345678]"); + public static final StringShort NON_EMBEDDED_STRING= StringShort.create(MAX_EMBEDDED_STRING.toString()+" "); + public static final StringShort MAX_SHORT_STRING= StringShort.create(createRandomString(StringShort.MAX_LENGTH)); + public static final StringTree MIN_TREE_STRING= StringTree.create(createRandomString(StringTree.MINIMUM_LENGTH)); + + + + static { + try { + // we should be able to actually build these, thanks to structural sharing. + DIABOLICAL_VECTOR_30_30 = createNastyNestedVector(30, 30); + DIABOLICAL_VECTOR_2_10000 = createNastyNestedVector(2, 10000); + DIABOLICAL_MAP_30_30 = createNastyNestedMap(30, 30); + DIABOLICAL_MAP_2_10000 = createNastyNestedMap(2, 10000); + } catch (Throwable t) { + t.printStackTrace(); + throw t; + } + } + + + /** + * Create a random test Blob of the given size + * @param size + * @return + */ + static Blob createTestBlob(long size) { + Blob b=Blob.createRandom(new Random(), size); + return b; + } + + private static String createRandomString(int n) { + char [] cs=new char[n]; + for (int i=0; i> T createTestIntVector(int size) { + AVector v = Vectors.empty(); + for (int i = 0; i < size; i++) { + v = v.append(CVMLong.create(i)); + } + return (T) v; + } + + private static AMap createNastyNestedMap(int fanout, int depth) { + AMap m = Maps.empty(); + for (long i = 0; i < depth; i++) { + m = createRepeatedValueMap(m, fanout); + m.getHash(); // needed to to stop hash calculations getting too deep + } + return m; + } + + private static AMap createRepeatedValueMap(Object v, int count) { + Object[] obs = new Object[count * 2]; + for (int i = 0; i < count; i++) { + obs[i * 2] = RT.cvm(i); + obs[i * 2 + 1] = RT.cvm(v); + } + return Maps.of(obs); + } + + @SuppressWarnings("unchecked") + public static R createRandomSubset(ADataStructure v, double prob, int seed) { + ADataStructure result=v.empty(); + + Random r=new Random(seed); + for (T o: (ASequence)RT.sequence(v)) { + if (r.nextDouble()<=prob) { + result=result.conj(o); + } + } + return (R) result; + } + + private static AVector createNastyNestedVector(int fanout, int depth) { + AVector m = Vectors.empty(); + for (int i = 0; i < depth; i++) { + m = Vectors.repeat(m, fanout); + m.getHash(); // needed to to stop hash calculations getting too deep + } + return m; + } + + @SuppressWarnings("unchecked") + private static > T createTestLongMap(int n) { + AMap a = Maps.empty(); + for (long i = 0; i < n; i++) { + CVMLong cl=CVMLong.create(i); + a = a.assoc(cl, cl); + } + return (T) a; + } + + @Test + public void validateDataObjects() throws InvalidDataException, ValidationException { + INT_VECTOR_300.validate(); + assertTrue(INT_VECTOR_300.isCanonical()); + INT_VECTOR_10.validate(); + assertTrue(INT_VECTOR_10.isCanonical()); + BAD_HASH.validate(); + } + + public static ACell[] VALUES=new ACell[] { + null, + Keywords.FOO, + FULL_BLOB, + MAX_EMBEDDED_BLOB, + LongBlob.create(-1), + INT_LIST_10, + Lists.empty(), + INT_VECTOR_300, + Vectors.empty(), + LONG_MAP_100, + Syntax.of(1), + Syntax.create(Vectors.empty(),Maps.of(1,2)), + Invoke.create(Init.GENESIS_ADDRESS, 0, (ACell)null), + Maps.empty(), + LONG_SET_10, + Sets.empty(), + Sets.of(1,2,3), + CVMDouble.ONE, + CVMDouble.NaN, + CVMLong.MAX_VALUE, + CVMLong.MIN_VALUE, + CVMByte.ZERO, + CVMBool.TRUE, + CVMBool.FALSE, + MAX_SHORT_STRING, + BAD_HASH, + Symbols.FOO, + Strings.EMPTY, + MAX_EMBEDDED_STRING, + Do.EMPTY, + Address.ZERO, + Address.create(666666), + AccountKey.ZERO, + CVMChar.A + }; + + public static class ValueArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(VALUES).map(cell -> Arguments.of(cell)); + } + } +} diff --git a/convex-core/src/test/java/convex/test/Testing.java b/convex-core/src/test/java/convex/test/Testing.java new file mode 100644 index 000000000..60d6ee612 --- /dev/null +++ b/convex-core/src/test/java/convex/test/Testing.java @@ -0,0 +1,39 @@ +package convex.test; + +import java.io.IOException; + +import convex.core.Constants; +import convex.core.data.ACell; +import convex.core.data.AList; +import convex.core.lang.Context; +import convex.core.lang.Reader; +import convex.core.util.Utils; + +public class Testing { + + /** + * Runs all tests in a forked context + * @param ctx + * @param resourceName + * @return Updates context after all test are run. This will be a new fork. + */ + public static Context runTests(Context ctx, String resourceName) { + ctx=ctx.fork(); + try { + String source=Utils.readResourceAsString(resourceName); + AList forms=Reader.readAll(source); + for (ACell form: forms) { + ctx=ctx.eval(form); + if (ctx.isExceptional()) { + System.err.println("Error in form: "+form); + return ctx; + } + ctx=ctx.withJuice(Constants.MAX_TRANSACTION_JUICE); + } + return ctx; + } catch (IOException e) { + throw Utils.sneakyThrow(e); + } + } + +} diff --git a/convex-core/src/test/java/convex/test/generators/AddressGen.java b/convex-core/src/test/java/convex/test/generators/AddressGen.java new file mode 100644 index 000000000..96b54ebbb --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/AddressGen.java @@ -0,0 +1,19 @@ +package convex.test.generators; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.data.Address; + +public class AddressGen extends Generator
{ + public AddressGen() { + super(Address.class); + } + + @Override + public Address generate(SourceOfRandomness r, GenerationStatus status) { + + return Address.create(r.nextLong(0, Long.MAX_VALUE)); + } +} diff --git a/convex-core/src/test/java/convex/test/generators/AnyMapGen.java b/convex-core/src/test/java/convex/test/generators/AnyMapGen.java new file mode 100644 index 000000000..80810cb2f --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/AnyMapGen.java @@ -0,0 +1,60 @@ +package convex.test.generators; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.data.ABlob; +import convex.core.data.AMap; +import convex.core.data.AString; +import convex.core.data.AVector; +import convex.core.data.BlobMap; +import convex.core.data.Format; +import convex.core.data.Maps; +import convex.test.Samples; + +/** + * Generator for arbitrary maps, including BlobMaps + * + */ +@SuppressWarnings("rawtypes") +public class AnyMapGen extends Generator { + public AnyMapGen() { + super(AMap.class); + } + + @Override + public AMap generate(SourceOfRandomness r, GenerationStatus status) { + + int type = r.nextInt(); + switch (type % 8) { + case 0: + return Maps.empty(); + case 1: + return Samples.LONG_MAP_5; + case 2: + return Samples.LONG_MAP_10; + case 3: + return Samples.LONG_MAP_100; + case 4: { + Object o1 = gen().make(PrimitiveGen.class).generate(r, status); + Object o2 = gen().make(StringGen.class).generate(r, status); + return Maps.of(o1, o2); + } + case 5: + return BlobMap.EMPTY; + case 6: { + ABlob o1 = Format.encodedBlob(gen().make(PrimitiveGen.class).generate(r, status)); + AString o2 = gen().make(StringGen.class).generate(r, status); + return BlobMap.create(o1, o2); + } + + default: { + AVector vec = gen().make(VectorGen.class).generate(r, status); + vec = (AVector) vec.subList(0, vec.size() & (~1)); + Object[] os = vec.toArray(); + return Maps.of(os); + } + } + } +} diff --git a/convex-core/src/test/java/convex/test/generators/BlobGen.java b/convex-core/src/test/java/convex/test/generators/BlobGen.java new file mode 100644 index 000000000..7fed957aa --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/BlobGen.java @@ -0,0 +1,48 @@ +package convex.test.generators; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.data.ABlob; +import convex.core.data.Blobs; +import convex.core.data.LongBlob; +import convex.test.Samples; + +/** + * Generator for binary Blobs + * + */ +public class BlobGen extends Generator { + public BlobGen() { + super(ABlob.class); + } + + @Override + public ABlob generate(SourceOfRandomness r, GenerationStatus status) { + + long len = status.size(); + int type = r.nextInt(); + switch (type % 10) { + case 0: + return LongBlob.create(r.nextLong()); + case 1: + return Samples.FULL_BLOB; + case 2: + return Samples.BIG_BLOB_TREE; + case 3: + return Samples.MAX_EMBEDDED_BLOB; + case 4: + return Samples.NON_EMBEDDED_BLOB; + case 5: { + // use a slice from a big blob + long length=Math.min(len, Samples.BIG_BLOB_LENGTH); + length=r.nextLong(0, length); + long start=r.nextLong(0,Samples.BIG_BLOB_LENGTH-length); + return Samples.BIG_BLOB_TREE.slice(start,length); + } + default: + return Blobs.createRandom(r.toJDKRandom(), len); + } + } +} diff --git a/convex-core/src/test/java/convex/test/generators/CollectionGen.java b/convex-core/src/test/java/convex/test/generators/CollectionGen.java new file mode 100644 index 000000000..6a4d5d340 --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/CollectionGen.java @@ -0,0 +1,38 @@ +package convex.test.generators; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.data.ACell; +import convex.core.data.ACollection; + +/** + * Generator for arbitrary collections + */ +public class CollectionGen extends Generator> { + @SuppressWarnings("rawtypes") + private static final Class cls = (Class) ACollection.class; + + @SuppressWarnings("unchecked") + public CollectionGen() { + super(cls); + } + + @SuppressWarnings("unchecked") + @Override + public ACollection generate(SourceOfRandomness r, GenerationStatus status) { + int type = r.nextInt(3); + switch (type) { + case 0: + return gen().make(VectorGen.class).generate(r, status); + case 1: + return gen().make(ListGen.class).generate(r, status); + case 2: + return gen().make(SetGen.class).generate(r, status); + + default: + throw new Error("Unexpected type: " + type); + } + } +} diff --git a/convex-core/src/test/java/convex/test/generators/DataStructureGen.java b/convex-core/src/test/java/convex/test/generators/DataStructureGen.java new file mode 100644 index 000000000..0e33fae7f --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/DataStructureGen.java @@ -0,0 +1,46 @@ +package convex.test.generators; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.data.ACell; +import convex.core.data.ADataStructure; +import convex.core.data.MapEntry; + +/** + * Generator for arbitrary collections + */ +public class DataStructureGen extends Generator> { + @SuppressWarnings("rawtypes") + private static final Class cls = (Class) ADataStructure.class; + + @SuppressWarnings("unchecked") + public DataStructureGen() { + super(cls); + } + + @SuppressWarnings("unchecked") + @Override + public ADataStructure generate(SourceOfRandomness r, GenerationStatus status) { + int type = r.nextInt(5); + + switch (type) { + case 0: + return gen().make(MapGen.class).generate(r, status); + case 1: + return gen().make(VectorGen.class).generate(r, status); + case 2: + return gen().make(ListGen.class).generate(r, status); + case 3: + return gen().make(SetGen.class).generate(r, status); + + // generate map entries as special cases of vectors + case 4: { + Generator vgen = gen().make(ValueGen.class); + return MapEntry.create(vgen.generate(r, status), vgen.generate(r, status)); + } + } + throw new Error("Bad Type!"); + } +} diff --git a/convex-core/src/test/java/convex/test/generators/FormGen.java b/convex-core/src/test/java/convex/test/generators/FormGen.java new file mode 100644 index 000000000..62788a2be --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/FormGen.java @@ -0,0 +1,72 @@ +package convex.test.generators; + +import java.util.List; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.data.ACell; +import convex.core.data.AHashMap; +import convex.core.data.Lists; +import convex.core.data.Strings; +import convex.core.data.Symbol; +import convex.core.data.Syntax; +import convex.core.data.Vectors; +import convex.core.lang.Core; +import convex.core.lang.RT; + +/** + * Generator for plausible forms + */ +public class FormGen extends Generator { + public FormGen() { + super(ACell.class); + } + + @Override + public ACell generate(SourceOfRandomness r, GenerationStatus status) { + int type = r.nextInt(8); + switch (type) { + case 0: + return null; + + case 1: + return Syntax.create(generate(r, status)); + + case 2: + return gen().make(PrimitiveGen.class).generate(r, status); + case 3: + return Strings.create(gen().type(String.class).generate(r, status)); + + case 4: + return gen().make(NumericGen.class).generate(r, status); + + case 5: { + // random form containing core symbol at head + List subForms = this.times(r.nextInt(4)).generate(r, status); + AHashMap env = Core.ENVIRONMENT; + int n = (int) env.count(); + Symbol sym = env.entryAt(r.nextInt(n)).getKey(); + return RT.cons(sym, Lists.create(subForms)); + } + + case 6: { + // random core symbol + AHashMap env = Core.ENVIRONMENT; + int n = (int) env.count(); + Symbol sym = env.entryAt(r.nextInt(n)).getKey(); + return sym; + } + + case 7: { + // a vector of random subforms + List subForms = this.times(r.nextInt(4)).generate(r, status); + return Vectors.create(subForms); + } + + default: + throw new Error("Unexpected type: " + type); + } + } +} diff --git a/convex-core/src/test/java/convex/test/generators/KeywordGen.java b/convex-core/src/test/java/convex/test/generators/KeywordGen.java new file mode 100644 index 000000000..282943353 --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/KeywordGen.java @@ -0,0 +1,33 @@ +package convex.test.generators; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.data.Keyword; +import convex.test.Samples; + +/** + * Generator for Keyword objects + * + */ +public class KeywordGen extends Generator { + public KeywordGen() { + super(Keyword.class); + } + + @Override + public Keyword generate(SourceOfRandomness r, GenerationStatus status) { + + int type = r.nextInt(5); + switch (type) { + case 0: + return Samples.FOO; + case 1: + return Samples.BAR; + default: { + return Keyword.create("key" + r.nextLong(0, status.size())); + } + } + } +} diff --git a/convex-core/src/test/java/convex/test/generators/ListGen.java b/convex-core/src/test/java/convex/test/generators/ListGen.java new file mode 100644 index 000000000..c64090fe7 --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/ListGen.java @@ -0,0 +1,26 @@ +package convex.test.generators; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.data.AList; +import convex.core.data.List; + +/** + * Generator for vectors of arbitrary values + * + */ +@SuppressWarnings("rawtypes") +public class ListGen extends Generator { + public ListGen() { + super(AList.class); + } + + @SuppressWarnings("unchecked") + @Override + public AList generate(SourceOfRandomness r, GenerationStatus status) { + + return List.reverse(gen().make(VectorGen.class).generate(r, status)); + } +} diff --git a/convex-core/src/test/java/convex/test/generators/MapGen.java b/convex-core/src/test/java/convex/test/generators/MapGen.java new file mode 100644 index 000000000..fe67bbc82 --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/MapGen.java @@ -0,0 +1,48 @@ +package convex.test.generators; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.data.ACell; +import convex.core.data.AHashMap; +import convex.core.data.AVector; +import convex.core.data.Maps; +import convex.test.Samples; + +/** + * Generator for arbitrary maps + * + */ +@SuppressWarnings("rawtypes") +public class MapGen extends Generator { + public MapGen() { + super(AHashMap.class); + } + + @SuppressWarnings("unchecked") + @Override public AHashMap generate( + SourceOfRandomness r, + GenerationStatus status) { + + int type=r.nextInt(); + switch (type%6) { + case 0: return Maps.empty(); + case 1: return Samples.LONG_MAP_5; + case 2: return Samples.LONG_MAP_10; + case 3: return Samples.LONG_MAP_100; + case 4: { + ACell o1=gen().make(PrimitiveGen.class).generate(r, status); + ACell o2=gen().make(StringGen.class).generate(r, status); + return Maps.create(o1,o2); + } + + default: { + AVector vec=gen().make(VectorGen.class).generate(r, status); + vec=(AVector) vec.subList(0, vec.size()&(~1)); + Object[] os=vec.toArray(); + return Maps.of(os); + } + } + } +} diff --git a/convex-core/src/test/java/convex/test/generators/NumericGen.java b/convex-core/src/test/java/convex/test/generators/NumericGen.java new file mode 100644 index 000000000..27cbe9c30 --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/NumericGen.java @@ -0,0 +1,39 @@ +package convex.test.generators; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.data.prim.APrimitive; +import convex.core.data.prim.CVMByte; +import convex.core.data.prim.CVMDouble; +import convex.core.data.prim.CVMLong; + +/** + * Generator for arbitrary numeric values + * + */ +public class NumericGen extends Generator { + public NumericGen() { + super(APrimitive.class); + } + + @Override + public APrimitive generate(SourceOfRandomness r, GenerationStatus status) { + int type = r.nextInt(3); + switch (type) { + case 0: + return CVMByte.create(r.nextLong()); + case 1: + return CVMLong.create(r.nextLong()); + case 2: + return CVMDouble.create(r.nextDouble()); +// TODO: bigger numerics? +// case 6: +// return gen().type(BigInteger.class).generate(r, status); +// case 7: +// return gen().type(BigDecimal.class).generate(r, status); + } + throw new Error("Unexpected type: " + type); + } +} diff --git a/convex-core/src/test/java/convex/test/generators/PrimitiveGen.java b/convex-core/src/test/java/convex/test/generators/PrimitiveGen.java new file mode 100644 index 000000000..1eba23e18 --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/PrimitiveGen.java @@ -0,0 +1,46 @@ +package convex.test.generators; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.data.ACell; +import convex.core.data.prim.CVMBool; +import convex.core.data.prim.CVMByte; +import convex.core.data.prim.CVMChar; +import convex.core.data.prim.CVMDouble; +import convex.core.data.prim.CVMLong; + +/** + * Generator for primitive data values + */ +public class PrimitiveGen extends Generator { + public final static PrimitiveGen INSTANCE = new PrimitiveGen(); + + // public final Generator BYTE = gen().type(byte.class); + + public PrimitiveGen() { + super(ACell.class); + } + + @Override + public ACell generate(SourceOfRandomness r, GenerationStatus status) { + int type = r.nextInt(6); + switch (type) { + case 0: + return null; + case 1: + return CVMByte.create(r.nextLong()); + case 2: + return CVMChar.create(r.nextLong()); + case 3: + return CVMLong.create(r.nextLong()); + case 4: + return CVMDouble.create(r.nextDouble()); + case 5: + return CVMBool.create(r.nextBoolean()); + default: + throw new Error("Unexpected type: " + type); + } + } +} diff --git a/convex-core/src/test/java/convex/test/generators/RecordGen.java b/convex-core/src/test/java/convex/test/generators/RecordGen.java new file mode 100644 index 000000000..492fb300e --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/RecordGen.java @@ -0,0 +1,36 @@ +package convex.test.generators; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.Belief; +import convex.core.Block; +import convex.core.Constants; +import convex.core.data.ARecord; +import convex.core.init.InitTest; +import convex.core.lang.TestState; + +/** + * Generator for binary Blobs + * + */ +public class RecordGen extends Generator { + public RecordGen() { + super(ARecord.class); + } + + @Override + public ARecord generate(SourceOfRandomness r, GenerationStatus status) { + + int type = r.nextInt(); + switch (type % 8) { + case 0: + return Belief.createSingleOrder(InitTest.HERO_KEYPAIR); + case 1: + return TestState.STATE; + default: + return Block.of(Constants.INITIAL_TIMESTAMP,InitTest.FIRST_PEER_KEY); + } + } +} diff --git a/convex-core/src/test/java/convex/test/generators/SetGen.java b/convex-core/src/test/java/convex/test/generators/SetGen.java new file mode 100644 index 000000000..2d343cff4 --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/SetGen.java @@ -0,0 +1,46 @@ +package convex.test.generators; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.data.ACell; +import convex.core.data.ASet; +import convex.core.data.AVector; +import convex.core.data.Sets; +import convex.test.Samples; + +/** + * Generator for sets of values + */ +@SuppressWarnings("rawtypes") +public class SetGen extends Generator { + public SetGen() { + super(ASet.class); + } + + @SuppressWarnings("unchecked") + @Override + public ASet generate(SourceOfRandomness r, GenerationStatus status) { + + int type = r.nextInt(); + switch (type % 6) { + case 0: + return Sets.empty(); + case 1: { + Object o1 = gen().make(ValueGen.class).generate(r, status); + return Sets.of(o1); + } + case 2: + return Samples.LONG_SET_5; + case 3: + return Samples.LONG_SET_10; + case 4: + return Samples.LONG_SET_100; + default: { + AVector o1 = gen().make(VectorGen.class).generate(r, status); + return Sets.create(o1); + } + } + } +} diff --git a/convex-core/src/test/java/convex/test/generators/StringGen.java b/convex-core/src/test/java/convex/test/generators/StringGen.java new file mode 100644 index 000000000..6de3dbce5 --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/StringGen.java @@ -0,0 +1,32 @@ +package convex.test.generators; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.data.AString; +import convex.core.data.Strings; +import convex.test.Samples; + +public class StringGen extends Generator { + public StringGen() { + super(AString.class); + } + + @Override + public AString generate(SourceOfRandomness r, GenerationStatus status) { + + int type=r.nextInt(); + switch (type%12) { + case 0: return Strings.empty(); + case 1: return Samples.MAX_EMBEDDED_STRING; + case 2: return Samples.NON_EMBEDDED_STRING; + case 3: return Samples.MAX_SHORT_STRING; + case 4: return Samples.MIN_TREE_STRING; + + default: { + return Strings.create(gen().type(String.class).generate(r, status)); + } + } + } +} diff --git a/convex-core/src/test/java/convex/test/generators/TransactionGen.java b/convex-core/src/test/java/convex/test/generators/TransactionGen.java new file mode 100644 index 000000000..8b691bf20 --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/TransactionGen.java @@ -0,0 +1,39 @@ +package convex.test.generators; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.Constants; +import convex.core.data.Address; +import convex.core.data.Vectors; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Invoke; +import convex.core.transactions.Transfer; +import convex.test.Samples; + +public class TransactionGen extends Generator { + public TransactionGen() { + super(ATransaction.class); + } + + @Override + public ATransaction generate(SourceOfRandomness r, GenerationStatus status) { + + long amt = r.nextLong(0, Constants.MAX_SUPPLY); + + Address src = Address.create(Samples.KEY_PAIR.getAccountKey()); + long seq = r.nextInt(10000); + int type = r.nextInt(2); + switch (type) { + case 0: { + return Transfer.create(src,seq, src, amt); + } + case 1: { + return Invoke.create(src,seq, Vectors.empty()); + } + default: + throw new Error("Invalid type: " + type); + } + } +} diff --git a/convex-core/src/test/java/convex/test/generators/ValueGen.java b/convex-core/src/test/java/convex/test/generators/ValueGen.java new file mode 100644 index 000000000..64ae987e7 --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/ValueGen.java @@ -0,0 +1,59 @@ +package convex.test.generators; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.data.ACell; +import convex.core.data.Symbol; +import convex.core.data.Syntax; +import convex.core.data.prim.CVMLong; + +/** + * Generator for arbitrary values + */ +public class ValueGen extends Generator { + public ValueGen() { + super(ACell.class); + } + + @Override + public ACell generate(SourceOfRandomness r, GenerationStatus status) { + int type = r.nextInt(15); + switch (type) { + case 0: + return null; + case 1: + return gen().make(PrimitiveGen.class).generate(r, status); + case 2: + return gen().make(StringGen.class).generate(r, status); + case 3: + return gen().make(VectorGen.class).generate(r, status); + case 4: + return gen().make(ListGen.class).generate(r, status); + case 5: + return gen().make(MapGen.class).generate(r, status); + case 6: + return gen().make(SetGen.class).generate(r, status); + case 7: + return gen().make(BlobGen.class).generate(r, status); + case 8: + return gen().make(AddressGen.class).generate(r, status); + case 9: + return gen().make(NumericGen.class).generate(r, status); + case 10: + return CVMLong.create(r.nextLong()); + case 11: + return Symbol.create("sym" + gen().type(Long.class).generate(r, status)); + case 12: + return gen().make(KeywordGen.class).generate(r, status); + case 13: + return gen().make(RecordGen.class).generate(r, status); + case 14: + return Syntax.create(generate(r, status)); + + default: + throw new Error("Unexpected type: " + type); + } + } +} diff --git a/convex-core/src/test/java/convex/test/generators/VectorGen.java b/convex-core/src/test/java/convex/test/generators/VectorGen.java new file mode 100644 index 000000000..ed9636aaf --- /dev/null +++ b/convex-core/src/test/java/convex/test/generators/VectorGen.java @@ -0,0 +1,70 @@ +package convex.test.generators; + +import com.pholser.junit.quickcheck.generator.GenerationStatus; +import com.pholser.junit.quickcheck.generator.Generator; +import com.pholser.junit.quickcheck.random.SourceOfRandomness; + +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.MapEntry; +import convex.core.data.Vectors; +import convex.test.Samples; + +/** + * Generator for vectors of arbitrary values + * + */ +@SuppressWarnings("rawtypes") +public class VectorGen extends Generator { + public VectorGen() { + super(AVector.class); + } + + @Override + public AVector generate(SourceOfRandomness r, GenerationStatus status) { + + int type = r.nextInt(15); + switch (type) { + case 0: { + Object o = gen().make(PrimitiveGen.class).generate(r, status); + return Vectors.of(o); + } + case 1: { + Object o1 = gen().make(PrimitiveGen.class).generate(r, status); + Object o2 = gen().make(StringGen.class).generate(r, status); + return Vectors.of(o1, o2); + } + case 2: { + Object o1 = gen().make(ValueGen.class).generate(r, status); + Object o2 = gen().make(StringGen.class).generate(r, status); + Object o3 = gen().make(FormGen.class).generate(r, status); + return Vectors.of(o1, o2, o3); + } + + case 3: + return Samples.INT_VECTOR_10; + case 4: + return Samples.INT_VECTOR_300; + case 5: + return Samples.INT_VECTOR_16; + case 6: + return Samples.INT_VECTOR_256; + case 7: + return Vectors.empty(); + case 8: { + ACell o1 = gen().make(ValueGen.class).generate(r, status); + ACell o2 = gen().make(ValueGen.class).generate(r, status); + return MapEntry.create(o1, o2); + } + default: { + int n = (int) (1 + (Math.sqrt(status.size()))); + Object[] obs = new Object[n]; + ValueGen g = gen().make(ValueGen.class); + for (int i = 0; i < n; i++) { + obs[i] = g.generate(r, status); + } + return Vectors.of(obs); + } + } + } +} diff --git a/convex-core/src/test/java/convex/util/BigIntegerParamTest.java b/convex-core/src/test/java/convex/util/BigIntegerParamTest.java new file mode 100644 index 000000000..56a02acac --- /dev/null +++ b/convex-core/src/test/java/convex/util/BigIntegerParamTest.java @@ -0,0 +1,46 @@ +package convex.util; + +import static org.junit.Assert.assertEquals; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import convex.core.data.AArrayBlob; +import convex.core.data.Blob; +import convex.core.util.Utils; + +@RunWith(Parameterized.class) +public class BigIntegerParamTest { + private BigInteger num; + + public BigIntegerParamTest(String label, BigInteger num) { + this.num = num; + } + + @Parameterized.Parameters(name = "{index}: {0}") + public static Collection dataExamples() { + return Arrays.asList(new Object[][] { { "Zero", BigInteger.ZERO }, + { "Short hex string CAFEBABE", Utils.hexToBigInt("CAFEBABE") }, + { "A big number", Utils.hexToBigInt( + "506bc1dc099358e5137292f4efdd57e400f29ba5132aa5d12b18dac1c1f6aaba645c0b7b58158babbfa6c6cd5a48aa7340a8749176b120e8516216787a13dc76") }, + { "Negative big number", Utils.hexToBigInt( + "506bc1dc099358e5137292f4efdd57e400f29ba5132aa5d12b18dac1c1f6aaba645c0b7b58158babbfa6c6cd5a48aa7340a8749176b120e8516216787a13dc76") + .negate() } }); + } + + @Test + public void testHexRoundTrip() { + if (num.signum() < 0) return; + String s = Utils.toHexString(num, (num.bitLength() / 4 + 2) & 0xFFFE); + AArrayBlob d = Blob.fromHex(s); + byte[] bs = d.getBytes(); + BigInteger b = new BigInteger(1, bs); + assertEquals(num, b); + } + +} diff --git a/convex-core/src/test/java/convex/util/BitsTest.java b/convex-core/src/test/java/convex/util/BitsTest.java new file mode 100644 index 000000000..5c1907b04 --- /dev/null +++ b/convex-core/src/test/java/convex/util/BitsTest.java @@ -0,0 +1,18 @@ +package convex.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import convex.core.util.Bits; + +public class BitsTest { + + @Test public void testLeadingZeros() { + assertEquals(16,Bits.leadingZeros(0x00FFFF)); + assertEquals(15,Bits.leadingZeros(0x010000)); + + assertEquals(18,Bits.leadingZeros(16383)); + assertEquals(17,Bits.leadingZeros(16384)); + } +} diff --git a/convex-core/src/test/java/convex/util/GenTestHuge.java b/convex-core/src/test/java/convex/util/GenTestHuge.java new file mode 100644 index 000000000..3be53524c --- /dev/null +++ b/convex-core/src/test/java/convex/util/GenTestHuge.java @@ -0,0 +1,64 @@ +package convex.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.runner.RunWith; + +import com.pholser.junit.quickcheck.Property; +import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; + +import convex.core.util.Huge; + +@RunWith(JUnitQuickcheck.class) +public class GenTestHuge { + + @Property + public void pairOps(Long a, Long b) { + Huge ha=Huge.create(a); + Huge hb=Huge.create(b); + + // multiplication + Huge p = Huge.multiply(a, b); + assertEquals(p.hi,Math.multiplyHigh(a, b)); + assertEquals(p.lo,a*b); + + Huge p0=Huge.multiply(a, 0); + assertEquals(Huge.ZERO,p0); + + Huge p1=Huge.multiply(a, 1); + assertEquals(ha,p1); + + // addition + Huge s=Huge.add(a,b); + assertEquals(s.lo,a+b); + assertEquals(s,ha.add(hb)); + + Huge s2=s.add(-b); + assertEquals(ha,s2); + + + } + + @Property + public void singleOps(Long a) { + Huge ha=Huge.create(a); + + Huge hneg=ha.negate(); + + assertEquals(ha,Huge.ZERO.add(a)); + assertEquals(ha,Huge.ZERO.add(ha)); + + assertEquals(ha,Huge.multiply(1L,a)); + assertEquals(Huge.ZERO,Huge.multiply(0L,a)); + + assertEquals(ha,hneg.negate()); + assertEquals(Huge.ZERO,ha.add(hneg)); + + // TODO: fix 128-bit mul + // Huge h2=ha.mul(ha); + // assertEquals(h2,hneg.mul(hneg)); + + } + + +} diff --git a/convex-core/src/test/java/convex/util/GenTestUMath.java b/convex-core/src/test/java/convex/util/GenTestUMath.java new file mode 100644 index 000000000..08655b1ae --- /dev/null +++ b/convex-core/src/test/java/convex/util/GenTestUMath.java @@ -0,0 +1,24 @@ +package convex.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.runner.RunWith; + +import com.pholser.junit.quickcheck.Property; +import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; + +import convex.core.util.UMath; + +@RunWith(JUnitQuickcheck.class) +public class GenTestUMath { + + + @Property + public void singleOps(Long a) { + long mulHigh=UMath.multiplyHigh(a, a); + + assertEquals(mulHigh,UMath.multiplyHigh(-a, -a)); + } + + +} diff --git a/convex-core/src/test/java/convex/util/HugeTest.java b/convex-core/src/test/java/convex/util/HugeTest.java new file mode 100644 index 000000000..cb86b2672 --- /dev/null +++ b/convex-core/src/test/java/convex/util/HugeTest.java @@ -0,0 +1,46 @@ +package convex.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import convex.core.util.Huge; +import convex.core.util.UMath; + +public class HugeTest { + + + @Test public void testMul() { + Huge h1=Huge.multiply(1, -1); + assertEquals(-1,h1.hi); + assertEquals(-1,h1.lo); + + Huge h2=Huge.multiply(0x100000000L, 0x100000000L); + assertEquals(1,h2.hi); + assertEquals(0,h2.lo); + } + + @Test public void testConstants() { + Huge zero=Huge.ZERO; + Huge one=Huge.ONE; + assertEquals(zero, zero.add(0)); + assertEquals(zero, one.sub(one)); + } + + @Test public void testAddRegression() { + long b=-212944119L; + Huge hb=Huge.create(b); + Huge s3=Huge.ZERO.add(b); + assertEquals(hb,s3); + } + + @Test public void testUnsignedCarry() { + assertEquals(0L,UMath.unsignedAddCarry(1L, 1L)); + assertEquals(1L,UMath.unsignedAddCarry(-1L, -1L)); + assertEquals(0L,UMath.unsignedAddCarry(-1L, 0)); + assertEquals(0L,UMath.unsignedAddCarry(0, -1L)); + assertEquals(1L,UMath.unsignedAddCarry(Long.MIN_VALUE, Long.MIN_VALUE)); + } + + +} diff --git a/convex-core/src/test/java/convex/util/TextTest.java b/convex-core/src/test/java/convex/util/TextTest.java new file mode 100644 index 000000000..17506a8a2 --- /dev/null +++ b/convex-core/src/test/java/convex/util/TextTest.java @@ -0,0 +1,28 @@ +package convex.util; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import convex.core.util.Text; + +public class TextTest { + + @Test + public void testWhiteSpace() { + checkWhiteSpace(0); + checkWhiteSpace(10); + checkWhiteSpace(32); + checkWhiteSpace(33); + checkWhiteSpace(95); + checkWhiteSpace(96); + checkWhiteSpace(97); + checkWhiteSpace(100); + } + + private void checkWhiteSpace(int len) { + String s = Text.whiteSpace(len); + assertEquals(len, s.length()); + assertEquals("", s.trim()); + } +} diff --git a/convex-core/src/test/java/convex/util/UMathTest.java b/convex-core/src/test/java/convex/util/UMathTest.java new file mode 100644 index 000000000..0a792b677 --- /dev/null +++ b/convex-core/src/test/java/convex/util/UMathTest.java @@ -0,0 +1,15 @@ +package convex.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import convex.core.util.UMath; + +public class UMathTest { + + @Test + public void testMultiplyHigh() { + assertEquals(1L,UMath.multiplyHigh(0x100000000L, 0x100000000L)); + } +} diff --git a/convex-core/src/test/java/convex/util/UtilsTest.java b/convex-core/src/test/java/convex/util/UtilsTest.java new file mode 100644 index 000000000..0a5bebf5a --- /dev/null +++ b/convex-core/src/test/java/convex/util/UtilsTest.java @@ -0,0 +1,375 @@ +package convex.util; + +import static convex.core.lang.TestState.STATE; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.math.BigInteger; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.Comparator; +import java.util.function.Function; + +import org.junit.Test; + +import convex.core.Block; +import convex.core.Peer; +import convex.core.State; +import convex.core.data.AVector; +import convex.core.data.Blob; +import convex.core.data.SignedData; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadSignatureException; +import convex.core.init.InitTest; +import convex.core.lang.TestState; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Invoke; +import convex.core.util.Bits; +import convex.core.util.Utils; + +public class UtilsTest { + + @Test + public void testToBigInteger() { + assertEquals(BigInteger.valueOf(255), Utils.toBigInteger(new byte[] { -1 })); + assertEquals(BigInteger.valueOf(256), Utils.toBigInteger(new byte[] { 1, 0 })); + assertEquals(BigInteger.valueOf(65536), Utils.toBigInteger(new byte[] { 1, 0, 0 })); + } + + @Test + public void testHexChar() { + assertEquals('f', Utils.toHexChar(15)); + assertEquals('a', Utils.toHexChar(10)); + assertEquals('9', Utils.toHexChar(9)); + assertEquals('0', Utils.toHexChar(0)); + } + + @Test(expected = IllegalArgumentException.class) + public void testBadHexCharNegative() { + Utils.toHexChar(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void testBadHexChar1() { + Utils.toHexChar(16); + } + + @Test + public void testHexString() { + assertEquals("ff", Utils.toHexString(new byte[] { -1 })); + assertEquals("81", Utils.toHexString(new byte[] { -127 })); + assertEquals("7f", Utils.toHexString(new byte[] { 127 })); + assertEquals("7c", Utils.toHexString(new byte[] { 124 })); + assertEquals("0012457c", Utils.toHexString(0x0012457c)); + } + + @Test + public void testHexToBytes() { + byte[] header = Utils.hexToBytes( + "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c"); + assertEquals(80, header.length); + assertEquals(1, header[0]); + assertEquals(124, header[79]); + assertEquals("7c", Utils.toHexString(header[79])); + assertEquals(0xdeadbeef, Utils.readInt(Utils.hexToBytes("deadbeef", 8), 0)); + + assertNull(Utils.hexToBytes("deadbeef", 10)); // wrong length + assertNull(Utils.hexToBytes("deadb", 5)); // odd length + assertNull(Utils.hexToBytes("zzzz", 4)); // invalid characters + } + + @Test + public void testHexVals() { + assertEquals(0, Utils.hexVal('0')); + assertEquals(15, Utils.hexVal('f')); + assertEquals(12, Utils.hexVal('C')); + } + + @Test + public void testExtractDigit() { + assertEquals(0, Utils.extractDigit(Blob.fromHex("0123"), 0)); + assertEquals(3, Utils.extractDigit(Blob.fromHex("0123"), 3)); + + assertThrows(IndexOutOfBoundsException.class, () -> Utils.extractDigit(Blob.fromHex("0123"), 4)); + assertThrows(IndexOutOfBoundsException.class, () -> Utils.extractDigit(Blob.fromHex("0123"), -1)); + } + + @Test + public void testBigIntegerToHex() { + assertEquals("0101", Utils.toHexString(BigInteger.valueOf(257), 4)); + assertEquals("0", Utils.toHexString(BigInteger.valueOf(0), 1)); + + assertThrows(IllegalArgumentException.class, () -> Utils.toHexString(BigInteger.valueOf(-100), 4)); + assertThrows(IllegalArgumentException.class, () -> Utils.toHexString(BigInteger.valueOf(257), 2)); + } + + @Test + public void testHexVal() { + for (int i = -128; i <= 127; i++) { + char c = (char) i; + try { + char rt = Utils.toHexChar(Utils.hexVal(c)); + assertEquals(Character.toLowerCase(c), rt); + } catch (IllegalArgumentException t) { + assert (!"0123456789abcdefABCDEF".contains(Character.toString(c))); + } + } + } + + @Test + public void testBitLength() { + assertEquals(1, Utils.bitLength(0)); // binary 0 + assertEquals(1, Utils.bitLength(-1)); // binary 1 + assertEquals(2, Utils.bitLength(1)); // binary 01 + assertEquals(2, Utils.bitLength(-2)); // binary 10 + assertEquals(3, Utils.bitLength(2)); // binary 010 + assertEquals(3, Utils.bitLength(-3)); // binary 101 + assertEquals(64, Utils.bitLength(Long.MAX_VALUE)); // max value + assertEquals(64, Utils.bitLength(Long.MAX_VALUE + 1)); // overflow + } + + @Test + public void testIntLeadingZeros() { + assertEquals(32, Bits.leadingZeros(0x00000000)); + assertEquals(16, Bits.leadingZeros(0x00008000)); + assertEquals(0, Bits.leadingZeros(-1)); + } + + @Test + public void testLongLeadingZeros() { + assertEquals(64, Bits.leadingZeros(0L)); + assertEquals(48, Bits.leadingZeros(0x00008000L)); + assertEquals(0, Bits.leadingZeros(-1L)); + } + + @Test + public void testWriteUInt256() { + BigInteger n = BigInteger.valueOf(7); + ByteBuffer b = ByteBuffer.allocate(32); + Utils.writeUInt256(b, n); + b.flip(); + assertEquals(32, b.remaining()); + byte[] bs = Utils.toByteArray(b); + assertEquals(32, bs.length); + assertEquals(0, b.remaining()); + assertEquals("0000000000000000000000000000000000000000000000000000000000000007", Utils.toHexString(bs)); + } + + @Test + public void testWriteBigUInt() { + byte[] ds = new byte[4]; + assertEquals("00000000", Utils.toHexString(ds)); + Utils.writeUInt(BigInteger.valueOf(7), ds, 0, 4); + assertEquals("00000007", Utils.toHexString(ds)); + assertEquals((short) 7, Utils.readShort(ds, 2)); // check short encoding + Utils.writeUInt(BigInteger.valueOf(0xffffffffl), ds, 0, 4); + assertEquals("ffffffff", Utils.toHexString(ds)); + + assertThrows(IllegalArgumentException.class, + () -> Utils.writeUInt(BigInteger.valueOf(0x100000000L), ds, 0, 32)); + assertThrows(IllegalArgumentException.class, () -> Utils.writeUInt(BigInteger.valueOf(-1), ds, 0, 32)); + } + + @Test + public void testToByteArray() { + ByteBuffer buf = ByteBuffer.allocate(1000); + byte[] bs1 = Utils.hexToBytes("cafebabe"); + buf.put(bs1); + + buf.flip(); + byte[] bs2 = Utils.toByteArray(buf); + assertArrayEquals(bs1, bs2); + assertEquals(0, buf.remaining()); + } + + @Test + public void testExtractBitsPositive() { + byte[] bs = Utils.hexToBytes("0FF107"); + assertEquals(0, Utils.extractBits(bs, 5, 23)); // 4 bits beyond end, should be zero-extended + assertEquals(0, Utils.extractBits(bs, 0, 23)); // zero length of bits + assertEquals(0xFF, Utils.extractBits(bs, 8, 12)); // the ff part + assertEquals(1, Utils.extractBits(bs, 1, 0)); // lowest bit + assertEquals(2, Utils.extractBits(bs, 2, 7)); // pick out the 1 in second bit + } + + @Test + public void testExtractBitsNegative() { + byte[] bs = Utils.hexToBytes("80F107"); + assertEquals(0x1F, Utils.extractBits(bs, 5, 23)); // 4 bits beyond end, should be sign-extended + assertEquals(0x0F, Utils.extractBits(bs, 8, 12)); // the 0f part + assertEquals(0, Utils.extractBits(bs, 0, 8)); // zero length of bits + assertEquals(1, Utils.extractBits(bs, 1, 0)); // lowest bit + assertEquals(2, Utils.extractBits(bs, 2, 7)); // pick out the 1 in second bit + assertEquals(7, Utils.extractBits(bs, 3, 70)); // pick 3 bits beyond array + assertEquals(-1, Utils.extractBits(bs, 32, 70)); // pick 32 bits beyond array + } + + @Test + public void testSetBitsPositive() { + byte[] bs = Utils.hexToBytes("0ff107"); + Utils.setBits(bs, 4, 0, 9); // set lowest hex digit to 9 + assertEquals("0ff109", Utils.toHexString(bs)); + Utils.setBits(bs, 6, 13, 0); // set 6 bits in middle of ff to zero + assertEquals("081109", Utils.toHexString(bs)); + Utils.setBits(bs, 8, 23, 255); // set 8 bits to one starting from highest bit + assertEquals("881109", Utils.toHexString(bs)); + Utils.setBits(bs, 8, 23, 0); // set 8 bits to zero starting from highest bit + assertEquals("081109", Utils.toHexString(bs)); + Utils.setBits(bs, 8, 24, 0xFF); // set 8 bits to one beyond end of array + assertEquals("081109", Utils.toHexString(bs)); + Utils.setBits(bs, 24, 0, 0xFFFFFF); // set all 24 bits to 1 + assertEquals("ffffff", Utils.toHexString(bs)); + } + + @Test + public void testReadWriteInt() { + byte[] bs = new byte[8]; + assertEquals(0, Utils.readInt(bs, 0)); + int a = 0xcafebabe; + Utils.writeInt(bs, 0, a); + assertEquals(a, Utils.readInt(bs, 0)); + Utils.writeInt(bs, 4, a); + assertEquals(0xbabecafe, Utils.readInt(bs, 2)); + assertEquals(0xcafebabe, Utils.readInt(bs, 4)); + } + + @Test + public void testReadWriteLong() { + byte[] bs = new byte[20]; + assertEquals(0, Utils.readLong(bs, 0)); + long a = 0xffffffffcafebabeL; + Utils.writeLong(bs, 0, a); + assertEquals(a, Utils.readLong(bs, 0)); + Utils.writeLong(bs, 4, a); + assertEquals(0xffffffffffffffffL, Utils.readLong(bs, 0)); + assertEquals(0xcafebabe00000000L, Utils.readLong(bs, 8)); + } + + @Test + public void testCheckedCasts() { + assertEquals(-1, Utils.checkedInt(-1)); + } + + @Test + public void testToInt() { + assertEquals(1, Utils.toInt(1)); + assertEquals(7, Utils.toInt(7.0f)); + assertEquals(8, Utils.toInt("8")); + assertEquals(-1, Utils.toInt("-1")); + } + + @Test + public void testInetSocketAddress() { + String s = "http://www.something-unusual.com:18888"; + InetSocketAddress sa = Utils.toInetSocketAddress(s); + assertNotNull(sa); + + assertNotNull(Utils.toInetSocketAddress("localhost:8080")); + + InetSocketAddress sa1=Utils.toInetSocketAddress("12.13.14.15:8080"); + assertNotNull(sa1); + assertNotNull(Utils.toInetSocketAddress("http:12.13.14.15:8080")); + + assertNull(Utils.toInetSocketAddress("@@@")); + + } + + @Test + public void testBinarySearchLeftmost() { + AVector L = Vectors.of( + CVMLong.create(1), + CVMLong.create(2), + CVMLong.create(2), + CVMLong.create(3) + ); + + // No match. + assertNull(Utils.binarySearchLeftmost(L, Function.identity(), Comparator.comparingLong(CVMLong::longValue), CVMLong.create(0))); + + // Exact match. + assertEquals( + CVMLong.create(2), + Utils.binarySearchLeftmost(L, Function.identity(), Comparator.comparingLong(CVMLong::longValue), CVMLong.create(2)) + ); + + assertEquals( + CVMLong.create(3), + Utils.binarySearchLeftmost(L, Function.identity(), Comparator.comparingLong(CVMLong::longValue), CVMLong.create(3)) + ); + + // Approximate match: 3 is the leftmost element. + assertEquals( + CVMLong.create(3), + Utils.binarySearchLeftmost(L, Function.identity(), Comparator.comparingLong(CVMLong::longValue), CVMLong.create(1000)) + ); + } + + @Test + public void testBinarySearchLeftmost2() { + AVector> L = Vectors.of( + Vectors.of(1, 1), + Vectors.of(1, 2), + Vectors.of(2, 1), + Vectors.of(2, 2), + Vectors.of(2, 3), + Vectors.of(3, 1) + ); + + assertEquals( + Vectors.of(2, 1), + Utils.binarySearchLeftmost(L, a -> a.get(0), Comparator.comparingLong(CVMLong::longValue), CVMLong.create(2)) + ); + + assertEquals( + Vectors.of(1, 1), + Utils.binarySearchLeftmost(L, a -> a.get(0), Comparator.comparingLong(CVMLong::longValue), CVMLong.create(1)) + ); + + } + + @Test + public void testBinarySearchLeftmost3() { + assertNull(Utils.binarySearchLeftmost(Vectors.empty(), Function.identity(), Comparator.comparingLong(CVMLong::longValue), CVMLong.create(2))); + } + + @Test + public void testStatesAsOfRange() throws BadSignatureException { + Peer peer = Peer.create(InitTest.FIRST_PEER_KEYPAIR, TestState.STATE); + + AVector states = Vectors.of(STATE); + + for (int i = 0; i < 10; i++) { + State state0 = states.get(states.count() - 1); + + long timestamp = state0.getTimeStamp().longValue() + 100; + + String command = "(def x " + timestamp + ")"; + SignedData data = peer.sign(Invoke.create(InitTest.HERO, timestamp, command)); + + Block block = Block.of(timestamp, InitTest.FIRST_PEER_KEY, data); + + State state1 = state0.applyBlock(block).getState(); + + states = states.conj(state1); + } + + AVector statesInRange = Utils.statesAsOfRange(states, STATE.getTimeStamp(), 1000, 2); + + assertEquals(2, statesInRange.count()); + + // First State in range must be the INITIAL value. + assertEquals(STATE, statesInRange.get(0)); + + // Since each iteration creates a snapshot of State advances by 100 milliseconds, + // the last State's timestamp in the range is the same as the initial timestamp + 1000 milliseconds. + assertEquals( + CVMLong.create(STATE.getTimeStamp().longValue() + 1000), + statesInRange.get(statesInRange.count() - 1).getTimeStamp() + ); + } + +} diff --git a/convex-core/src/test/java/etch/api/TestEtch.java b/convex-core/src/test/java/etch/api/TestEtch.java new file mode 100644 index 000000000..c5b1a91f9 --- /dev/null +++ b/convex-core/src/test/java/etch/api/TestEtch.java @@ -0,0 +1,73 @@ +package etch.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.Hash; +import convex.core.data.Ref; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import etch.Etch; +import etch.EtchStore; + +public class TestEtch { + private static final int ITERATIONS = 3; + + @Test + public void testTempStore() throws IOException { + EtchStore store=EtchStore.createTemp(); + Etch etch = store.getEtch(); + + AVector v=Vectors.of(1,2,3); + Hash h = v.getHash(); + Ref r=v.getRef(); + + assertNull(etch.read(h)); + + // write the Ref + Ref r2=etch.write(h, r); + + assertEquals(v.getEncoding(), etch.read(h).getValue().getEncoding()); + + assertEquals(h,r2.getHash()); + } + + @Test + public void testRandomWritesStore() throws IOException, BadFormatException { + EtchStore store=EtchStore.createTemp(); + Etch etch = store.getEtch(); + + int COUNT = 1000; + for (int i = 0; i < COUNT; i++) { + Long a = (long) i; + AVector v=Vectors.of(a); + Hash key = v.getHash(); + + etch.write(key, v.getRef()); + + Ref r2 = etch.read(key); + assertEquals(v,r2.getValue()); + assertNotNull(r2, "Stored value not found for vector value: " + v); + } + + for (int ii = 0; ii < ITERATIONS; ii++) { + for (int i = 0; i < COUNT; i++) { + Long a = (long) i; + AVector v=Vectors.of(a); + Hash key = v.getHash(); + Ref r2 = etch.read(key); + + assertNotNull(r2, "Stored value not found for vector value: " + v); + assertEquals(v, r2.getValue()); + } + } + } +} diff --git a/convex-core/src/test/resources/contracts/box/test1.con b/convex-core/src/test/resources/contracts/box/test1.con new file mode 100644 index 000000000..1bf16dcff --- /dev/null +++ b/convex-core/src/test/resources/contracts/box/test1.con @@ -0,0 +1,33 @@ +;; Assumed done in test setup already +;; (import convex.asset :as asset) +;; (import :as box) +;; (def box (get *aliases* 'box)) + +(def b1 (call box (create-box))) + +(assert (long? b1)) +(assert (= (asset/balance box *address*) #{b1})) +(assert (asset/owns? *address* [box #{b1}])) + +;; burn the box b1 +(call box (burn #{b1})) +(assert (not (asset/owns? *address* [box #{b1}]))) +(assert (= (asset/balance box *address*) #{})) + + +;; use a zombie actor +(def zombie (deploy `(do + (import convex.asset :as asset) + (def box ~box) + (set-controller *caller*) + (defn receive-asset ^{:callable? true} [a data] (asset/accept *caller* a))))) + +(def b2 (call box (create-box))) + +(assert (= #{b2} (asset/balance box *address*))) +(asset/transfer zombie [box #{b2}]) + +(assert (= #{b2} (asset/balance box zombie))) + +(eval-as zombie `(asset/transfer *caller* [box (asset/balance box *address*)])) +(assert (= #{b2} (asset/balance box *address*))) diff --git a/convex-core/src/test/resources/contracts/deposit-box.con b/convex-core/src/test/resources/contracts/deposit-box.con new file mode 100644 index 000000000..8d86a8a6d --- /dev/null +++ b/convex-core/src/test/resources/contracts/deposit-box.con @@ -0,0 +1,19 @@ +;; A mostly stateless smart contract that allows deposits from any account +;; and a full withdrawal by anyone. A public charity box. +;; +(fn [] + + ;; Deposit function accepts any offer. + ;; + (defn deposit + ^{:callable? true} + [] + (accept *offer*)) + + + ;; Withdraw function sends the caller the complete balance. + ;; + (defn withdraw + ^{:callable? true} + [] + (transfer *caller* *balance*))) diff --git a/convex-core/src/test/resources/contracts/exceptional.con b/convex-core/src/test/resources/contracts/exceptional.con new file mode 100644 index 000000000..c52bff489 --- /dev/null +++ b/convex-core/src/test/resources/contracts/exceptional.con @@ -0,0 +1,30 @@ +(do ;; Test contract for state changes and rollback. + + (def fragile :ok) + + (defn halt-fn + ^{:callable? true} + [x] + (halt x) + (return :foo) + :bar) + + (defn rollback-fn + ^{:callable? true} + [x] + (def fragile :broken) + (rollback x) + :bar) + + (defn break-fn + ^{:callable? true} + [x] + (def fragile :broken) + x) + + (defn get-fragile + ^{:callable? true} + [] + (return fragile)) + + ) diff --git a/convex-core/src/test/resources/contracts/funding.con b/convex-core/src/test/resources/contracts/funding.con new file mode 100644 index 000000000..ca1e1cfe2 --- /dev/null +++ b/convex-core/src/test/resources/contracts/funding.con @@ -0,0 +1,54 @@ +(do ;; Testing contract for fund transfers via offer / accept + + + ;; function that accepts quarter of all funds offered + (defn accept-quarter + ^{:callable? true} + [] + (accept (long (* 0.25 *offer*)))) + + ;; function that accepts half all funds offered + (defn accept-all + ^{:callable? true} + [] + (accept *offer*)) + + ;; function that accepts funds then rolls back + (defn accept-rollback + ^{:callable? true} + [] + (accept *offer*) + (rollback :foo)) + + ;; function that accepts funds repeatedly + ;; Note: *offer* should be reduced to zero by first accept. + (defn accept-repeat + ^{:callable? true} + [] + (accept *offer*) + (assert (== 0 *offer*)) + (accept *offer*) + (accept *offer*) + *offer*) + + ;; function that accepts nothing + (defn accept-zero + ^{:callable? true} + [] + (accept 0)) + + ;; function that accepts offer, and forwards to self + (defn accept-forward + ^{:callable? true} + [] + (let [amt *offer*] + (accept amt) + (call *address* amt (accept-all)))) + + ;; function that accepts nothing, but returns the offered value + (defn echo-offer + ^{:callable? true} + [] + *offer*) + + ) diff --git a/convex-core/src/test/resources/contracts/hello.con b/convex-core/src/test/resources/contracts/hello.con new file mode 100644 index 000000000..3a28f9c69 --- /dev/null +++ b/convex-core/src/test/resources/contracts/hello.con @@ -0,0 +1,16 @@ +;; The Hello World of smart contracts +(do + + (def people #{}) + + (defn greet + ^{:callable? true} + [name] + (if (people name) + (str "Welcome back " name) + (do + (def people (conj people name)) + (str "Hello " name) + ))) + + ) diff --git a/convex-core/src/test/resources/contracts/nft/simple-nft-test.con b/convex-core/src/test/resources/contracts/nft/simple-nft-test.con new file mode 100644 index 000000000..f75c92f35 --- /dev/null +++ b/convex-core/src/test/resources/contracts/nft/simple-nft-test.con @@ -0,0 +1,21 @@ +;; Assumed done in test setup already +;; (import convex.asset :as asset) +;; (def nft (import convex.simple-nft :as nft)) + +;; Testing with one account +(do + (def n1 (call nft (create))) + (assert (long? n1)) + (assert (contains-key? (asset/balance nft *address*) n1))) + +;; Testing quantities +(do + (assert (= #{} (asset/quantity-zero nft))) + (assert (= #{1 2 3 4} (asset/quantity-add nft #{1 2} #{3 4}))) + (assert (= #{1 2 3 4} (asset/quantity-add nft #{1 2 3} #{2 3 4}))) + (assert (= #{1 2} (asset/quantity-sub nft #{1 2 3} #{3 4 5}))) + + (assert (asset/quantity-contains? nft #{1 2 3} #{2 3})) + (assert (not (asset/quantity-contains? nft #{1 2 3} #{3 4}))) + (assert (not (asset/quantity-contains? nft #{1 2 3} #{4 5 6}))) + ) diff --git a/convex-core/src/test/resources/contracts/token.con b/convex-core/src/test/resources/contracts/token.con new file mode 100644 index 000000000..a480220ef --- /dev/null +++ b/convex-core/src/test/resources/contracts/token.con @@ -0,0 +1,42 @@ +;; Example token implementation +(fn [seed ;; a random number + supply ;; total token supply for smart contract + owner ;; initial owner of all the tokens + ] + (assert (address? owner) (long? supply) (> supply 0)) + `(do + ;; assets to set up the contract safely + + + ;; initial balances + (def balances {~owner ~supply}) + + ;;transfer function + (defn transfer + ^{:callable? true} + [target amount] + (assert (address? target) (long? amount) (<= 0 amount (balances *caller*))) + (if (not (= target *caller*)) + (let [srcbal (balances *caller*) + dstbal (balances target) + newbal (if dstbal (+ dstbal amount) amount)] + (def balances (assoc balances + *caller* (- srcbal amount) ;; new balance for caller + target newbal ;; new balance for target + ))))) + + ;; a function that should never be called, not exported + (defn bad-function [] (def balances {})) + + ;;return total supply + (defn total-supply + ^{:callable? true} + [] + ~supply) + + ;; get balance, or 0 if no balance for specified address + (defn balance + ^{:callable? true} + [acct] + (let [b (balances acct)] (if b b 0))) + )) diff --git a/convex-core/src/test/resources/examples/adventure.cvx b/convex-core/src/test/resources/examples/adventure.cvx new file mode 100644 index 000000000..4bb933239 --- /dev/null +++ b/convex-core/src/test/resources/examples/adventure.cvx @@ -0,0 +1,21 @@ +(do + + (defn runner [form] + (let [cmds (cond (list? form) + (let [n (count form)] + (cond (== n 0) [] + (= 'do ) (vec (next form)) + (vec form))) + [form])] + (cond + (empty? cmds) "You do nothing." + (= ['quit] cmds) (do (undef *lang*) (return "Exiting game... goodbye!"))) + (reduce (fn [s c] (str s " " c)) "You don't know how to:" cmds))) + + (defn start + ^{:doc {:description "Start the adventure!"}} + [] + (do + (def *lang* runner) "Welcome to the Adventure!")) + + ) \ No newline at end of file diff --git a/convex-core/src/test/resources/junit-platform.properties b/convex-core/src/test/resources/junit-platform.properties new file mode 100644 index 000000000..77ace3bc2 --- /dev/null +++ b/convex-core/src/test/resources/junit-platform.properties @@ -0,0 +1,4 @@ +junit.jupiter.execution.parallel.enabled=false +junit.jupiter.execution.parallel.mode.default = concurrent +junit.jupiter.execution.parallel.mode.classes.default = concurrent +junit.jupiter.execution.parallel.config.dynamic.factor = 2 diff --git a/convex-core/src/test/resources/testsource/min.con b/convex-core/src/test/resources/testsource/min.con new file mode 100644 index 000000000..b8dcc281c --- /dev/null +++ b/convex-core/src/test/resources/testsource/min.con @@ -0,0 +1,8 @@ +(defn min [& vals] + (let [fst (first vals) + n (count vals)] + (loop [min fst i 1] + (if (>= i n) + min + (let [v (nth vals i)] + (recur (if (< v min) v min) (inc i))))))) \ No newline at end of file diff --git a/convex-gui/.gitignore b/convex-gui/.gitignore new file mode 100644 index 000000000..e661051a1 --- /dev/null +++ b/convex-gui/.gitignore @@ -0,0 +1,5 @@ +/target/ +/.settings/ +/.classpath +/.project +/pom.xml.versionsBackup diff --git a/convex-gui/pom.xml b/convex-gui/pom.xml new file mode 100644 index 000000000..1adc33041 --- /dev/null +++ b/convex-gui/pom.xml @@ -0,0 +1,103 @@ + + + world.convex + convex + 0.7.0-rc3 + + 4.0.0 + + convex-gui + + Convex GUI + Convex desktop GUI and test applications + https://convex.world + + + + + maven-assembly-plugin + 3.3.0 + + ${project.directory} + + + convex.gui.manager.PeerGUI + + + + jar-with-dependencies + + + + + + create-archive + package + + single + + + + + + + + + + world.convex + convex-peer + ${convex.version} + + + + net.java.dev.jna + jna + 5.8.0 + + + + org.apache.commons + commons-text + 1.9 + + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + + + + io.github.vincenzopalazzo + material-ui-swing + 1.1.2 + + + ch.qos.logback + logback-classic + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + + + + diff --git a/convex-gui/src/main/assembly/full.xml b/convex-gui/src/main/assembly/full.xml new file mode 100644 index 000000000..e8be999d2 --- /dev/null +++ b/convex-gui/src/main/assembly/full.xml @@ -0,0 +1,34 @@ + + full + + jar + + + + ${project.basedir} + / + + README* + LICENSE* + NOTICE* + + + + ${project.build.directory} + / + + *.jar + + + + + + / + true + true + true + test + + + \ No newline at end of file diff --git a/convex-gui/src/main/assembly/testing.xml b/convex-gui/src/main/assembly/testing.xml new file mode 100644 index 000000000..cf7092fbb --- /dev/null +++ b/convex-gui/src/main/assembly/testing.xml @@ -0,0 +1,30 @@ + + testing + + jar + + false + + + / + true + true + true + test + + + + + + ${project.build.directory}/test-classes + / + + **/*.class + + true + + + \ No newline at end of file diff --git a/convex-gui/src/main/java/convex/gui/client/ConvexClient.java b/convex-gui/src/main/java/convex/gui/client/ConvexClient.java new file mode 100644 index 000000000..3dc04286e --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/client/ConvexClient.java @@ -0,0 +1,116 @@ +package convex.gui.client; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.EventQueue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; + +import convex.api.Convex; +import convex.core.State; +import convex.core.store.AStore; +import convex.core.store.Stores; +import convex.gui.client.panels.HomePanel; +import convex.gui.components.models.StateModel; +import convex.gui.manager.mainpanels.AboutPanel; +import convex.gui.utils.Toolkit; + +/** + * A Client application for the Convex Network. + * + * Doesn't run a Peer. Connects to convex.world. + */ +@SuppressWarnings("serial") +public class ConvexClient extends JPanel { + + private static final Logger log = LoggerFactory.getLogger(ConvexClient.class.getName()); + + public static final AStore CLIENT_STORE = Stores.getGlobalStore(); + + private static JFrame frame; + + private static StateModel latestState = StateModel.create(null); + + public static long maxBlock = 0; + + protected Convex convex=null; + + /** + * Launch the application. + * @param args Command line argument + */ + public static void main(String[] args) { + log.info("Running Convex Client"); + // call to set up Look and Feel + Toolkit.init(); + + EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + try { + ConvexClient.frame = new JFrame(); + frame.setTitle("Convex Client"); + frame.setIconImage(Toolkit.getImage(ConvexClient.class.getResource("/images/Convex.png"))); + frame.setBounds(100, 100, 1024, 768); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + ConvexClient window = new ConvexClient(); + frame.getContentPane().add(window, BorderLayout.CENTER); + frame.pack(); + frame.setVisible(true); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + /* + * Main component panel + */ + JPanel panel = new JPanel(); + + HomePanel homePanel = new HomePanel(); + AboutPanel aboutPanel = new AboutPanel(); + JTabbedPane tabs = new JTabbedPane(); + JPanel mainPanel = new JPanel(); + + /** + * Create the application. + */ + public ConvexClient() { + setLayout(new BorderLayout()); + this.add(tabs, BorderLayout.CENTER); + + tabs.add("Home", homePanel); + tabs.add("About", aboutPanel); + + } + + public void switchPanel(String title) { + int n = tabs.getTabCount(); + for (int i = 0; i < n; i++) { + if (tabs.getTitleAt(i).contentEquals(title)) { + tabs.setSelectedIndex(i); + return; + } + } + System.err.println("Missing tab: " + title); + } + + public static State getLatestState() { + return latestState.getValue(); + } + + public static Component getFrame() { + return frame; + } + + public static StateModel getStateModel() { + return latestState; + } +} diff --git a/convex-gui/src/main/java/convex/gui/client/panels/HomePanel.java b/convex-gui/src/main/java/convex/gui/client/panels/HomePanel.java new file mode 100644 index 000000000..9f72e8b86 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/client/panels/HomePanel.java @@ -0,0 +1,41 @@ +package convex.gui.client.panels; + +import javax.swing.JPanel; +import java.awt.BorderLayout; +import java.awt.Dimension; + +import javax.swing.JLabel; +import javax.swing.SwingConstants; + +import convex.gui.components.WorldPanel; + +import java.awt.Font; + +@SuppressWarnings("serial") +public class HomePanel extends JPanel { + + + /** + * Create the panel. + */ + public HomePanel() { + setPreferredSize(new Dimension(800,600)); + setLayout(new BorderLayout(0, 0)); + + JPanel panel = new JPanel(); + add(panel); + panel.setLayout(new BorderLayout(0, 0)); + + JLabel lblWelome = new JLabel("Welcome to Convex"); + lblWelome.setFont(new Font("Monospaced", Font.PLAIN, 18)); + lblWelome.setHorizontalAlignment(SwingConstants.CENTER); + panel.add(lblWelome, BorderLayout.NORTH); + + panel.add(new WorldPanel(), BorderLayout.CENTER); + + JLabel lblConn = new JLabel("Connecting.."); + panel.add(lblConn, BorderLayout.SOUTH); + + } + +} diff --git a/convex-gui/src/main/java/convex/gui/components/AccountChooserPanel.java b/convex-gui/src/main/java/convex/gui/components/AccountChooserPanel.java new file mode 100644 index 000000000..1caf0b6df --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/AccountChooserPanel.java @@ -0,0 +1,102 @@ +package convex.gui.components; + +import java.awt.FlowLayout; + +import javax.swing.ComboBoxModel; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.ListModel; + +import convex.core.State; +import convex.core.crypto.WalletEntry; +import convex.core.data.Address; +import convex.core.util.Text; +import convex.gui.manager.PeerGUI; +import convex.gui.manager.mainpanels.WalletPanel; + +/** + * Panel allowing the selection of account and query mode + */ +@SuppressWarnings("serial") +public class AccountChooserPanel extends JPanel { + + private JComboBox modeCombo; + public JComboBox addressCombo; + private JLabel lblNewLabel_1; + private JLabel lblNewLabel; + + private ComboBoxModel addressModel = createAddressList(WalletPanel.getListModel()); + private JLabel balanceLabel; + + public AccountChooserPanel() { + FlowLayout flowLayout = new FlowLayout(); + flowLayout.setAlignment(FlowLayout.LEFT); + setLayout(flowLayout); + + modeCombo = new JComboBox(); + modeCombo.setToolTipText("Use Transact to execute transactions (uses cash).\n\n" + + "Use Query to compute results without changing on-chain state (free)."); + modeCombo.addItem("Transact"); + modeCombo.addItem("Query"); + + lblNewLabel_1 = new JLabel("Mode:"); + add(lblNewLabel_1); + add(modeCombo); + + lblNewLabel = new JLabel("Account:"); + add(lblNewLabel); + + addressCombo = new JComboBox(); + addressCombo.setEditable(false); + add(addressCombo); + addressCombo.setModel(addressModel); + + balanceLabel = new JLabel("Balance: "); + add(balanceLabel); + + PeerGUI.getStateModel().addPropertyChangeListener(pc -> { + updateBalance((State) pc.getNewValue(), getSelectedAddress()); + }); + + addressCombo.addItemListener(e -> { + updateBalance(PeerGUI.getLatestState(), getSelectedAddress()); + }); + + updateBalance(PeerGUI.getLatestState(), getSelectedAddress()); + } + + public Address getSelectedAddress() { + WalletEntry we = (WalletEntry) addressModel.getSelectedItem(); + return (we == null) ? null : we.getAddress(); + } + + private ComboBoxModel createAddressList(ListModel m) { + int n = m.getSize(); + DefaultComboBoxModel cm = new DefaultComboBoxModel(); + for (int i = 0; i < n; i++) { + WalletEntry we = m.getElementAt(i); + cm.addElement(we); + } + cm.addElement(null); + return cm; + } + + private void updateBalance(State s, Address a) { + if ((s == null) || (a == null)) { + balanceLabel.setText("Balance: "); + } else { + Long amt= s.getBalance(a); + balanceLabel.setText("Balance: " + ((amt==null)?"Null":Text.toFriendlyBalance(amt))); + } + } + + public String getMode() { + return (String) modeCombo.getSelectedItem(); + } + + public WalletEntry getWalletEntry() { + return (WalletEntry) addressCombo.getSelectedItem(); + } +} diff --git a/convex-gui/src/main/java/convex/gui/components/ActionPanel.java b/convex-gui/src/main/java/convex/gui/components/ActionPanel.java new file mode 100644 index 000000000..84420afcb --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/ActionPanel.java @@ -0,0 +1,23 @@ +package convex.gui.components; + +import java.awt.FlowLayout; + +import javax.swing.JPanel; +import javax.swing.border.BevelBorder; + +/** + * A panel used for displaying a list of action buttons at the bottom of the + * screen. + */ +@SuppressWarnings("serial") +public class ActionPanel extends JPanel { + + public ActionPanel() { + super(); + + FlowLayout flowLayout = new FlowLayout(FlowLayout.LEFT); + setLayout(flowLayout); + + setBorder(new BevelBorder(BevelBorder.RAISED, null, null, null, null)); + } +} diff --git a/convex-gui/src/main/java/convex/gui/components/BaseListComponent.java b/convex-gui/src/main/java/convex/gui/components/BaseListComponent.java new file mode 100644 index 000000000..e653e3863 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/BaseListComponent.java @@ -0,0 +1,16 @@ +package convex.gui.components; + +import javax.swing.JPanel; +import javax.swing.border.BevelBorder; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; + +@SuppressWarnings("serial") +public class BaseListComponent extends JPanel { + + public BaseListComponent() { + setBorder(new CompoundBorder(new BevelBorder(BevelBorder.RAISED, null, null, null, null), + new EmptyBorder(2, 2, 2, 2))); + + } +} diff --git a/convex-gui/src/main/java/convex/gui/components/BlockViewComponent.java b/convex-gui/src/main/java/convex/gui/components/BlockViewComponent.java new file mode 100644 index 000000000..5b99d15c2 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/BlockViewComponent.java @@ -0,0 +1,82 @@ +package convex.gui.components; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; + +import javax.swing.JPanel; + +import convex.core.Block; +import convex.core.Order; +import convex.core.Peer; +import convex.core.State; +import convex.core.data.AVector; +import convex.core.data.Hash; +import convex.gui.components.models.StateModel; +import convex.gui.manager.PeerGUI; + +/** + * Panel presenting a summary graphic of the most recent blocks for a given + * PeerView + */ +@SuppressWarnings("serial") +public class BlockViewComponent extends JPanel { + + private PeerView peerView; + + public BlockViewComponent(PeerView peer) { + this.peerView = peer; + + setBackground(null); + setPreferredSize(new Dimension(1000, 10)); + + if (peer!=null) { + StateModel model=peer.getStateModel(); + model.addPropertyChangeListener(e -> { + repaint(); + }); + } + } + + @Override + public void paintComponent(Graphics g) { + g.setColor(Color.black); + int pw = getWidth(); + int ph = getHeight(); + g.fillRect(0, 0, pw, ph); + + Peer p = peerView.peerServer.getPeer(); + Order order = p.getPeerOrder(); + if (order==null) return; // no current peer order - maybe not a valid peer? + AVector blocks = order.getBlocks(); + int n = (int) blocks.count(); + + + int W = 10; + long tw = W * PeerGUI.maxBlock; + long offset = Math.max(0, tw - pw); + + for (int i = (int) (offset / W); i < n; i++) { + Color c = Color.orange; + if (i < order.getProposalPoint()) c = Color.yellow; + if (i < order.getConsensusPoint()) c = Color.green; + if (p.getConsensusPoint() != order.getConsensusPoint()) { + System.out.println("Strange consensus?"); + } + int x = (int) (W * i - offset); + g.setColor(c); + g.fillRect(x + 1, 1, W - 2, W - 2); + + if (c == Color.green) { + g.setColor(Color.black); + State s = p.getStates().get(i + 1); + for (int j = 0; j < 6; j++) { + Hash h = s.getHash(); + if (h.byteAt(j) < 0) { + g.fillRect(x + 2, 2 + j, 6, 1); + } + } + } + } + } +} diff --git a/convex-gui/src/main/java/convex/gui/components/CodeLabel.java b/convex-gui/src/main/java/convex/gui/components/CodeLabel.java new file mode 100644 index 000000000..d3d34a6ee --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/CodeLabel.java @@ -0,0 +1,16 @@ +package convex.gui.components; + +import javax.swing.JTextArea; + +import convex.gui.utils.Toolkit; + +@SuppressWarnings("serial") +public class CodeLabel extends JTextArea { + + public CodeLabel(String text) { + this.setText(text); + this.setBackground(null); + this.setEditable(false); + this.setFont(Toolkit.SMALL_MONO_FONT); + } +} diff --git a/convex-gui/src/main/java/convex/gui/components/DefaultReceiveAction.java b/convex-gui/src/main/java/convex/gui/components/DefaultReceiveAction.java new file mode 100644 index 000000000..6b3ef374e --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/DefaultReceiveAction.java @@ -0,0 +1,51 @@ +package convex.gui.components; + +import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.JComponent; + +import convex.core.Result; +import convex.core.lang.RT; +import convex.core.util.Utils; + +public class DefaultReceiveAction implements Consumer { + + public static final Logger log = LoggerFactory.getLogger(DefaultReceiveAction.class.getName()); + + private JComponent parent; + + public DefaultReceiveAction(JComponent parent) { + this.parent = parent; + } + + @Override + public void accept(Result t) { + if (t.isError()) { + handleError(RT.jvm(t.getID()),t.getErrorCode(),t.getValue()); + } else { + handleResult(t.getValue()); + } + } + + protected void handleResult(Object m) { + showResult(m); + } + + protected void handleError(long id, Object code, Object msg) { + showError(code,msg); + } + + private void showError(Object code, Object msg) { + String resultString = "Error executing transaction: " + code + " "+msg; + log.info(resultString); + Toast.display(parent, resultString, Toast.FAIL); + } + + private void showResult(Object v) { + String resultString = "Transaction executed successfully\n" + "Result: " + Utils.toString(v); + log.info(resultString); + Toast.display(parent, resultString, Toast.SUCCESS); + } +} diff --git a/convex-gui/src/main/java/convex/gui/components/DropdownMenu.java b/convex-gui/src/main/java/convex/gui/components/DropdownMenu.java new file mode 100644 index 000000000..bc8756ea5 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/DropdownMenu.java @@ -0,0 +1,30 @@ +package convex.gui.components; + +import javax.swing.JButton; +import javax.swing.JPopupMenu; + +import convex.gui.utils.Toolkit; + +/** + * A dropdown menu that can be used wherever an embedded menu is needed. + */ +@SuppressWarnings("serial") +public class DropdownMenu extends JButton { + + private JPopupMenu popupMenu; + + public DropdownMenu(JPopupMenu popupMenu) { + + super(); + this.popupMenu = popupMenu; + this.setBorder(null); + this.setIcon(Toolkit.COG); + this.addActionListener(e -> { + popupMenu.show(this, 0, this.getHeight()); + }); + } + + public JPopupMenu getMenu() { + return popupMenu; + } +} diff --git a/convex-gui/src/main/java/convex/gui/components/Identicon.java b/convex-gui/src/main/java/convex/gui/components/Identicon.java new file mode 100644 index 000000000..ecbea0502 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/Identicon.java @@ -0,0 +1,42 @@ +package convex.gui.components; + +import java.awt.image.BufferedImage; + +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.border.BevelBorder; + +import convex.core.data.ABlob; +import convex.core.util.Utils; +import convex.gui.utils.Toolkit; + +/** + * A simple identicon for visualising hash values. + */ +@SuppressWarnings("serial") +public class Identicon extends JLabel { + + public static BufferedImage createImage(ABlob data, int renderSize) { + int SIZE = 3; + byte[] bs = data.getBytes(); + + BufferedImage bi = new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_INT_RGB); + + for (int y = 0; y < SIZE; y++) { + for (int x = 0; x < SIZE; x++) { + int i = x + y * SIZE; + int bits = Utils.extractBits(bs, 9, 9 * i); // take 3 bits per channel + int rgb = ((bits & 0b111000000) << 15) + ((bits & 0b111000) << 10) + ((bits & 0b111) << 5); + bi.setRGB(x, y, rgb); + } + } + + return Toolkit.smoothResize(bi, renderSize, renderSize); + } + + public Identicon(ABlob a) { + super(new ImageIcon(Identicon.createImage(a, 36))); + + setBorder(new BevelBorder(BevelBorder.RAISED, null, null, null, null)); + } +} diff --git a/convex-gui/src/main/java/convex/gui/components/PeerComponent.java b/convex-gui/src/main/java/convex/gui/components/PeerComponent.java new file mode 100644 index 000000000..b1ca24527 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/PeerComponent.java @@ -0,0 +1,139 @@ +package convex.gui.components; + +import java.awt.BorderLayout; + +import javax.swing.JButton; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JTextArea; +import javax.swing.border.EmptyBorder; + +import convex.core.Peer; +import convex.core.data.ACell; +import convex.gui.components.models.StateModel; +import convex.gui.manager.PeerGUI; +import convex.gui.manager.windows.etch.EtchWindow; +import convex.gui.manager.windows.peer.PeerWindow; +import convex.gui.manager.windows.state.StateWindow; +import convex.gui.utils.Toolkit; +import convex.peer.Server; +import etch.EtchStore; + +@SuppressWarnings("serial") +public class PeerComponent extends BaseListComponent { + + public PeerView peer; + JTextArea description; + private PeerGUI manager; + + public void launchPeerWindow(PeerView peer) { + PeerWindow pw = new PeerWindow(manager, peer); + pw.launch(); + } + + public void launchEtchWindow(PeerView peer) { + EtchWindow ew = new EtchWindow(manager, peer); + ew.launch(); + } + + public void launchExploreWindow(PeerView peer) { + Server s = peer.peerServer; + ACell p = s.getPeer().getConsensusState(); + StateWindow pw = new StateWindow(manager, p); + pw.launch(); + } + + public PeerComponent(PeerGUI manager, PeerView value) { + this.manager = manager; + this.peer = value; + + setLayout(new BorderLayout(0, 0)); + + // setPreferredSize(new Dimension(1000, 90)); + + JButton button = new JButton(""); + button.setBorder(null); + add(button, BorderLayout.WEST); + button.setIcon(Toolkit.CONVEX); + button.addActionListener(e -> { + launchPeerWindow(this.peer); + }); + + JPanel panel = new JPanel(); + panel.setBorder(new EmptyBorder(5, 5, 5, 5)); + add(panel); + panel.setLayout(new BorderLayout(0, 0)); + + description = new JTextArea((peer == null) ? "No peer" : peer.toString()); + description.setFont(Toolkit.SMALL_MONO_FONT); + description.setEditable(false); + description.setBorder(null); + description.setBackground(null); + panel.add(description, BorderLayout.CENTER); + + // Setup popup menu for peer + JPopupMenu popupMenu = new JPopupMenu(); + if (peer.isLocal()) { + JMenuItem closeButton = new JMenuItem("Shutdown Peer"); + closeButton.addActionListener(e -> { + peer.close(); + }); + popupMenu.add(closeButton); + + JMenuItem exploreButton = new JMenuItem("Explore state"); + exploreButton.addActionListener(e -> { + launchExploreWindow(peer); + }); + popupMenu.add(exploreButton); + + if (peer.peerServer.getStore() instanceof EtchStore) { + JMenuItem storeButton = new JMenuItem("Explore Etch store"); + storeButton.addActionListener(e -> { + launchEtchWindow(peer); + }); + popupMenu.add(storeButton); + } + + + JMenuItem killConn = new JMenuItem("Kill Connections"); + killConn.addActionListener(e -> { + peer.peerServer.getConnectionManager().closeAllConnections(); + }); + popupMenu.add(killConn); + + } else { + JMenuItem closeButton = new JMenuItem("Close connection"); + closeButton.addActionListener(e -> { + peer.close(); + }); + popupMenu.add(closeButton); + } + + JMenuItem replButton = new JMenuItem("Launch REPL"); + replButton.addActionListener(e -> launchPeerWindow(this.peer)); + + popupMenu.add(replButton); + + JPanel blockView = new BlockViewComponent(peer); + add(blockView, BorderLayout.SOUTH); + + DropdownMenu dm = new DropdownMenu(popupMenu); + add(dm, BorderLayout.EAST); + + if (peer!=null) { + StateModel model=peer.peerModel; + if (model!=null) { + model.addPropertyChangeListener(e->{ + blockView.repaint(); + description.setText(peer.toString()); + }); + } + } + + PeerGUI.tickState.addPropertyChangeListener(e->{ + description.setText(peer.toString()); + }); + + } +} diff --git a/convex-gui/src/main/java/convex/gui/components/PeerView.java b/convex-gui/src/main/java/convex/gui/components/PeerView.java new file mode 100644 index 000000000..17f930173 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/PeerView.java @@ -0,0 +1,92 @@ +package convex.gui.components; + +import java.net.InetSocketAddress; + +import convex.api.Convex; +import convex.core.Peer; +import convex.core.State; +import convex.core.data.AccountKey; +import convex.core.data.PeerStatus; +import convex.core.util.Text; +import convex.gui.components.models.StateModel; +import convex.gui.manager.PeerGUI; +import convex.peer.ConnectionManager; +import convex.peer.Server; + +/** + * Class representing a lightweight view of a Peer. + * + * Peer may be either a local Server or remote. + */ +public class PeerView { + public Convex peerConnection = null; + public Server peerServer = null; + + public StateModel peerModel = new StateModel<>(null); + public StateModel stateModel = new StateModel<>(null); + + @Override + public String toString() { + StringBuilder sb=new StringBuilder(); + if (peerServer != null) { + State state=PeerGUI.getLatestState(); + AccountKey paddr=peerServer.getPeerKey(); + sb.append("0x"+paddr.toChecksumHex()+"\n"); + sb.append("Local peer on: " + peerServer.getHostAddress() + " with store "+peerServer.getStore()+"\n"); + + PeerStatus ps=state.getPeer(paddr); + if (ps!=null) { + sb.append("Peer Stake: "+Text.toFriendlyBalance(ps.getPeerStake())); + sb.append(" "); + sb.append("Delegated Stake: "+Text.toFriendlyBalance(ps.getDelegatedStake())); + } + ConnectionManager cm=peerServer.getConnectionManager(); + sb.append("\n"); + sb.append("Connections: "+cm.getConnectionCount()); + } else if (peerConnection != null) { + sb.append("Remote peer at: " + peerConnection.getRemoteAddress()+"\n"); + } else { + sb.append("Unknown"); + } + return sb.toString(); + } + + /** + * Poll the current peer state. Updates state models if necessary. + * + * Returns null if not a local peer. + * + * @return Peer state for this PeerView + */ + public Peer checkPeer() { + if (peerServer != null) { + Peer p = peerServer.getPeer(); + peerModel.setValue(p); + if (p!=null) stateModel.setValue(p.getConsensusState()); + return p; + } + return null; + } + + public void close() { + if (peerServer != null) peerServer.close(); + if (peerConnection != null) peerConnection.close(); + } + + public InetSocketAddress getHostAddress() { + // this is direct connection to a peer, so get its host address + if (peerServer != null) return peerServer.getHostAddress(); + + // need to get the remote address from the PeerConnection + return (InetSocketAddress) peerConnection.getRemoteAddress(); + } + + public boolean isLocal() { + return peerServer != null; + } + + public StateModel getStateModel() { + return stateModel; + } + +} \ No newline at end of file diff --git a/convex-gui/src/main/java/convex/gui/components/ScrollyList.java b/convex-gui/src/main/java/convex/gui/components/ScrollyList.java new file mode 100644 index 000000000..7187779ab --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/ScrollyList.java @@ -0,0 +1,96 @@ +package convex.gui.components; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.Rectangle; +import java.util.function.Function; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.ListModel; +import javax.swing.Scrollable; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; + +/** + * Component that represents a convenient Scrollable list of child components, + * based on a List model. + * + * @param Type of list model elements + */ +@SuppressWarnings("serial") +public class ScrollyList extends JScrollPane { + private final Function builder; + private final ListModel model; + private final ScrollablePanel listPanel = new ScrollablePanel(); + + private void refreshList() { + listPanel.removeAll(); + int n = model.getSize(); + for (int i = 0; i < n; i++) { + E we = model.getElementAt(i); + listPanel.add(builder.apply(we)); + } + this.revalidate(); + } + + private static class ScrollablePanel extends JPanel implements Scrollable { + + @Override + public Dimension getPreferredScrollableViewportSize() { + return new Dimension(800, 600); + } + + @Override + public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { + return 60; + } + + @Override + public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { + // TODO Auto-generated method stub + return 180; + } + + @Override + public boolean getScrollableTracksViewportWidth() { + return true; + } + + @Override + public boolean getScrollableTracksViewportHeight() { + return false; + } + } + + public ScrollyList(ListModel model, Function builder) { + super(); + this.builder = builder; + this.model = model; + this.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + + listPanel.setLayout(new GridLayout(0, 1)); + setViewportView(listPanel); + getViewport().setBackground(null); + + model.addListDataListener(new ListDataListener() { + @Override + public void intervalAdded(ListDataEvent e) { + refreshList(); + } + + @Override + public void intervalRemoved(ListDataEvent e) { + refreshList(); + } + + @Override + public void contentsChanged(ListDataEvent e) { + refreshList(); + } + }); + + refreshList(); + } +} diff --git a/convex-gui/src/main/java/convex/gui/components/Toast.java b/convex-gui/src/main/java/convex/gui/components/Toast.java new file mode 100644 index 000000000..da4a347f5 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/Toast.java @@ -0,0 +1,93 @@ +package convex.gui.components; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Point; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.geom.RoundRectangle2D; + +import javax.swing.JComponent; +import javax.swing.JTextArea; +import javax.swing.JWindow; + +/** + * A simple class for implementing a "Toast" style notification. + */ +@SuppressWarnings("serial") +public class Toast extends JWindow { + + public static final Color SUCCESS = new Color(100,150,0); + public static final Color FAIL = new Color(150,50,50); + public static final Color INFO = new Color(50,100,150); + + public Toast(JComponent parent, JComponent component, Color color) { + super(); + this.setLayout(new BorderLayout()); + this.setBackground(color); + this.getContentPane().setBackground(color); + add(component); + + Point pp=parent.getLocationOnScreen(); + + int px=(int) pp.getX(); + int py=(int) pp.getY(); + int pw=parent.getWidth(); + int ph=parent.getHeight(); + int h=50; + this.setLocation(px, py+ph-h); + setSize(pw,h); + + addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + setShape(new RoundRectangle2D.Float(0, 0,getWidth(), getHeight(), 16, 16)); + } + }); + } + + @Override + public void paint(Graphics g) { + super.paint(g); + } + + private void doDisplay(final long millis) { + setVisible(true); + long start=System.currentTimeMillis(); + new Thread(()->{ + try { + long time=start; + while (time<(start+millis)) { + Thread.sleep(100); + time=System.currentTimeMillis(); + // drop opacity after 50% of time has elapsed + double opac=Math.min(1.0,Math.max(0.0,(2*(1.0-(time-start)/(double)millis)))); + setOpacity((float)opac) ; + } + } catch (InterruptedException e) { + // set interrupted flag, clear toast and return + setOpacity(0.0f); + Thread.currentThread().interrupt(); + } finally { + setVisible(false); + } + }).start();; + } + + public static void display(JComponent parent, JComponent component, Color colour) { + Toast toast=new Toast(parent,component,colour); + toast.doDisplay(2000); + } + + public static void display(JComponent parent, JComponent component) { + display(parent,component,SUCCESS); + } + + public static void display(JComponent parent, String message, Color colour) { + JTextArea ta=new JTextArea(message); + ta.setBackground(null); + ta.setEditable(false); + display(parent,ta,colour); + } +} diff --git a/convex-gui/src/main/java/convex/gui/components/UnlockWalletDialog.java b/convex-gui/src/main/java/convex/gui/components/UnlockWalletDialog.java new file mode 100644 index 000000000..05371017f --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/UnlockWalletDialog.java @@ -0,0 +1,93 @@ +package convex.gui.components; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPasswordField; +import javax.swing.KeyStroke; + +import convex.gui.utils.Toolkit; + +@SuppressWarnings("serial") +public class UnlockWalletDialog extends JDialog { + private JPasswordField passwordField; + + private char[] passPhrase = null; + + public static UnlockWalletDialog show(WalletComponent parent) { + UnlockWalletDialog dialog = new UnlockWalletDialog(parent); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + return dialog; + } + + char[] getPassPhrase() { + return passPhrase; + } + + public UnlockWalletDialog(WalletComponent walletComponent) { + this.setIconImage(Toolkit.WARNING.getImage()); + setAlwaysOnTop(true); + + setModalityType(ModalityType.DOCUMENT_MODAL); + setTitle("Unlock Wallet"); + setModal(true); + + JPanel panel_2 = new JPanel(); + getContentPane().add(panel_2, BorderLayout.NORTH); + panel_2.setLayout(new BorderLayout(0, 0)); + + JPanel panel_1 = new JPanel(); + panel_2.add(panel_1, BorderLayout.SOUTH); + + JButton btnUnlock = new JButton("Unlock"); + panel_1.add(btnUnlock); + btnUnlock.addActionListener(e -> { + this.passPhrase = passwordField.getPassword(); + close(); + }); + JButton btnCancel = new JButton("Cancel"); + panel_1.add(btnCancel); + + JPanel panel = new JPanel(); + panel_2.add(panel); + + JLabel lblPassphrase = new JLabel("Passphrase: "); + panel.add(lblPassphrase); + + passwordField = new JPasswordField(); + passwordField.setFont(new Font("Monospaced", Font.BOLD, 13)); + passwordField.setColumns(20); + panel.add(passwordField); + btnCancel.addActionListener(e -> close()); + + Action closeAction = new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + close(); + } + }; + + getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), + "close"); + getRootPane().getActionMap().put("close", closeAction); + + pack(); // set dialog to correct size given contents + + } + + public void close() { + passwordField = null; + setVisible(false); + } + +} diff --git a/convex-gui/src/main/java/convex/gui/components/WalletComponent.java b/convex-gui/src/main/java/convex/gui/components/WalletComponent.java new file mode 100644 index 000000000..2c82c6655 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/WalletComponent.java @@ -0,0 +1,100 @@ +package convex.gui.components; + +import java.awt.BorderLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.Insets; + +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; + +import convex.core.State; +import convex.core.crypto.WalletEntry; +import convex.core.data.Address; +import convex.core.util.Text; +import convex.gui.manager.PeerGUI; +import convex.gui.utils.Toolkit; + +@SuppressWarnings("serial") +public class WalletComponent extends BaseListComponent { + Icon icon = Toolkit.LOCKED_ICON; + + JButton lockButton; + + WalletEntry walletEntry; + + JPanel buttons = new JPanel(); + + private Address address; + + public WalletComponent(WalletEntry initialWalletEntry) { + this.walletEntry = initialWalletEntry; + address = walletEntry.getAddress(); + + setLayout(new BorderLayout()); + + // lock button + lockButton = new JButton(""); + buttons.add(lockButton); + lockButton.setIcon(walletEntry.isLocked() ? Toolkit.LOCKED_ICON : Toolkit.UNLOCKED_ICON); + lockButton.addActionListener(e -> { + if (walletEntry.isLocked()) { + UnlockWalletDialog dialog = UnlockWalletDialog.show(this); + char[] passPhrase = dialog.getPassPhrase(); + try { + walletEntry = walletEntry.unlock(passPhrase); + icon = Toolkit.UNLOCKED_ICON; + } catch (Throwable e1) { + JOptionPane.showMessageDialog(this, "Unable to unlock wallet: " + e1.getMessage()); + } + } else { + try { + walletEntry = walletEntry.lock(); + } catch (IllegalStateException e1) { + // OK, must be already locked. + } + icon = Toolkit.LOCKED_ICON; + } + lockButton.setIcon(icon); + }); + + // panel of buttons on right + add(buttons, BorderLayout.EAST); + + // identicon + JLabel identicon = new Identicon(walletEntry.getAddress().getHash()); + GridBagConstraints gbc_btnNewButton = new GridBagConstraints(); + gbc_btnNewButton.insets = new Insets(0, 0, 5, 5); + gbc_btnNewButton.gridx = 0; + gbc_btnNewButton.gridy = 0; + JPanel idPanel = new JPanel(); + idPanel.setLayout(new GridBagLayout()); + idPanel.add(identicon); + add(idPanel, BorderLayout.WEST); + + // address field + JPanel cPanel = new JPanel(); + cPanel.setLayout(new GridLayout(0, 1)); + CodeLabel addressLabel = new CodeLabel(address.toString()); + addressLabel.setFont(Toolkit.MONO_FONT); + cPanel.add(addressLabel); + CodeLabel infoLabel = new CodeLabel(getInfoString()); + cPanel.add(infoLabel); + add(cPanel, BorderLayout.CENTER); + + PeerGUI.getStateModel().addPropertyChangeListener(e -> { + infoLabel.setText(getInfoString()); + }); + } + + private String getInfoString() { + State s = PeerGUI.getLatestState(); + Long bal=s.getBalance(address); + return "Balance: " + ((bal==null)?"Null":Text.toFriendlyBalance(s.getBalance(address))); + } + +} diff --git a/convex-gui/src/main/java/convex/gui/components/WorldPanel.java b/convex-gui/src/main/java/convex/gui/components/WorldPanel.java new file mode 100644 index 000000000..3902bccb4 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/WorldPanel.java @@ -0,0 +1,54 @@ +package convex.gui.components; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.geom.Point2D; +import java.awt.image.BufferedImage; +import java.io.IOException; + +import javax.imageio.ImageIO; +import javax.swing.JPanel; + +import convex.gui.utils.RobinsonProjection; + +@SuppressWarnings("serial") +public class WorldPanel extends JPanel { + BufferedImage image; + + public WorldPanel() { + try { + image = ImageIO.read(Thread.currentThread().getContextClassLoader().getResource("images/world.png")); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void paintComponent(Graphics g) { + if (image == null) return; + + int w = this.getWidth(); + int h = this.getHeight(); + int sw = image.getWidth(); + int sh = image.getHeight(); + int dw = Math.min(w, h * sw / sh); + int dh = Math.min(h, w * sh / sw); + + int y = (h - dh) / 2; + + g.drawImage(image, 0, y, dw, y + dh, 0, 0, sw, sh, null); + + paintDot(g, 51.5073219, -0.1276474, 0, y, dw, dh); // London + paintDot(g, -33.928992, 18.417396, 0, y, dw, dh); // Cape Town + paintDot(g, 35.6828387, 139.7594549, 0, y, dw, dh); // Tokyo + paintDot(g, 23.135305, -82.3589631, 0, y, dw, dh); // Havana + } + + private void paintDot(Graphics g, double latitude, double longitude, int x, int y, int dw, int dh) { + g.setColor(Color.RED); + Point2D pt = RobinsonProjection.getPoint(latitude, longitude); + int px = (int) (x + dw * pt.getX()); + int py = (int) (y + dh * pt.getY()); + g.fillOval(px, py, 5, 5); + } +} diff --git a/convex-gui/src/main/java/convex/gui/components/models/AccountsTableModel.java b/convex-gui/src/main/java/convex/gui/components/models/AccountsTableModel.java new file mode 100644 index 000000000..41a5a077a --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/models/AccountsTableModel.java @@ -0,0 +1,90 @@ +package convex.gui.components.models; + +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableModel; + +import convex.core.State; +import convex.core.data.ACell; +import convex.core.data.AMap; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.core.data.Keyword; +import convex.core.init.Init; +import convex.core.util.Utils; + +@SuppressWarnings("serial") +public class AccountsTableModel extends AbstractTableModel implements TableModel { + + private State state; + + public AccountsTableModel(State state) { + this.state = state; + } + + private static final String[] FIXED_COLS = new String[] { "Address", "Type", "Count", "Balance", "Name", "Env.Size", "Allowance" }; + + public String getColumnName(int col) { + if (col < FIXED_COLS.length) return FIXED_COLS[col]; + return "FOO"; + } + + @Override + public int getRowCount() { + return Utils.checkedInt(state.getAccounts().count()); + } + + @Override + public int getColumnCount() { + // TODO token columns? + return FIXED_COLS.length; + } + + @Override + public boolean isCellEditable(int row, int col) { + return false; + } + + @SuppressWarnings("unchecked") + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + Address address = Address.create(rowIndex); + AccountStatus as = getEntry(rowIndex); + switch (columnIndex) { + case 0: + return address.toString(); + case 1: + return as.isActor()?"Actor":"User"; + case 2: { + long seq=as.getSequence(); + return (seq>=0)?seq:""; + } + case 3: + return as.getBalance(); + case 4: { + ACell o = as.getHoldings().get(Init.REGISTRY_ADDRESS); + if (o == null) return ""; + if (!(o instanceof AMap)) return ""; + AMap a = (AMap) o; + return a.get(Keyword.create("name")); + } + case 5: + return as.getMemorySize(); + case 6: + return as.getMemory(); + default: + return ""; + } + } + + public void setState(State newState) { + if (state != newState) { + state = newState; + fireTableDataChanged(); + } + } + + public AccountStatus getEntry(long ix) { + return state.getAccounts().get(ix); + } + +} diff --git a/convex-gui/src/main/java/convex/gui/components/models/OracleTableModel.java b/convex-gui/src/main/java/convex/gui/components/models/OracleTableModel.java new file mode 100644 index 000000000..c0c92597e --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/models/OracleTableModel.java @@ -0,0 +1,109 @@ +package convex.gui.components.models; + +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableModel; + +import convex.core.State; +import convex.core.data.ACell; +import convex.core.data.AMap; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.core.data.Keyword; +import convex.core.data.MapEntry; +import convex.core.data.Symbol; +import convex.core.util.Utils; + +/** + * Model for the Oracle table + */ +@SuppressWarnings("serial") +public class OracleTableModel extends AbstractTableModel implements TableModel { + + private State state; + private Address oracle; + + Symbol LIST_S=Symbol.create("*list*"); + Symbol RESULTS_S=Symbol.create("*results*"); + + Keyword DESC_K=Keyword.create("desc"); + + public OracleTableModel(State state, Address oracle) { + this.state = state; + this.oracle=oracle; + } + + private static final String[] FIXED_COLS=new String[] { + "Key","Description", "Finalised?","Value" + }; + + public String getColumnName(int col) { + if (col list=as.getEnvironmentValue(LIST_S); + if (list==null) { + System.err.println("OracleTableModel missing oracle list? in "+oracle); + return 0; + } + return Utils.checkedInt(list.count()); + } + + @Override + public int getColumnCount() { + return FIXED_COLS.length; + } + + @Override + public boolean isCellEditable(int row, int col) { + return false; + } + + @SuppressWarnings("unchecked") + public AMap getList() { + AMap env=state.getAccount(oracle).getEnvironment(); + AMap list=(AMap) env.get(LIST_S); + return list; + } + + @SuppressWarnings("unchecked") + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + AMap env=state.getAccount(oracle).getEnvironment(); + AMap list=(AMap) env.get(LIST_S); + MapEntry me=list.entryAt(rowIndex); + ACell key=me.getKey(); + switch (columnIndex) { + case 0: return key.toString(); + case 1: { + AMap data=(AMap) me.getValue(); + return data.get(DESC_K); + } + case 2: { + boolean done=((AMap) env.get(RESULTS_S)).containsKey(key); + return done?"Yes":"No"; + } + case 3: { + AMap results=((AMap) env.get(RESULTS_S)); + MapEntry rme=results.getEntry(key); + return (rme==null)?"":rme.getValue(); + } + + default: return ""; + } + } + + public void setState(State newState) { + if (state!=newState) { + state=newState; + fireTableDataChanged(); + } + } +} diff --git a/convex-gui/src/main/java/convex/gui/components/models/StateModel.java b/convex-gui/src/main/java/convex/gui/components/models/StateModel.java new file mode 100644 index 000000000..5bb227167 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/components/models/StateModel.java @@ -0,0 +1,68 @@ +package convex.gui.components.models; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +// import java.util.logging.Logger; + +import javax.swing.SwingUtilities; + +/** + * Model for state values which may be observer / listened to. + * + * Fires a property changed event for the property "value" whenever it is + * updated. + * + * @param + */ +public class StateModel { + + // private static final Logger log = Logger.getLogger(StateModel.class.getName()); + + + private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this); + + T value; + + public StateModel(T value) { + this.value = value; + } + + public StateModel() { + this(null); + } + + public static StateModel create(T value) { + return new StateModel(value); + } + + public T getValue() { + return value; + } + + /** + * Sets the value for this state model, firing any relevant property change + * listeners. + * + * @param newValue + */ + public void setValue(T newValue) { + T oldValue = this.value; + this.value = newValue; + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + // log.info("State update reported"); + propertyChangeSupport.firePropertyChange(new PropertyChangeEvent(this, "value", oldValue, newValue)); + } + }); + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + propertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + propertyChangeSupport.removePropertyChangeListener(listener); + } +} diff --git a/convex-gui/src/main/java/convex/gui/etch/EtchExplorer.java b/convex-gui/src/main/java/convex/gui/etch/EtchExplorer.java new file mode 100644 index 000000000..9e9b4128c --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/etch/EtchExplorer.java @@ -0,0 +1,112 @@ +package convex.gui.etch; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.EventQueue; +import java.awt.Toolkit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; + +import convex.core.store.Stores; +import convex.gui.components.models.StateModel; +import convex.gui.etch.panels.DatabasePanel; +import etch.EtchStore; + +/** + * A Client application for the Convex Network + */ +@SuppressWarnings("serial") +public class EtchExplorer extends JPanel { + + public static final Logger log = LoggerFactory.getLogger(EtchExplorer.class.getName()); + + private static JFrame frame; + + public static long maxBlock = 0; + + /** + * Launch the application. + * @param args Command line args + */ + public static void main(String[] args) { + // call to set up Look and Feel + convex.gui.utils.Toolkit.init(); + + EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + try { + EtchExplorer.frame = new JFrame(); + frame.setTitle("Etch Explorer"); + frame.setIconImage(Toolkit.getDefaultToolkit() + .getImage(EtchExplorer.class.getResource("/images/Convex.png"))); + frame.setBounds(100, 100, 1024, 768); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + EtchExplorer window = new EtchExplorer(); + frame.getContentPane().add(window, BorderLayout.CENTER); + frame.pack(); + frame.setVisible(true); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + /* + * Main component panel + */ + JPanel panel = new JPanel(); + + private static StateModel etchState = StateModel.create((EtchStore)Stores.getGlobalStore()); + + DatabasePanel homePanel = new DatabasePanel(this); + JTabbedPane tabs = new JTabbedPane(); + JPanel mainPanel = new JPanel(); + + + /** + * Create the application. + */ + public EtchExplorer() { + + + setLayout(new BorderLayout()); + this.add(tabs, BorderLayout.CENTER); + + tabs.add("Database", homePanel); + } + + public void switchPanel(String title) { + int n = tabs.getTabCount(); + for (int i = 0; i < n; i++) { + if (tabs.getTitleAt(i).contentEquals(title)) { + tabs.setSelectedIndex(i); + return; + } + } + System.err.println("Missing tab: " + title); + } + + public static Component getFrame() { + return frame; + } + + public StateModel getEtchState() { + return etchState; + } + + public EtchStore getStore() { + return etchState.getValue(); + } + + public void setStore(EtchStore newEtch) { + EtchStore e=etchState.getValue(); + e.close(); + etchState.setValue(newEtch); + } +} diff --git a/convex-gui/src/main/java/convex/gui/etch/panels/DatabasePanel.java b/convex-gui/src/main/java/convex/gui/etch/panels/DatabasePanel.java new file mode 100644 index 000000000..91d799a6c --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/etch/panels/DatabasePanel.java @@ -0,0 +1,98 @@ +package convex.gui.etch.panels; + +import javax.swing.JPanel; +import java.awt.BorderLayout; +import java.awt.Dimension; + +import javax.swing.JLabel; +import javax.swing.SwingConstants; + +import java.awt.Font; +import java.awt.GridLayout; +import java.io.File; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; +import javax.swing.border.EtchedBorder; +import javax.swing.border.TitledBorder; + +import convex.gui.components.ActionPanel; +import convex.gui.etch.EtchExplorer; +import etch.EtchStore; + +import javax.swing.JButton; +import javax.swing.JFileChooser; + +@SuppressWarnings("serial") +public class DatabasePanel extends JPanel { + + public static final Logger log = LoggerFactory.getLogger(DatabasePanel.class.getName()); + + + /** + * Create the panel. + * @param explorer + */ + public DatabasePanel(EtchExplorer explorer) { + setPreferredSize(new Dimension(800,600)); + setLayout(new BorderLayout(0, 0)); + + JPanel panel = new JPanel(); + add(panel); + panel.setLayout(new GridLayout(0, 1, 0, 0)); + + JPanel filePanel = new JPanel(); + filePanel.setBorder(new TitledBorder(new EtchedBorder(EtchedBorder.LOWERED, new Color(255, 255, 255), new Color(160, 160, 160)), "File", TitledBorder.LEADING, TitledBorder.TOP, null, new Color(0, 0, 0))); + panel.add(filePanel); + filePanel.setLayout(new BorderLayout(0, 0)); + + JLabel lblSelectPrompt = new JLabel("Select an Etch Database to Explore"); + filePanel.add(lblSelectPrompt, BorderLayout.NORTH); + + JLabel lblDatabaseFile = new JLabel(); + filePanel.add(lblDatabaseFile); + lblDatabaseFile.setText(explorer.getStore().getFileName()); + explorer.getEtchState().addPropertyChangeListener(pc->{ + lblDatabaseFile.setText(((EtchStore)pc.getNewValue()).getFileName()); + }); + + + JPanel actionPanel = new ActionPanel(); + filePanel.add(actionPanel, BorderLayout.SOUTH); + + JButton btnOpen = new JButton("Open File..."); + actionPanel.add(btnOpen); + final JFileChooser fc = new JFileChooser(); + btnOpen.addActionListener(e->{ + if (e.getSource() == btnOpen) { + fc.setCurrentDirectory(explorer.getStore().getFile()); + int returnVal = fc.showOpenDialog(DatabasePanel.this); + + if (returnVal == JFileChooser.APPROVE_OPTION) { + File file = fc.getSelectedFile(); + log.info("Opening Etch Database: {}", file.getName()); + + if (file.exists()) { + try { + EtchStore newEtch=EtchStore.create(file); + explorer.setStore(newEtch); + } catch (IOException ex) { + log.error("Error opening Etch database: " + ex.getMessage()); + + } + } + } + } + }); + + + JLabel lblWelome = new JLabel("Welcome to Convex"); + lblWelome.setFont(new Font("Monospaced", Font.PLAIN, 18)); + lblWelome.setHorizontalAlignment(SwingConstants.CENTER); + panel.add(lblWelome); + + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/PeerGUI.java b/convex-gui/src/main/java/convex/gui/manager/PeerGUI.java new file mode 100644 index 000000000..ba4d3033c --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/PeerGUI.java @@ -0,0 +1,317 @@ +package convex.gui.manager; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.EventQueue; +import java.awt.Toolkit; +import java.awt.event.WindowEvent; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import convex.api.Convex; +import convex.core.Order; +import convex.core.Peer; +import convex.core.Result; +import convex.core.State; +import convex.core.crypto.AKeyPair; +import convex.core.crypto.WalletEntry; +import convex.core.data.ACell; +import convex.core.data.AccountKey; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.core.init.Init; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Invoke; +import convex.core.util.Utils; +import convex.gui.components.PeerView; +import convex.gui.components.models.StateModel; +import convex.gui.manager.mainpanels.AboutPanel; +import convex.gui.manager.mainpanels.AccountsPanel; +import convex.gui.manager.mainpanels.ActorsPanel; +import convex.gui.manager.mainpanels.HomePanel; +import convex.gui.manager.mainpanels.KeyGenPanel; +import convex.gui.manager.mainpanels.MessageFormatPanel; +import convex.gui.manager.mainpanels.PeersListPanel; +import convex.gui.manager.mainpanels.WalletPanel; +import convex.peer.Server; + +@SuppressWarnings("serial") +public class PeerGUI extends JPanel { + + private static final Logger log = LoggerFactory.getLogger(PeerGUI.class.getName()); + + private static JFrame frame; + + public static List KEYPAIRS=new ArrayList<>(); + + private static final int NUM_PEERS=8; + + static { + for (int i=0; i PEERKEYS=KEYPAIRS.stream().map(kp->kp.getAccountKey()).collect(Collectors.toList()); + + public static State genesisState=Init.createState(PEERKEYS); + private static StateModel latestState = StateModel.create(genesisState); + public static StateModel tickState = StateModel.create(0L); + + public static long maxBlock = 0; + + /** + * Launch the application. + * @param args Command line args + * @throws IOException + */ + public static void main(String[] args) throws IOException { + // TODO: Store config + // Stores.setGlobalStore(EtchStore.create(new File("peers-shared-db"))); + + // call to set up Look and Feel + convex.gui.utils.Toolkit.init(); + + EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + try { + PeerGUI.frame = new JFrame(); + frame.setTitle("Convex Peer Manager"); + frame.setIconImage(Toolkit.getDefaultToolkit() + .getImage(PeerGUI.class.getResource("/images/Convex.png"))); + frame.setBounds(100, 100, 1024, 768); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + PeerGUI window = new PeerGUI(); + frame.getContentPane().add(window, BorderLayout.CENTER); + frame.pack(); + frame.setVisible(true); + + frame.addWindowListener(new java.awt.event.WindowAdapter() { + public void windowClosing(WindowEvent winEvt) { + // shut down peers gracefully + window.peerPanel.closePeers(); + } + }); + + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + /* + * Main component panel + */ + JPanel panel = new JPanel(); + + HomePanel homePanel = new HomePanel(); + PeersListPanel peerPanel; + WalletPanel walletPanel = new WalletPanel(); + KeyGenPanel keyGenPanel = new KeyGenPanel(this); + MessageFormatPanel messagePanel = new MessageFormatPanel(this); + AboutPanel aboutPanel = new AboutPanel(); + JTabbedPane tabs = new JTabbedPane(); + JPanel mainPanel = new JPanel(); + JPanel accountsPanel = new AccountsPanel(this); + + /** + * Create the application. + */ + public PeerGUI() { + peerPanel= new PeersListPanel(this); + + setLayout(new BorderLayout()); + this.add(tabs, BorderLayout.CENTER); + + tabs.add("Home", homePanel); + tabs.add("Peers", peerPanel); + tabs.add("Wallet", getWalletPanel()); + tabs.add("Accounts", accountsPanel); + tabs.add("KeyGen", keyGenPanel); + tabs.add("Message", messagePanel); + tabs.add("Actors", new ActorsPanel(this)); + tabs.add("About", aboutPanel); + + // launch local peers for testing + EventQueue.invokeLater(() -> { + peerPanel.launchAllPeers(this); + }); + + updateThread.start(); + } + + private boolean updateRunning = true; + + private long cp = 0; + + private Thread updateThread = new Thread(new Runnable() { + @Override + public void run() { + while (updateRunning) { + try { + Thread.sleep(100); + tickState.setValue(tickState.getValue()+1); + + java.util.List peerViews = peerPanel.getPeerViews(); + peerPanel.repaint(); + State latest = latestState.getValue(); + for (PeerView s : peerViews) { + s.checkPeer(); + + Server serv=s.peerServer; + if (serv==null) continue; + + Peer p = serv.getPeer(); + if (p==null) continue; + + Order order=p.getPeerOrder(); + if (order==null) continue; // not an active peer? + maxBlock = Math.max(maxBlock, order.getBlockCount()); + + long pcp = p.getConsensusPoint(); + if (pcp > cp) { + cp = pcp; + //String ls="PeerGUI Consensus State update detected at depth "+cp; + //System.err.println(ls); + latest = p.getConsensusState(); + + } + } + latestState.setValue(latest); // trigger peer view repaints etc. + + } catch (InterruptedException e) { + // + log.warn("Update thread interrupted abnormally: "+e.getMessage()); + e.printStackTrace(); + Thread.currentThread().interrupt(); + } + } + log.info("Manager update thread ended"); + } + }, "GUI Manager state update thread"); + + + + @Override + public void finalize() { + // terminate the update thread + updateRunning = false; + } + + public void switchPanel(String title) { + int n = tabs.getTabCount(); + for (int i = 0; i < n; i++) { + if (tabs.getTitleAt(i).contentEquals(title)) { + tabs.setSelectedIndex(i); + return; + } + } + System.err.println("Missing tab: " + title); + } + + public WalletPanel getWalletPanel() { + return walletPanel; + } + + /** + * Builds a connection to the peer network + * @param address Address for connection + * @param kp Key Pair for connection + * @return Convex connection instance + * @throws IOException + * @throws TimeoutException + */ + public static Convex makeConnection(Address address,AKeyPair kp) throws IOException, TimeoutException { + InetSocketAddress host = getDefaultPeer().getHostAddress(); + return Convex.connect(host,address, kp); + } + + /** + * Executes a transaction using the given Wallet + * + * @param code Code to execute + * @param we Wallet to use + * @return Future for Result + */ + public static CompletableFuture execute(WalletEntry we, ACell code) { + Address address = we.getAddress(); + AccountStatus as = getLatestState().getAccount(address); + long sequence = as.getSequence() + 1; + ATransaction trans = Invoke.create(address,sequence, code); + return execute(we,trans); + } + + /** + * Executes a transaction using the given Wallet + * + * @param we Wallet to use + * @param trans Transaction to execute + * @return Future for Result + */ + public static CompletableFuture execute(WalletEntry we, ATransaction trans) { + try { + AKeyPair kp = we.getKeyPair(); + Convex convex = makeConnection(we.getAddress(),kp); + CompletableFuture fr= convex.transact(trans); + log.trace("Sent transaction: {}",trans); + return fr; + } catch (IOException | TimeoutException e) { + throw Utils.sneakyThrow(e); + } + } + + /** + * Executes a transaction using the given Wallet + * + * @param we Wallet to use + * @param trans Transaction to execute + * @param receiveAction + */ + public static void execute(WalletEntry we, ATransaction trans, Consumer receiveAction) { + execute(we,trans).thenAcceptAsync(receiveAction); + } + + public static State getLatestState() { + return latestState.getValue(); + } + + public static Component getFrame() { + return frame; + } + + public static StateModel getStateModel() { + return latestState; + } + + public static PeerView getDefaultPeer() { + return PeersListPanel.getFirst(); + } + + public static Address getUserAddress(int i) { + return Init.getGenesisPeerAddress(i); + } + + public static AKeyPair getUserKeyPair(int i) { + return KEYPAIRS.get(i); + } + + public static Address getGenesisAddress() { + return Init.getGenesisAddress(); + } +} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/AboutPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/AboutPanel.java new file mode 100644 index 000000000..69f65d8b8 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/mainpanels/AboutPanel.java @@ -0,0 +1,81 @@ +package convex.gui.manager.mainpanels; + +import java.awt.BorderLayout; + +import javax.swing.JButton; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextArea; + +import convex.core.State; +import convex.core.data.prim.CVMLong; +import convex.core.util.Text; +import convex.gui.components.ActionPanel; +import convex.gui.manager.PeerGUI; +import convex.gui.utils.Toolkit; + +@SuppressWarnings("serial") +public class AboutPanel extends JPanel { + + private final JTextArea textArea; + + public AboutPanel() { + setLayout(new BorderLayout(0, 0)); + + JPanel panel = new ActionPanel(); + add(panel, BorderLayout.SOUTH); + + JButton creditsButton = new JButton("Credits"); + panel.add(creditsButton); + + JPanel panel_1 = new JPanel(); + add(panel_1, BorderLayout.CENTER); + panel_1.setLayout(new BorderLayout(0, 0)); + + textArea = new JTextArea(); + textArea.setEditable(false); + textArea.setBackground(null); + textArea.setFont(Toolkit.SMALL_MONO_FONT); + + PeerGUI.getStateModel().addPropertyChangeListener(e -> { + updateState((State) e.getNewValue()); + }); + + panel_1.add(textArea); + creditsButton.addActionListener(e -> { + JOptionPane.showMessageDialog(null, + "Icons made by Freepik from www.flaticon.com\n" + "Royalty free map image by J. Bruce Jones", + "Credits", JOptionPane.PLAIN_MESSAGE); + }); + + updateState(PeerGUI.getLatestState()); + } + + private String lpad(Object s) { + return Text.leftPad(s.toString(), 25); + } + + private void updateState(State s) { + StringBuilder sb = new StringBuilder(); + CVMLong timestamp = s.getTimeStamp(); + + sb.append("Consensus state hash: " + s.getHash().toHexString() + "\n"); + sb.append("Timestamp: " + Text.dateFormat(timestamp.longValue()) + " (" + timestamp + ")\n"); + sb.append("\n"); + sb.append("Max Blocks: " + lpad(PeerGUI.maxBlock) + "\n"); + sb.append("\n"); + sb.append("Account statistics\n"); + sb.append(" # Accounts: " + lpad(s.getAccounts().count()) + "\n"); + sb.append(" # Peers: " + lpad(s.getPeers().count()) + "\n"); + sb.append("\n"); + sb.append("Globals\n"); + sb.append(" fees: " + lpad(Text.toFriendlyBalance(s.getGlobalFees().longValue())) + "\n"); + sb.append(" juice-price: " + lpad(Text.toFriendlyBalance(s.getJuicePrice().longValue())) + "\n"); + sb.append("\n"); + sb.append("Total funds: " + lpad(Text.toFriendlyBalance(s.computeTotalFunds())) + "\n"); + sb.append("Total stake: " + lpad(Text.toFriendlyBalance(s.computeStakes().get(null))) + "\n"); + + textArea.setText(sb.toString()); + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/AccountsPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/AccountsPanel.java new file mode 100644 index 000000000..ce92725e3 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/mainpanels/AccountsPanel.java @@ -0,0 +1,150 @@ +package convex.gui.manager.mainpanels; + +import java.awt.BorderLayout; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.table.DefaultTableCellRenderer; + +import convex.core.State; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.gui.components.ActionPanel; +import convex.gui.components.models.AccountsTableModel; +import convex.gui.manager.PeerGUI; +import convex.gui.manager.windows.actor.ActorWindow; +import convex.gui.utils.Toolkit; + +@SuppressWarnings("serial") +public class AccountsPanel extends JPanel { + AccountsTableModel tableModel = new AccountsTableModel(PeerGUI.getLatestState()); + JTable table = new JTable(tableModel); + + static class ActorRenderer extends DefaultTableCellRenderer { + public ActorRenderer() { + super(); + } + + public void setValue(Object value) { + setText(value.toString()); + } + } + + public AccountsPanel(PeerGUI manager) { + setLayout(new BorderLayout()); + + PeerGUI.getStateModel().addPropertyChangeListener(pc -> { + State newState = (State) pc.getNewValue(); + tableModel.setState(newState); + }); + + DefaultTableCellRenderer leftRenderer = new DefaultTableCellRenderer(); + leftRenderer.setHorizontalAlignment(JLabel.LEFT); + DefaultTableCellRenderer rightRenderer = new DefaultTableCellRenderer(); + rightRenderer.setHorizontalAlignment(JLabel.RIGHT); + + table.getColumnModel().getColumn(0).setCellRenderer(leftRenderer); + table.getColumnModel().getColumn(0).setPreferredWidth(150); + + ActorRenderer actorRenderer = new ActorRenderer(); + actorRenderer.setHorizontalAlignment(JLabel.CENTER); + table.getColumnModel().getColumn(1).setPreferredWidth(70); + table.getColumnModel().getColumn(1).setCellRenderer(actorRenderer); + + table.getColumnModel().getColumn(2).setCellRenderer(rightRenderer); + table.getColumnModel().getColumn(2).setPreferredWidth(70); + + table.getColumnModel().getColumn(3).setCellRenderer(rightRenderer); + table.getColumnModel().getColumn(3).setPreferredWidth(180); + + table.getColumnModel().getColumn(4).setPreferredWidth(200); + table.getColumnModel().getColumn(4).setCellRenderer(leftRenderer); + + table.getColumnModel().getColumn(5).setPreferredWidth(100); + table.getColumnModel().getColumn(5).setCellRenderer(rightRenderer); + + table.getColumnModel().getColumn(6).setPreferredWidth(100); + table.getColumnModel().getColumn(6).setCellRenderer(rightRenderer); + + // popup menu, not sure why this doesn't work.... + final JPopupMenu popupMenu = new JPopupMenu(); + JMenuItem copyItem = new JMenuItem("Copy Address"); + copyItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + copyAddress(); + } + }); + + table.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + int r = table.rowAtPoint(e.getPoint()); + if (r >= 0 && r < table.getRowCount()) { + table.setRowSelectionInterval(r, r); + } else { + table.clearSelection(); + } + + if (e.isPopupTrigger() || (e.getButton() & MouseEvent.BUTTON3) > 0) { + popupMenu.show(e.getComponent(), e.getX(), e.getY()); + } + } + }); + + JPanel actionPanel = new ActionPanel(); + add(actionPanel, BorderLayout.SOUTH); + + JButton btnCopy = new JButton("Copy Address"); + actionPanel.add(btnCopy); + btnCopy.addActionListener(e -> { + copyAddress(); + }); + + JButton btnActor = new JButton("Examine Actor..."); + actionPanel.add(btnActor); + btnActor.addActionListener(e -> { + long ix=table.getSelectedRow(); + AccountStatus as = tableModel.getEntry(ix); + if (as == null) return; + Address addr = Address.create(ix); + if (!as.isActor()) return; + ActorWindow pw = new ActorWindow(manager, addr); + pw.launch(); + }); + + // Turn off auto-resize, since we want a scrollable table + table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + + JScrollPane scrollPane = new JScrollPane(table); + scrollPane.getViewport().setBackground(null); + add(scrollPane, BorderLayout.CENTER); + + table.setFont(Toolkit.SMALL_MONO_FONT); + table.getTableHeader().setFont(Toolkit.SMALL_MONO_FONT); + ((DefaultTableCellRenderer) table.getTableHeader().getDefaultRenderer()).setHorizontalAlignment(JLabel.LEFT); + + } + + private void copyAddress() { + int row = table.getSelectedRow(); + if (row < 0) return; + + Address addr=Address.create(row); + Clipboard clipboard = java.awt.Toolkit.getDefaultToolkit().getSystemClipboard(); + StringSelection stringSelection = new StringSelection(addr.toHexString()); + clipboard.setContents(stringSelection, null); + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/ActorsPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/ActorsPanel.java new file mode 100644 index 000000000..ae3da68cc --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/mainpanels/ActorsPanel.java @@ -0,0 +1,38 @@ +package convex.gui.manager.mainpanels; + +import java.awt.BorderLayout; + +import javax.swing.JPanel; +import javax.swing.JTabbedPane; + +import convex.gui.manager.PeerGUI; +import convex.gui.manager.mainpanels.actors.DeployPanel; +import convex.gui.manager.mainpanels.actors.MarketsPanel; +import convex.gui.manager.mainpanels.actors.OraclePanel; + +/** + * Top level panel that displays some standard Actors + */ +@SuppressWarnings("serial") +public class ActorsPanel extends JPanel { + + private JTabbedPane typePane; + + public ActorsPanel(PeerGUI manager) { + setLayout(new BorderLayout(0, 0)); + + typePane = new JTabbedPane(); + + typePane.add("Oracle", new OraclePanel()); + + // TODO: fix registry address + // typePane.add("Registry", new ActorInvokePanel(manager, ...)); + + typePane.add("Prediction Markets", new MarketsPanel(manager)); + + typePane.add("Deploy", new DeployPanel()); + + add(typePane, BorderLayout.CENTER); + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/DeployPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/DeployPanel.java new file mode 100644 index 000000000..95bdd379c --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/mainpanels/DeployPanel.java @@ -0,0 +1,44 @@ +package convex.gui.manager.mainpanels; + +import java.awt.BorderLayout; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import convex.gui.components.AccountChooserPanel; +import convex.gui.components.ActionPanel; + +@SuppressWarnings("serial") +public class DeployPanel extends JPanel { + + + private AccountChooserPanel acctChooser; + + public DeployPanel() { + setLayout(new BorderLayout()); + + // =========================================== + // Top panel + acctChooser=new AccountChooserPanel(); + this.add(acctChooser, BorderLayout.NORTH); + + // =========================================== + // Centre panel + JPanel centrePanel=new JPanel(); + this.add(centrePanel,BorderLayout.CENTER); + + centrePanel.add(new JLabel("Tool for deploying standard Actors")); + + // ============================================ + // Action buttons + ActionPanel actionPanel=new ActionPanel(); + add(actionPanel, BorderLayout.SOUTH); + + JButton deployButton=new JButton("Deploy..."); + actionPanel.add(deployButton); + deployButton.addActionListener(e->{ + + }); + } +} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/HomePanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/HomePanel.java new file mode 100644 index 000000000..53a0dc69f --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/mainpanels/HomePanel.java @@ -0,0 +1,36 @@ +package convex.gui.manager.mainpanels; + +import javax.swing.JPanel; +import java.awt.BorderLayout; +import java.awt.Dimension; + +import javax.swing.JLabel; +import javax.swing.SwingConstants; + +import convex.gui.components.WorldPanel; + +import java.awt.Font; + +@SuppressWarnings("serial") +public class HomePanel extends JPanel { + + /** + * Create the panel. + */ + public HomePanel() { + setPreferredSize(new Dimension(800,600)); + setLayout(new BorderLayout(0, 0)); + + JPanel panel = new JPanel(); + add(panel); + panel.setLayout(new BorderLayout(0, 0)); + + JLabel lblWelome = new JLabel("Welome to Convex"); + lblWelome.setFont(new Font("Monospaced", Font.PLAIN, 18)); + lblWelome.setHorizontalAlignment(SwingConstants.CENTER); + panel.add(lblWelome, BorderLayout.NORTH); + + panel.add(new WorldPanel(), BorderLayout.CENTER); + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/KeyGenPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/KeyGenPanel.java new file mode 100644 index 000000000..b23cf6ea4 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/mainpanels/KeyGenPanel.java @@ -0,0 +1,221 @@ +package convex.gui.manager.mainpanels; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.border.EmptyBorder; + +import convex.core.crypto.AKeyPair; +import convex.core.crypto.Ed25519KeyPair; +import convex.core.crypto.Mnemonic; +import convex.core.crypto.WalletEntry; +import convex.core.data.Blob; +import convex.core.data.Hash; +import convex.core.util.Utils; +import convex.gui.components.ActionPanel; +import convex.gui.manager.PeerGUI; +import convex.gui.utils.Toolkit; + +@SuppressWarnings("serial") +public class KeyGenPanel extends JPanel { + + JTextArea mnemonicArea; + JTextArea privateKeyArea; + JTextArea publicKeyArea; + JTextArea addressArea; + + JButton addWalletButton = new JButton("Add to wallet"); + + private String hexKeyFormat(String pk) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < (pk.length() / 32); i++) { + if (i > 0) sb.append('\n'); + for (int j = 0; j < 4; j++) { + if (j > 0) sb.append(' '); + int ix = 8 * (j + (i * 4)); + sb.append(pk.substring(ix, ix + 8)); + } + } + return sb.toString(); + } + + private void updateMnemonic() { + String s = mnemonicArea.getText(); + try { + byte[] bs = Mnemonic.decode(s, 128); + Hash h = Blob.wrap(bs).getContentHash(); + AKeyPair kp = AKeyPair.create(h.getBytes()); + String privateKeyString = kp.getEncodedPrivateKey().toHexString(); + privateKeyArea.setText(hexKeyFormat(privateKeyString)); + } catch (Exception ex) { + String pks = ""; + if (s.isBlank()) pks = ""; + privateKeyArea.setText(pks); + } + updatePublicKeys(); + } + + private void updatePrivateKey() { + try { + mnemonicArea.setText(""); + updatePublicKeys(); + } catch (Exception ex) { + System.err.println(ex.getMessage()); + return; + } + } + + private void updatePublicKeys() { + String s = privateKeyArea.getText(); + try { + Blob b = Blob.fromHex(Utils.stripWhiteSpace(s)); + AKeyPair kp = Ed25519KeyPair.create(b.getBytes()); + // String pk=Utils.toHexString(kp.getPrivateKey(),64); + addressArea.setText(kp.getAccountKey().toChecksumHex()); + publicKeyArea.setText(hexKeyFormat(kp.getAccountKey().toChecksumHex())); + addWalletButton.setEnabled(true); + } catch (Exception ex) { + addressArea.setText(""); + publicKeyArea.setText(""); + addWalletButton.setEnabled(false); + + return; + } + } + + /** + * Create the panel. + * @param manager + */ + public KeyGenPanel(PeerGUI manager) { + setLayout(new BorderLayout(0, 0)); + + JPanel actionPanel = new ActionPanel(); + add(actionPanel, BorderLayout.SOUTH); + + JButton btnRecreate = new JButton("Generate"); + actionPanel.add(btnRecreate); + btnRecreate.addActionListener(e -> { + mnemonicArea.setText(Mnemonic.createSecureRandom()); + updateMnemonic(); + }); + + JButton btnNewButton = new JButton("Export..."); + actionPanel.add(btnNewButton); + + actionPanel.add(addWalletButton); + addWalletButton.addActionListener(e -> { + String pks = privateKeyArea.getText(); + pks = Utils.stripWhiteSpace(pks); + WalletEntry we = WalletEntry.create(null,AKeyPair.create(Utils.hexToBytes(pks))); + manager.getWalletPanel().addWalletEntry(we); + manager.switchPanel("Wallet"); + + }); + + JPanel formPanel = new JPanel(); + formPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); + add(formPanel, BorderLayout.NORTH); + GridBagLayout gbl_formPanel = new GridBagLayout(); + gbl_formPanel.columnWidths = new int[] { 156, 347, 0 }; + gbl_formPanel.rowHeights = new int[] { 22, 0, 0, 0, 0 }; + gbl_formPanel.columnWeights = new double[] { 1.0, 1.0, Double.MIN_VALUE }; + gbl_formPanel.rowWeights = new double[] { 0.0, 1.0, 1.0, 1.0, Double.MIN_VALUE }; + formPanel.setLayout(gbl_formPanel); + + JLabel lblNewLabel = new JLabel("Mnenomic Phrase"); + GridBagConstraints gbc_lblNewLabel = new GridBagConstraints(); + gbc_lblNewLabel.anchor = GridBagConstraints.WEST; + gbc_lblNewLabel.anchor = GridBagConstraints.WEST; + gbc_lblNewLabel.insets = new Insets(0, 0, 5, 5); + gbc_lblNewLabel.gridx = 0; + gbc_lblNewLabel.gridy = 0; + formPanel.add(lblNewLabel, gbc_lblNewLabel); + + mnemonicArea = new JTextArea(); + mnemonicArea.setWrapStyleWord(true); + mnemonicArea.setLineWrap(true); + mnemonicArea.setRows(3); + GridBagConstraints gbc_mnemonicArea = new GridBagConstraints(); + gbc_mnemonicArea.fill = GridBagConstraints.HORIZONTAL; + gbc_mnemonicArea.insets = new Insets(0, 0, 5, 0); + gbc_mnemonicArea.gridx = 1; + gbc_mnemonicArea.gridy = 0; + mnemonicArea.setColumns(32); + mnemonicArea.setFont(new Font("Monospaced", Font.BOLD, 13)); + formPanel.add(mnemonicArea, gbc_mnemonicArea); + mnemonicArea.getDocument().addDocumentListener(Toolkit.createDocumentListener(() -> { + if (!mnemonicArea.isFocusOwner()) return; + updateMnemonic(); + })); + + JLabel lblPrivateKey = new JLabel("Private key"); + GridBagConstraints gbc_lblPrivateKey = new GridBagConstraints(); + gbc_lblPrivateKey.anchor = GridBagConstraints.WEST; + gbc_lblPrivateKey.insets = new Insets(0, 0, 5, 5); + gbc_lblPrivateKey.gridx = 0; + gbc_lblPrivateKey.gridy = 1; + formPanel.add(lblPrivateKey, gbc_lblPrivateKey); + + privateKeyArea = new JTextArea(); + privateKeyArea.setFont(new Font("Monospaced", Font.BOLD, 13)); + GridBagConstraints gbc_privateKeyArea = new GridBagConstraints(); + gbc_privateKeyArea.insets = new Insets(0, 0, 5, 0); + gbc_privateKeyArea.fill = GridBagConstraints.HORIZONTAL; + gbc_privateKeyArea.gridx = 1; + gbc_privateKeyArea.gridy = 1; + formPanel.add(privateKeyArea, gbc_privateKeyArea); + privateKeyArea.setText("(mnemonic not ready)"); + privateKeyArea.getDocument().addDocumentListener(Toolkit.createDocumentListener(() -> { + if (!privateKeyArea.isFocusOwner()) return; + updatePrivateKey(); + })); + + JLabel lblPublicKey = new JLabel("Public Key"); + GridBagConstraints gbc_lblPublicKey = new GridBagConstraints(); + gbc_lblPublicKey.anchor = GridBagConstraints.WEST; + gbc_lblPublicKey.insets = new Insets(0, 0, 5, 5); + gbc_lblPublicKey.gridx = 0; + gbc_lblPublicKey.gridy = 2; + formPanel.add(lblPublicKey, gbc_lblPublicKey); + + publicKeyArea = new JTextArea(); + publicKeyArea.setEditable(false); + publicKeyArea.setRows(4); + publicKeyArea.setText("(private key not ready)"); + publicKeyArea.setFont(new Font("Monospaced", Font.BOLD, 13)); + GridBagConstraints gbc_publicKeyArea = new GridBagConstraints(); + gbc_publicKeyArea.insets = new Insets(0, 0, 5, 0); + gbc_publicKeyArea.fill = GridBagConstraints.HORIZONTAL; + gbc_publicKeyArea.gridx = 1; + gbc_publicKeyArea.gridy = 2; + formPanel.add(publicKeyArea, gbc_publicKeyArea); + + JLabel lblNewLabel_1 = new JLabel("Address"); + GridBagConstraints gbc_lblNewLabel_1 = new GridBagConstraints(); + gbc_lblNewLabel_1.anchor = GridBagConstraints.WEST; + gbc_lblNewLabel_1.insets = new Insets(0, 0, 0, 5); + gbc_lblNewLabel_1.gridx = 0; + gbc_lblNewLabel_1.gridy = 3; + formPanel.add(lblNewLabel_1, gbc_lblNewLabel_1); + + addressArea = new JTextArea(); + addressArea.setEditable(false); + addressArea.setFont(new Font("Monospaced", Font.BOLD, 13)); + addressArea.setColumns(40); + GridBagConstraints gbc_addressArea = new GridBagConstraints(); + gbc_addressArea.fill = GridBagConstraints.HORIZONTAL; + gbc_addressArea.gridx = 1; + gbc_addressArea.gridy = 3; + formPanel.add(addressArea, gbc_addressArea); + + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/MessageFormatPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/MessageFormatPanel.java new file mode 100644 index 000000000..9be83fffe --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/mainpanels/MessageFormatPanel.java @@ -0,0 +1,134 @@ +package convex.gui.manager.mainpanels; + +import java.awt.BorderLayout; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSplitPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; + +import convex.core.data.ACell; +import convex.core.data.Blob; +import convex.core.data.Format; +import convex.core.exceptions.ParseException; +import convex.core.lang.Reader; +import convex.core.util.Utils; +import convex.gui.components.ActionPanel; +import convex.gui.manager.PeerGUI; +import convex.gui.utils.Toolkit; + +@SuppressWarnings("serial") +public class MessageFormatPanel extends JPanel { + + final JTextArea dataArea; + final JTextArea messageArea; + private JPanel buttonPanel; + protected PeerGUI manager; + private JButton clearButton; + private JPanel upperPanel; + private JPanel instructionsPanel; + private JLabel lblNewLabel; + private JTextField hashLabel; + + private static String HASHLABEL = "Hash: "; + + public MessageFormatPanel(PeerGUI manager) { + this.manager = manager; + setLayout(new BorderLayout(0, 0)); + + JSplitPane splitPane = new JSplitPane(); + splitPane.setOneTouchExpandable(true); + splitPane.setResizeWeight(0.5); + splitPane.setOrientation(JSplitPane.VERTICAL_SPLIT); + add(splitPane, BorderLayout.CENTER); + + // Top panel component + upperPanel = new JPanel(); + upperPanel.setLayout(new BorderLayout(0, 0)); + dataArea = new JTextArea(); + dataArea.setToolTipText("Enter data objects here"); + upperPanel.add(dataArea, BorderLayout.CENTER); + dataArea.setFont(Toolkit.MONO_FONT); + dataArea.setLineWrap(true); + dataArea.getDocument().addDocumentListener(Toolkit.createDocumentListener(() -> updateData())); + + // Bottom panel component + JPanel lowerPanel = new JPanel(); + lowerPanel.setLayout(new BorderLayout(0, 0)); + + messageArea = new JTextArea(); + messageArea.setToolTipText("Enter binary hex representation here"); + messageArea.setFont(Toolkit.MONO_FONT); + lowerPanel.add(messageArea, BorderLayout.CENTER); + + splitPane.setRightComponent(lowerPanel); + + hashLabel = new JTextField(HASHLABEL); + hashLabel.setToolTipText("Hash code of the data object's serilaised representation = Data Object ID"); + hashLabel.setBorder(null); + hashLabel.setBackground(null); + hashLabel.setFont(Toolkit.MONO_FONT); + lowerPanel.add(hashLabel, BorderLayout.SOUTH); + messageArea.getDocument().addDocumentListener(Toolkit.createDocumentListener(() -> updateMessage())); + + splitPane.setLeftComponent(upperPanel); + + buttonPanel = new ActionPanel(); + add(buttonPanel, BorderLayout.SOUTH); + + clearButton = new JButton("Clear"); + clearButton.setToolTipText("Press to clear the input areas"); + buttonPanel.add(clearButton); + + instructionsPanel = new JPanel(); + add(instructionsPanel, BorderLayout.NORTH); + + lblNewLabel = new JLabel("Use this fine tool to convert data to formatted binary messages, and vice versa"); + instructionsPanel.add(lblNewLabel); + clearButton.addActionListener(e -> { + dataArea.setText(""); + messageArea.setText(""); + }); + } + + private void updateMessage() { + if (!messageArea.isFocusOwner()) return; // prevent mutual recursion + String data = ""; + String msg = messageArea.getText(); + try { + Blob b = Blob.fromHex(Utils.stripWhiteSpace(msg)); + Object o = Format.read(b); + data = Utils.print(o); + hashLabel.setText(HASHLABEL + b.getContentHash().toHexString()); + } catch (ParseException e) { + data = "Unable to interpret message: " + e.getMessage(); + hashLabel.setText(HASHLABEL + " "); + } catch (Exception e) { + data = e.getMessage(); + } + dataArea.setText(data); + } + + private void updateData() { + if (!dataArea.isFocusOwner()) return; // prevent mutual recursion + String msg = ""; + String data = dataArea.getText(); + hashLabel.setText(HASHLABEL + " "); + if (!data.isBlank()) try { + messageArea.setEnabled(false); + ACell o = Reader.read(data); + Blob b = Format.encodedBlob(o); + hashLabel.setText(HASHLABEL + b.getContentHash().toHexString()); + msg = b.toHexString(); + messageArea.setEnabled(true); + } catch (ParseException e) { + msg = e.getMessage(); + } catch (Exception e) { + msg = e.getMessage(); + } + messageArea.setText(msg); + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/PeersListPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/PeersListPanel.java new file mode 100644 index 000000000..3b704f10f --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/mainpanels/PeersListPanel.java @@ -0,0 +1,157 @@ +package convex.gui.manager.mainpanels; + +import java.awt.BorderLayout; +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; + +import convex.api.Convex; +import convex.core.crypto.AKeyPair; +import convex.core.data.Address; +import convex.core.data.Hash; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.store.Stores; +import convex.gui.components.ActionPanel; +import convex.gui.components.PeerComponent; +import convex.gui.components.PeerView; +import convex.gui.components.ScrollyList; +import convex.gui.manager.PeerGUI; +import convex.peer.API; +import convex.peer.Server; +import etch.EtchStore; + +@SuppressWarnings({ "serial", "unused" }) +public class PeersListPanel extends JPanel { + + JPanel peersPanel; + static DefaultListModel peerList = new DefaultListModel(); + + JPanel peerViewPanel; + JScrollPane scrollPane; + + private static final Logger log = LoggerFactory.getLogger(PeersListPanel.class.getName()); + + public void launchAllPeers(PeerGUI manager) { + try { + int N=PeerGUI.KEYPAIRS.size(); + List serverList = API.launchLocalPeers(PeerGUI.KEYPAIRS,PeerGUI.genesisState); + for (Server server: serverList) { + PeerView peer = new PeerView(); + peer.peerServer = server; + // InetSocketAddress sa = server.getHostAddress(); + addPeer(peer); + } + } catch (Exception e) { + if (e instanceof ClosedChannelException) { + // Ignore + } else { + throw(e); + } + + } + + } + + // TODO + public void launchPeer(PeerGUI manager) { + + AKeyPair kp=AKeyPair.generate(); + HashMap config=new HashMap<>(); + config.put(Keywords.KEYPAIR, kp); + config.put(Keywords.STATE, PeerGUI.genesisState); + Server server=API.launchPeer(config); + // server. + + PeerView peer = new PeerView(); + peer.peerServer = server; + addPeer(peer); + } + + public static PeerView getFirst() { + return peerList.elementAt(0); + } + + /** + * Gets a list of all locally operating Servers from the current peer list. + * + * @return List of local PeerView objects + */ + public List getPeerViews() { + ArrayList al = new ArrayList<>(); + int n = peerList.getSize(); + for (int i = 0; i < n; i++) { + PeerView p = peerList.getElementAt(i); + al.add(p); + } + return al; + } + + private void addPeer(PeerView peer) { + peerList.addElement(peer); + } + + /** + * Create the panel. + * @param manager + */ + public PeersListPanel(PeerGUI manager) { + setLayout(new BorderLayout(0, 0)); + + JPanel toolBar = new ActionPanel(); + add(toolBar, BorderLayout.SOUTH); + + JButton btnLaunch = new JButton("Launch!"); + toolBar.add(btnLaunch); + btnLaunch.addActionListener(e -> launchPeer(manager)); + + JButton btnConnect = new JButton("Connect..."); + toolBar.add(btnConnect); + btnConnect.addActionListener(e -> { + String input = JOptionPane.showInputDialog("Enter host address: ", ""); + if (input==null) return; // no result? + + String[] ss = input.split(":"); + String host = ss[0].trim(); + int port = (ss.length > 1) ? Integer.parseInt(ss[1].trim()) : 0; + InetSocketAddress hostAddress = new InetSocketAddress(host, port); + Convex pc; + try { + // TODO: we want to receive anything? + pc = Convex.connect(hostAddress, null,null); + PeerView pv = new PeerView(); + pv.peerConnection = pc; + addPeer(pv); + } catch (Throwable e1) { + JOptionPane.showMessageDialog(this, "Connect failed: " + e1.toString()); + } + + }); + + ScrollyList scrollyList = new ScrollyList(peerList, + peer -> new PeerComponent(manager, peer)); + add(scrollyList, BorderLayout.CENTER); + } + + public void closePeers() { + int n = peerList.getSize(); + for (int i = 0; i < n; i++) { + PeerView p = peerList.getElementAt(i); + p.peerServer.close(); + } + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/WalletPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/WalletPanel.java new file mode 100644 index 000000000..ac092f7ed --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/mainpanels/WalletPanel.java @@ -0,0 +1,60 @@ +package convex.gui.manager.mainpanels; + +import java.awt.BorderLayout; + +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.ListModel; + +import convex.core.crypto.AKeyPair; +import convex.core.crypto.WalletEntry; +import convex.gui.components.ActionPanel; +import convex.gui.components.ScrollyList; +import convex.gui.components.WalletComponent; +import convex.gui.manager.PeerGUI; + +@SuppressWarnings("serial") +public class WalletPanel extends JPanel { + + public static WalletEntry HERO; + + private static DefaultListModel listModel = new DefaultListModel<>();; + ScrollyList walletList; + + public void addWalletEntry(WalletEntry we) { + listModel.addElement(we); + } + + /** + * Create the panel. + */ + public WalletPanel() { + setLayout(new BorderLayout(0, 0)); + + JPanel toolBar = new ActionPanel(); + add(toolBar, BorderLayout.SOUTH); + + // new wallet button + JButton btnNew = new JButton("New"); + toolBar.add(btnNew); + btnNew.addActionListener(e -> { + listModel.addElement(WalletEntry.create(null,AKeyPair.generate())); + }); + + // inital list + HERO = WalletEntry.create(PeerGUI.getUserAddress(0), PeerGUI.getUserKeyPair(0)); + addWalletEntry(HERO); + //addWalletEntry(WalletEntry.create(PeerGUI.getUserAddress(1), PeerGUI.getUserKeyPair(1))); + //addWalletEntry(WalletEntry.create(PeerGUI.getUserAddress(2),PeerGUI.getUserKeyPair(2))); + + // create and add ScrollyList + walletList = new ScrollyList(listModel, we -> new WalletComponent(we)); + add(walletList, BorderLayout.CENTER); + } + + public static ListModel getListModel() { + return listModel; + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/DeployPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/DeployPanel.java new file mode 100644 index 000000000..17dadb3a4 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/DeployPanel.java @@ -0,0 +1,43 @@ +package convex.gui.manager.mainpanels.actors; + +import java.awt.BorderLayout; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import convex.gui.components.AccountChooserPanel; +import convex.gui.components.ActionPanel; + +@SuppressWarnings("serial") +public class DeployPanel extends JPanel { + + private AccountChooserPanel acctChooser; + + public DeployPanel() { + setLayout(new BorderLayout()); + + // =========================================== + // Top panel + acctChooser = new AccountChooserPanel(); + this.add(acctChooser, BorderLayout.NORTH); + + // =========================================== + // Centre panel + JPanel centrePanel = new JPanel(); + this.add(centrePanel, BorderLayout.CENTER); + + centrePanel.add(new JLabel("Tool for deploying standard Actors")); + + // ============================================ + // Action buttons + ActionPanel actionPanel = new ActionPanel(); + add(actionPanel, BorderLayout.SOUTH); + + JButton deployButton = new JButton("Deploy..."); + actionPanel.add(deployButton); + deployButton.addActionListener(e -> { + + }); + } +} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/MarketComponent.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/MarketComponent.java new file mode 100644 index 000000000..6672c191b --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/MarketComponent.java @@ -0,0 +1,214 @@ +package convex.gui.manager.mainpanels.actors; + +import java.awt.BorderLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.text.DecimalFormat; +import java.util.HashMap; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; + +import convex.core.State; +import convex.core.crypto.WalletEntry; +import convex.core.data.ACell; +import convex.core.data.AList; +import convex.core.data.AMap; +import convex.core.data.AVector; +import convex.core.data.Address; +import convex.core.data.Keyword; +import convex.core.data.List; +import convex.core.data.Lists; +import convex.core.data.Symbol; +import convex.core.data.prim.CVMLong; +import convex.core.lang.Context; +import convex.core.lang.RT; +import convex.core.lang.Symbols; +import convex.gui.components.BaseListComponent; +import convex.gui.components.CodeLabel; +import convex.gui.components.DefaultReceiveAction; +import convex.gui.manager.PeerGUI; +import convex.gui.utils.Toolkit; + +@SuppressWarnings("serial") +public class MarketComponent extends BaseListComponent { + + private Address address; + AVector outcomes; + + HashMap probLabels = new HashMap<>(); // probabilities + HashMap tsLabels = new HashMap<>(); // total stake + HashMap osLabels = new HashMap<>(); // owned stake + + CodeLabel title; + private int numOutcomes; + private MarketsPanel marketsPanel; + + @SuppressWarnings("unchecked") + public MarketComponent(MarketsPanel marketsPanel, Address addr) { + this.marketsPanel = marketsPanel; + this.address = addr; + State state = PeerGUI.getLatestState(); + + // prediction market data + AMap pmEnv = state.getEnvironment(addr); + outcomes = RT.keys(pmEnv.get(Symbol.create("totals"))); + + numOutcomes = outcomes.size(); + + // oracle data + Address oracleAddress = (Address) pmEnv.get(Symbol.create("oracle")); + if (oracleAddress == null) throw new Error("No oracle symbol in environment?"); + Object key = pmEnv.get(Symbol.create("oracle-key")); + + AMap oracleEnv = state.getEnvironment(oracleAddress); + AMap fullList = (AMap) oracleEnv.get(Symbol.create("full-list")); + AMap oracleData = (AMap) fullList.get(key); + if (oracleData == null) throw new Error("No oracle data for key?"); + + // Layout + setLayout(new BorderLayout()); + + // Top label + String oName = RT.jvm( oracleData.get(Keyword.create("desc"))); + if (oName == null) oName = "Nameless Oracle"; + title = new CodeLabel(oName); + title.setFont(Toolkit.MONO_FONT); + add(title, BorderLayout.NORTH); + + // Centre panel + JPanel jp = new JPanel(); + add(jp, BorderLayout.CENTER); + + jp.setLayout(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.ipadx = 10; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + + gbc.gridx = 0; + jp.add(new JLabel("Outcome")); + gbc.gridx = 1; + jp.add(new JLabel("Probability")); + gbc.gridx = 2; + jp.add(new JLabel("Total Stake")); + gbc.gridx = 3; + jp.add(new JLabel("Owned Stake")); + gbc.gridx = 4; + jp.add(new JLabel("Actions")); + for (int i = 0; i < numOutcomes; i++) { + Object outcome = outcomes.get(i); + + gbc.gridy = 1 + i; + gbc.gridx = 0; + JLabel oLabel = new JLabel(outcome.toString() + " "); + oLabel.setHorizontalAlignment(SwingConstants.LEFT); + jp.add(oLabel, gbc); + + gbc.gridx = 1; + JLabel pLabel = new JLabel("??"); + jp.add(pLabel, gbc); + probLabels.put(outcome, pLabel); + + gbc.gridx = 2; + JLabel tsLabel = new JLabel("0"); + jp.add(tsLabel, gbc); + tsLabels.put(outcome, tsLabel); + + gbc.gridx = 3; + JLabel osLabel = new JLabel("0"); + jp.add(osLabel, gbc); + osLabels.put(outcome, osLabel); + + gbc.gridx = 4; + JPanel bp = new JPanel(); + bp.setLayout(new GridLayout(0, 2)); + JButton buyButton = new JButton("Buy"); + buyButton.addActionListener(e -> buy(outcome)); + bp.add(buyButton); + JButton sellButton = new JButton("Sell"); + sellButton.addActionListener(e -> sell(outcome)); + bp.add(sellButton); + jp.add(bp, gbc); + } + + // Top label + add(new CodeLabel("Market Address: " + address.toString()), BorderLayout.SOUTH); + + // state updates + updateStatus(state); + + PeerGUI.getStateModel().addPropertyChangeListener(e -> { + State s = (State) e.getNewValue(); + updateStatus(s); + }); + + marketsPanel.acctChooser.addressCombo.addActionListener(e -> { + State s = PeerGUI.getLatestState(); + updateStatus(s); + }); + } + + private void sell(Object outcome) { + changeStake(outcome, -1000000); + } + + private void buy(Object outcome) { + changeStake(outcome, 1000000); + } + + private void changeStake(Object outcome, long delta) { + State state = PeerGUI.getLatestState(); + Long stk = getStake(state, outcome); + if (stk == null) stk = 0L; + long newStake = Math.max(0L, stk + delta); + long offer = Math.max(0, delta); // covers stake increase for sure? + + WalletEntry we = marketsPanel.acctChooser.getWalletEntry(); + AList cc = Lists.of(Symbol.create("stake"), outcome, newStake); + AList cmd = List.of(Symbols.CALL, address, offer, cc); + PeerGUI.execute(we, cmd).thenAcceptAsync(new DefaultReceiveAction(marketsPanel)); + } + + static DecimalFormat probFormatter = new DecimalFormat("0.0"); + + private void updateStatus(State state) { + try { + Address caller = marketsPanel.acctChooser.getSelectedAddress(); + Context ctx = Context.createFake(state, caller); // fake for caller + for (int i = 0; i < numOutcomes; i++) { + ACell outcome = outcomes.get(i); + + double p = RT.jvm(ctx.actorCall(address, 0, "price", outcome).getResult()); + if (Double.isNaN(p)) p = 1.0 / numOutcomes; + String prob = probFormatter.format(p * 100.0) + "%"; + probLabels.get(outcome).setText(prob); + + Long ts = RT.jvm( ctx.actorCall(address, 0, "totals", outcome).getResult()); + String totalStake = ts.toString(); + tsLabels.get(outcome).setText(totalStake); + + @SuppressWarnings("unchecked") + AMap stks = (AMap) ctx.actorCall(address, 0, "stakes", outcome) + .getResult(); + CVMLong stk = stks.get(caller); + String ownStake = (stk == null) ? "0" : stk.toString(); + osLabels.get(outcome).setText(ownStake); + } + } catch (Throwable t) { + t.printStackTrace(); + } + } + + private Long getStake(State state, Object outcome) { + Address caller = marketsPanel.acctChooser.getSelectedAddress(); + Context ctx = Context.createFake(state, caller); + @SuppressWarnings("unchecked") + AMap stks = (AMap) ctx.actorCall(address, 0, "stakes", RT.cvm(outcome)).getResult(); + return stks.get(caller).longValue(); + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/MarketsPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/MarketsPanel.java new file mode 100644 index 000000000..97151f897 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/MarketsPanel.java @@ -0,0 +1,53 @@ +package convex.gui.manager.mainpanels.actors; + +import java.awt.BorderLayout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JPanel; + +import convex.core.data.Address; +import convex.gui.components.AccountChooserPanel; +import convex.gui.components.ActionPanel; +import convex.gui.components.ScrollyList; +import convex.gui.manager.PeerGUI; + +/** + * Panel displaying current prediction markets + */ +@SuppressWarnings("serial") +public class MarketsPanel extends JPanel { + + private static final Logger log = LoggerFactory.getLogger(MarketsPanel.class.getName()); + AccountChooserPanel acctChooser; + + static DefaultListModel
marketList = new DefaultListModel
(); + + public MarketsPanel(PeerGUI manager) { + this.setLayout(new BorderLayout()); + + // =========================================== + // Top panel + acctChooser = new AccountChooserPanel(); + this.add(acctChooser, BorderLayout.NORTH); + + // =========================================== + // Central scrolling list + ScrollyList
scrollyList = new ScrollyList
(marketList, + addr -> new MarketComponent(this, addr)); + add(scrollyList, BorderLayout.CENTER); + + // ============================================ + // Action buttons + ActionPanel actionPanel = new ActionPanel(); + add(actionPanel, BorderLayout.SOUTH); + + JButton scanButton = new JButton("Scan"); + actionPanel.add(scanButton); + scanButton.addActionListener(e -> { + log.info("Scanning for prediction market Actors..."); + }); + } +} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/OraclePanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/OraclePanel.java new file mode 100644 index 000000000..85af3c8d3 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/OraclePanel.java @@ -0,0 +1,193 @@ +package convex.gui.manager.mainpanels.actors; + +import java.awt.BorderLayout; +import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.table.DefaultTableCellRenderer; + +import convex.core.Result; +import convex.core.State; +import convex.core.data.ACell; +import convex.core.data.Address; +import convex.core.data.MapEntry; +import convex.core.lang.RT; +import convex.core.lang.Reader; +import convex.core.util.Utils; +import convex.gui.components.ActionPanel; +import convex.gui.components.CodeLabel; +import convex.gui.components.DefaultReceiveAction; +import convex.gui.components.Toast; +import convex.gui.components.models.OracleTableModel; +import convex.gui.manager.PeerGUI; +import convex.gui.manager.mainpanels.WalletPanel; +import convex.gui.utils.Toolkit; + +@SuppressWarnings("serial") +public class OraclePanel extends JPanel { + + public static final Logger log = LoggerFactory.getLogger(OraclePanel.class.getName()); + + Address oracleAddress = PeerGUI.getLatestState().lookupCNS("convex.trusted-oracle"); + Address oracleActorAddress = PeerGUI.getLatestState().lookupCNS("convex.trusted-oracle.actor"); + + OracleTableModel tableModel = new OracleTableModel(PeerGUI.getLatestState(), oracleActorAddress); + JTable table = new JTable(tableModel); + + JScrollPane scrollPane = new JScrollPane(table);; + + long key = 1; + + public OraclePanel() { + this.setLayout(new BorderLayout()); + + // =========================================== + // Top label + add(new CodeLabel("Oracle at address: " + oracleAddress), BorderLayout.NORTH); + + // =========================================== + // Central table + PeerGUI.getStateModel().addPropertyChangeListener(pc -> { + State newState = (State) pc.getNewValue(); + tableModel.setState(newState); + }); + + // Column layouts + DefaultTableCellRenderer leftRenderer = new DefaultTableCellRenderer(); + leftRenderer.setHorizontalAlignment(JLabel.LEFT); + table.getColumnModel().getColumn(0).setCellRenderer(leftRenderer); + table.getColumnModel().getColumn(0).setPreferredWidth(80); + table.getColumnModel().getColumn(1).setCellRenderer(leftRenderer); + table.getColumnModel().getColumn(1).setPreferredWidth(300); + table.getColumnModel().getColumn(2).setCellRenderer(leftRenderer); + table.getColumnModel().getColumn(2).setPreferredWidth(80); + table.getColumnModel().getColumn(3).setCellRenderer(leftRenderer); + table.getColumnModel().getColumn(3).setPreferredWidth(300); + + // fonts + table.setFont(Toolkit.SMALL_MONO_FONT); + table.getTableHeader().setFont(Toolkit.SMALL_MONO_FONT); + + table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); // useful in scroll pane + scrollPane.getViewport().setBackground(null); + add(scrollPane, BorderLayout.CENTER); + + // ============================================ + // Action buttons + ActionPanel actionPanel = new ActionPanel(); + add(actionPanel, BorderLayout.SOUTH); + + JButton createButton = new JButton("Create..."); + actionPanel.add(createButton); + createButton.addActionListener(e -> { + String desc = JOptionPane.showInputDialog(this, "Enter Oracle description as plain text:"); + if ((desc == null) || (desc.isBlank())) return; + + ACell code = Reader.read("(call " + oracleAddress + " " + "(register " + (key++) + + " {:desc \"" + desc + "\" :trust #{*address*}}))"); + execute(code); + }); + + JButton finaliseButton = new JButton("Finalise..."); + actionPanel.add(finaliseButton); + finaliseButton.addActionListener(e -> { + String value = JOptionPane.showInputDialog(this, "Enter final value:"); + if ((value == null) || (value.isBlank())) return; + int ix = table.getSelectedRow(); + if (ix < 0) return; + MapEntry me = tableModel.getList().entryAt(ix); + + ACell code = Reader.read( + "(call " + oracleAddress + " " + "(provide " + me.getKey() + " " + value + "))"); + execute(code); + }); + + JButton makeMarketButton = new JButton("Make Market"); + actionPanel.add(makeMarketButton); + makeMarketButton.addActionListener(e -> { + int ix = table.getSelectedRow(); + if (ix < 0) return; + MapEntry me = tableModel.getList().entryAt(ix); + Object key = me.getKey(); + log.info("Making market: " + key); + + String opts = JOptionPane + .showInputDialog("Enter a list of possible values (forms, may separate with commas)"); + if ((opts == null) || opts.isBlank()) { + Toast.display(scrollPane, "Prediction market making cancelled", Toast.INFO); + return; + } + String outcomeString = "[" + opts + "]"; + + String actorCode; + try { + actorCode = Utils.readResourceAsString("actors/prediction-market.con"); + String source = "(let [pmc " + actorCode + " ] " + "(deploy (pmc " + " 0x" + + oracleAddress.toString() + " " + key + " " + outcomeString + ")))"; + ACell code = Reader.read(source); + PeerGUI.execute(WalletPanel.HERO, code).thenAcceptAsync(createMarketAction); + } catch (Exception e1) { + e1.printStackTrace(); + } + }); + } + + private void execute(ACell code) { + PeerGUI.execute(WalletPanel.HERO, code).thenAcceptAsync(receiveAction); + } + + private final Consumer createMarketAction = new Consumer() { + protected void handleResult(Object m) { + if (m instanceof Address) { + try { + Thread.sleep(200); + } catch (InterruptedException e) { + // ignore + } + Address addr = (Address) m; + MarketsPanel.marketList.addElement(addr); + showResult("Prediction market deployed: " + addr); + } else { + String resultString = "Expected Address but got: " + m; + log.warn(resultString); + Toast.display(scrollPane, resultString, Toast.FAIL); + } + } + + protected void handleError(long id, Object code, Object msg) { + showError(code,msg); + } + + @Override + public void accept(Result t) { + if (t.isError()) { + handleError(RT.jvm(t.getID()),t.getErrorCode(),t.getValue()); + } else { + handleResult(t.getValue()); + } + } + + }; + + private final DefaultReceiveAction receiveAction = new DefaultReceiveAction(scrollPane); + + private void showError(Object code, Object msg) { + String resultString = "Error executing transaction: " + code + " "+msg; + log.info(resultString); + Toast.display(scrollPane, resultString, Toast.FAIL); + } + + private void showResult(Object v) { + String resultString = "Transaction executed successfully\n" + "Result: " + Utils.toString(v); + log.info(resultString); + Toast.display(scrollPane, resultString, Toast.SUCCESS); + } +} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/BaseWindow.java b/convex-gui/src/main/java/convex/gui/manager/windows/BaseWindow.java new file mode 100644 index 000000000..92c8a84a9 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/windows/BaseWindow.java @@ -0,0 +1,32 @@ +package convex.gui.manager.windows; + +import java.awt.BorderLayout; + +import javax.swing.JFrame; +import javax.swing.JPanel; + +import convex.gui.manager.PeerGUI; + +@SuppressWarnings("serial") +public abstract class BaseWindow extends JPanel { + + protected final PeerGUI manager; + + public BaseWindow(PeerGUI manager) { + super(); + this.manager = manager; + setLayout(new BorderLayout()); + } + + public abstract String getTitle(); + + public JFrame launch() { + JFrame f = new JFrame(getTitle()); + f.getContentPane().add(this); + f.pack(); + f.setVisible(true); + f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + f.setLocationRelativeTo(PeerGUI.getFrame()); + return f; + } +} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorInfoPanel.java b/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorInfoPanel.java new file mode 100644 index 000000000..f2f740f9e --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorInfoPanel.java @@ -0,0 +1,51 @@ +package convex.gui.manager.windows.actor; + +import java.awt.BorderLayout; +import java.awt.Dimension; + +import javax.swing.JPanel; +import javax.swing.JTextArea; + +import convex.core.State; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.gui.manager.PeerGUI; +import convex.gui.utils.Toolkit; + +@SuppressWarnings("serial") +public class ActorInfoPanel extends JPanel { + + protected PeerGUI manager; + protected Address actor; + protected JTextArea infoArea; + + public ActorInfoPanel(PeerGUI manager, Address contract) { + this.manager = manager; + this.actor = contract; + setLayout(new BorderLayout(0, 0)); + + this.setPreferredSize(new Dimension(600, 400)); + + infoArea = new JTextArea(); + add(infoArea, BorderLayout.CENTER); + infoArea.setBackground(null); + infoArea.setFont(Toolkit.SMALL_MONO_FONT); + + PeerGUI.getStateModel().addPropertyChangeListener(e -> { + updateInfo((State) e.getNewValue()); + }); + updateInfo(PeerGUI.getLatestState()); + } + + private void updateInfo(State latestState) { + StringBuilder sb = new StringBuilder(); + AccountStatus as = latestState.getAccount(actor); + + sb.append("Actor Address: " + actor.toHexString() + "\n"); + sb.append("Actor Balance: " + as.getBalance() + "\n"); + sb.append("\n"); + + infoArea.setText(sb.toString()); + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorInvokePanel.java b/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorInvokePanel.java new file mode 100644 index 000000000..36b8e6ff0 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorInvokePanel.java @@ -0,0 +1,44 @@ +package convex.gui.manager.windows.actor; + +import java.awt.BorderLayout; + +import javax.swing.DefaultListModel; +import javax.swing.JPanel; + +import convex.core.data.ASet; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.core.data.Symbol; +import convex.gui.components.AccountChooserPanel; +import convex.gui.components.ScrollyList; +import convex.gui.manager.PeerGUI; + +@SuppressWarnings("serial") +public class ActorInvokePanel extends JPanel { + + protected PeerGUI manager; + protected Address contract; + AccountChooserPanel execPanel = new AccountChooserPanel(); + + DefaultListModel exportList = new DefaultListModel(); + + public ActorInvokePanel(PeerGUI manager, Address contract) { + this.manager = manager; + this.contract = contract; + + setLayout(new BorderLayout()); + + AccountStatus as = PeerGUI.getLatestState().getAccount(contract); + ASet exports = as.getCallableFunctions(); + for (Symbol s : exports) { + exportList.addElement(s); + } + + ScrollyList scrollyList = new ScrollyList(exportList, + sym -> new SmartOpComponent(this, contract, sym)); + add(scrollyList, BorderLayout.CENTER); + + add(execPanel, BorderLayout.NORTH); + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorWindow.java b/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorWindow.java new file mode 100644 index 000000000..4bd32d578 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorWindow.java @@ -0,0 +1,44 @@ +package convex.gui.manager.windows.actor; + +import java.awt.BorderLayout; + +import javax.swing.JTabbedPane; + +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.gui.manager.PeerGUI; +import convex.gui.manager.windows.BaseWindow; +import convex.gui.manager.windows.state.StateTreePanel; + +@SuppressWarnings("serial") +public class ActorWindow extends BaseWindow { + Address contract; + + JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.TOP); + + public ActorWindow(PeerGUI manager, Address contract) { + super(manager); + this.contract = contract; + AccountStatus as = PeerGUI.getLatestState().getAccount(contract); + + PeerGUI.getStateModel().addPropertyChangeListener(e -> { + + }); + + add(tabbedPane, BorderLayout.CENTER); + + tabbedPane.add("Overview", new ActorInfoPanel(manager, contract)); + tabbedPane.add("Environment", new StateTreePanel(as.getEnvironment())); + tabbedPane.add("Operations", new ActorInvokePanel(manager, contract)); + } + + @Override + public String getTitle() { + try { + return "Contract view - " + contract.toHexString(); + } catch (Exception e) { + return "Contract view - Unknown"; + } + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/actor/ArgBox.java b/convex-gui/src/main/java/convex/gui/manager/windows/actor/ArgBox.java new file mode 100644 index 000000000..e41ded4fc --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/windows/actor/ArgBox.java @@ -0,0 +1,13 @@ +package convex.gui.manager.windows.actor; + +import javax.swing.JTextField; + +import convex.gui.utils.Toolkit; + +@SuppressWarnings("serial") +public class ArgBox extends JTextField { + + public ArgBox() { + setFont(Toolkit.SMALL_MONO_FONT); + } +} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/actor/ParamLabel.java b/convex-gui/src/main/java/convex/gui/manager/windows/actor/ParamLabel.java new file mode 100644 index 000000000..66a7393a9 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/windows/actor/ParamLabel.java @@ -0,0 +1,20 @@ +package convex.gui.manager.windows.actor; + +import javax.swing.JLabel; +import javax.swing.SwingConstants; + +import convex.gui.utils.Toolkit; + +/** + * Generic label component for displaying code + */ +@SuppressWarnings("serial") +public class ParamLabel extends JLabel { + + public ParamLabel(String text) { + super(" " + text + " "); + this.setHorizontalAlignment(SwingConstants.RIGHT); + this.setFont(Toolkit.SMALL_MONO_FONT); + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/actor/SmartOpComponent.java b/convex-gui/src/main/java/convex/gui/manager/windows/actor/SmartOpComponent.java new file mode 100644 index 000000000..509689e09 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/windows/actor/SmartOpComponent.java @@ -0,0 +1,177 @@ +package convex.gui.manager.windows.actor; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.GridLayout; +import java.net.InetSocketAddress; +import java.util.HashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import convex.api.Convex; +import convex.core.Result; +import convex.core.crypto.WalletEntry; +import convex.core.data.ACell; +import convex.core.data.AList; +import convex.core.data.AVector; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.core.data.Lists; +import convex.core.data.Symbol; +import convex.core.data.Vectors; +import convex.core.lang.AFn; +import convex.core.lang.RT; +import convex.core.lang.Reader; +import convex.core.lang.Symbols; +import convex.core.lang.impl.Fn; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Invoke; +import convex.gui.components.AccountChooserPanel; +import convex.gui.components.BaseListComponent; +import convex.gui.components.CodeLabel; +import convex.gui.components.Toast; +import convex.gui.manager.PeerGUI; +import convex.gui.utils.Toolkit; + + +@SuppressWarnings("serial") +public class SmartOpComponent extends BaseListComponent { + + protected ActorInvokePanel parent; + protected Symbol sym; + int paramCount; + + /** + * Fields for each argument by position. + * + * Null entry used for funds offer + */ + private HashMap paramFields = new HashMap<>(); + + private static final Logger log = LoggerFactory.getLogger(SmartOpComponent.class.getName()); + + public SmartOpComponent(ActorInvokePanel parent, Address contract, Symbol sym) { + this.parent = parent; + this.sym = sym; + + setFont(Toolkit.SMALL_MONO_FONT); + setLayout(new BorderLayout(0, 0)); + + CodeLabel opName = new CodeLabel(sym.toString()); + opName.setFont(Toolkit.MONO_FONT); + add(opName, BorderLayout.NORTH); + + JPanel paramPanel = new JPanel(); + paramPanel.setLayout(new GridLayout(0, 3, 4, 4)); // 3 columns, small hgap and vgap + + AccountStatus as = PeerGUI.getLatestState().getAccount(contract); + + AFn fn = as.getCallableFunction(sym); + + // Function might be a map or set + AVector params = (fn instanceof Fn) ? ((Fn) fn).getParams() + : Vectors.of(Symbols.FOO); + paramCount = params.size(); + + for (int i = 0; i < paramCount; i++) { + ACell paramSym = params.get(i); + paramPanel.add(new ParamLabel(RT.str(paramSym))); + JTextField argBox = new ArgBox(); + paramPanel.add(argBox); + paramFields.put(i, argBox); + paramPanel.add(new JLabel("")); // TODO: descriptions? + } + paramPanel.add(new ParamLabel("")); + JTextField offerBox = new ArgBox(); + paramFields.put(null, offerBox); + paramPanel.add(offerBox); + paramPanel.add(new JLabel("Offer funds (0 or blank for no offer)")); + + add(paramPanel, BorderLayout.CENTER); + + JPanel aPanel = new JPanel(); + aPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); + JButton execButton = new JButton("Execute"); + aPanel.add(execButton); + execButton.addActionListener(e -> execute()); + + add(aPanel, BorderLayout.SOUTH); + + } + + private void execute() { + InetSocketAddress addr = PeerGUI.getDefaultPeer().getHostAddress(); + + AVector args = Vectors.empty(); + for (int i = 0; i < paramCount; i++) { + JTextField argBox = paramFields.get(i); + String s = argBox.getText(); + ACell arg = (s.isBlank()) ? null : Reader.read(s); + args = args.conj(arg); + } + String offerString = paramFields.get(null).getText(); + Long offer = (offerString.isBlank()) ? null : Long.parseLong(offerString.trim()); + + AList rest = Lists.of(Lists.create(args).cons(sym)); // (foo 1 2 3) + if (offer != null) { + rest = rest.cons(RT.cvm(offer)); + } + + try { + ACell message = RT.cons(Symbols.CALL, parent.contract, rest); + + AccountChooserPanel execPanel = parent.execPanel; + WalletEntry we = execPanel.getWalletEntry(); + Address myAddress=we.getAddress(); + + // connect to Peer as a client + Convex peerConnection = Convex.connect(addr, we.getAddress(),we.getKeyPair()); + + String mode = execPanel.getMode(); + Result r=null; + if (mode.equals("Query")) { + r=peerConnection.querySync(message); + } else if (mode.equals("Transact")) { + if (we.isLocked()) { + JOptionPane.showMessageDialog(this, + "Please select an unlocked wallet address to use for transactions before sending"); + return; + } + + ATransaction trans = Invoke.create(myAddress,-1, message); + r = peerConnection.transactSync(trans); + } else { + throw new Error("Unexpected mode: "+mode); + } + if (r.isError()) { + showError(r.getErrorCode(),r.getValue()); + } else { + showResult(r.getValue()); + } + + } catch (Throwable e) { + log.warn(e.getMessage()); + Toast.display(parent, "Unexpected Error: "+e.getMessage(), Toast.FAIL); + + } + + } + + private void showError(Object code, Object msg) { + String resultString = "Error executing transaction: " + code + " "+msg; + log.info(resultString); + Toast.display(parent, resultString, Toast.FAIL); + } + + private void showResult(Object v) { + String resultString = "Transaction executed successfully"; + log.info(resultString); + Toast.display(parent, resultString, Toast.SUCCESS); + } +} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/etch/EtchWindow.java b/convex-gui/src/main/java/convex/gui/manager/windows/etch/EtchWindow.java new file mode 100644 index 000000000..1d2593a3a --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/windows/etch/EtchWindow.java @@ -0,0 +1,47 @@ +package convex.gui.manager.windows.etch; + +import java.awt.BorderLayout; + +import javax.swing.JTabbedPane; + +import convex.gui.components.PeerComponent; +import convex.gui.components.PeerView; +import convex.gui.manager.PeerGUI; +import convex.gui.manager.windows.BaseWindow; +import etch.EtchStore; + +@SuppressWarnings("serial") +public class EtchWindow extends BaseWindow { + EtchStore store; + PeerView peer; + + + + public EtchStore getEtchStore() { + return store; + } + + JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.TOP); + + public EtchWindow(PeerGUI manager, PeerView peer) { + super(manager); + this.peer=peer; + this.store=(EtchStore) peer.peerServer.getStore(); + + PeerComponent pcom=new PeerComponent(manager,peer); + add(pcom, BorderLayout.NORTH); + + add(tabbedPane, BorderLayout.CENTER); + } + + @Override + public String getTitle() { + try { + return "Storage view - "+peer.getHostAddress(); + } + catch (Exception e) { + return "Storage view - Unknown"; + } + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/peer/PeerWindow.java b/convex-gui/src/main/java/convex/gui/manager/windows/peer/PeerWindow.java new file mode 100644 index 000000000..ebd058b60 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/windows/peer/PeerWindow.java @@ -0,0 +1,44 @@ +package convex.gui.manager.windows.peer; + +import java.awt.BorderLayout; + +import javax.swing.JTabbedPane; + +import convex.gui.components.PeerComponent; +import convex.gui.components.PeerView; +import convex.gui.manager.PeerGUI; +import convex.gui.manager.windows.BaseWindow; + +@SuppressWarnings("serial") +public class PeerWindow extends BaseWindow { + PeerView peer; + + public PeerView getPeerView() { + return peer; + } + + JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.TOP); + + public PeerWindow(PeerGUI manager, PeerView peer) { + super(manager); + this.peer = peer; + + add(tabbedPane, BorderLayout.CENTER); + tabbedPane.addTab("REPL", null, new REPLPanel(this.getPeerView()), null); + tabbedPane.addTab("Stress", null, new StressPanel(this.getPeerView()), null); + + PeerComponent pcom = new PeerComponent(manager, peer); + add(pcom, BorderLayout.NORTH); + + } + + @Override + public String getTitle() { + try { + return "Peer view - " + peer.peerConnection.getRemoteAddress(); + } catch (Exception e) { + return "Peer view - Unknown"; + } + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/peer/REPLPanel.java b/convex-gui/src/main/java/convex/gui/manager/windows/peer/REPLPanel.java new file mode 100644 index 000000000..f72bc9cd0 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/windows/peer/REPLPanel.java @@ -0,0 +1,296 @@ +package convex.gui.manager.windows.peer; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import javax.swing.JButton; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import convex.api.Convex; +import convex.core.Result; +import convex.core.crypto.AKeyPair; +import convex.core.crypto.WalletEntry; +import convex.core.data.ACell; +import convex.core.data.AList; +import convex.core.data.AString; +import convex.core.data.AVector; +import convex.core.data.Address; +import convex.core.lang.Reader; +import convex.core.lang.Symbols; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Invoke; +import convex.core.util.Utils; +import convex.gui.components.AccountChooserPanel; +import convex.gui.components.ActionPanel; +import convex.gui.components.PeerView; + +@SuppressWarnings("serial") +public class REPLPanel extends JPanel { + + JTextArea inputArea; + JTextArea outputArea; + private JButton btnClear; + private JButton btnInfo; + + private ArrayList history=new ArrayList<>(); + private int historyPosition=0; + + private InputListener inputListener=new InputListener(); + + + private JPanel panel_1; + + private AccountChooserPanel execPanel = new AccountChooserPanel(); + + private final Convex convex; + + private static final Logger log = LoggerFactory.getLogger(REPLPanel.class.getName()); + + public void setInput(String s) { + inputArea.setText(s); + } + + @Override + public void setVisible(boolean value) { + super.setVisible(value); + if (value) inputArea.requestFocusInWindow(); + } + + protected void handleResult(Result r) { + if (r.isError()) { + handleError(r.getErrorCode(),r.getValue(),r.getTrace()); + } else { + handleResult((Object)r.getValue()); + } + } + + protected void handleResult(Object m) { + outputArea.append(" => " + m + "\n"); + outputArea.setCaretPosition(outputArea.getDocument().getLength()); + } + + protected void handleError(Object code, Object msg, AVector trace) { + outputArea.append(" Exception: " + code + " "+ msg+"\n"); + if (trace!=null) for (AString s: trace) { + outputArea.append(" - "+s.toString()+"\n"); + } + } + + /** + * Create the panel. + * @param peerView + */ + public REPLPanel(PeerView peerView) { + setLayout(new BorderLayout(0, 0)); + + InetSocketAddress addr = peerView.getHostAddress(); + if (addr == null) { + JOptionPane.showMessageDialog(this, "Error: peer shut down already?"); + throw new Error("Connect fail, no remote address"); + } + try { + // Connect to peer as a client + convex = Convex.connect(addr, getAddress(),getKeyPair()); + } catch (Exception ex) { + throw Utils.sneakyThrow(ex); + } + + JSplitPane splitPane = new JSplitPane(); + splitPane.setResizeWeight(0.8); + splitPane.setOneTouchExpandable(true); + splitPane.setOrientation(JSplitPane.VERTICAL_SPLIT); + add(splitPane, BorderLayout.CENTER); + + outputArea = new JTextArea(); + outputArea.setRows(15); + outputArea.setEditable(false); + outputArea.setLineWrap(true); + outputArea.setFont(new Font("Monospaced", Font.PLAIN, 13)); + //DefaultCaret caret = (DefaultCaret)(outputArea.getCaret()); + //caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); + splitPane.setLeftComponent(new JScrollPane(outputArea)); + + inputArea = new JTextArea(); + inputArea.setRows(5); + inputArea.setFont(new Font("Monospaced", Font.PLAIN, 13)); + inputArea.getDocument().addDocumentListener(inputListener); + inputArea.addKeyListener(inputListener); + splitPane.setRightComponent(new JScrollPane(inputArea)); + + // stop ctrl+arrow losing focus + setFocusTraversalKeysEnabled(false); + inputArea.setFocusTraversalKeysEnabled(false); + + + add(execPanel, BorderLayout.NORTH); + + panel_1 = new ActionPanel(); + add(panel_1, BorderLayout.SOUTH); + + btnClear = new JButton("Clear"); + panel_1.add(btnClear); + btnClear.addActionListener(e -> outputArea.setText("")); + + btnInfo = new JButton("Connection Info"); + panel_1.add(btnInfo); + btnInfo.addActionListener(e -> { + String infoString = ""; + infoString += "Remote host: " + convex.getRemoteAddress() + "\n"; + infoString += "Sequence: " + convex.getSequence() + "\n"; + infoString += "Connection Account: " + convex.getAddress() + "\n"; + + JOptionPane.showMessageDialog(this, infoString); + }); + + // Get initial focus in REPL input area + addComponentListener(new ComponentAdapter() { + public void componentShown(ComponentEvent ce) { + inputArea.requestFocusInWindow(); + } + }); + } + + private AKeyPair getKeyPair() { + WalletEntry we=execPanel.getWalletEntry(); + if (we==null) return null; + return we.getKeyPair(); + } + + private Address getAddress() { + WalletEntry we=execPanel.getWalletEntry(); + return we.getAddress(); + } + + private void sendMessage(String s) { + if (s.isBlank()) return; + + history.add(s); + historyPosition=history.size(); + + SwingUtilities.invokeLater(() -> { + outputArea.append(s); + outputArea.append("\n"); + try { + AList forms = Reader.readAll(s); + ACell message = (forms.count()==1)?forms.get(0):forms.cons(Symbols.DO); + Future future; + String mode = execPanel.getMode(); + if (mode.equals("Query")) { + AKeyPair kp=getKeyPair(); + if (kp == null) { + future = convex.query(message,null); + } else { + future = convex.query(message, getAddress()); + } + } else if (mode.equals("Transact")) { + WalletEntry we = execPanel.getWalletEntry(); + if ((we == null) || (we.isLocked())) { + JOptionPane.showMessageDialog(this, + "Please select an address to use for transactions before sending"); + return; + } + Address address = getAddress(); + ATransaction trans = Invoke.create(address,-1, message); + future = convex.transact(trans); + } else { + throw new Error("Unrecognosed REPL mode: " + mode); + } + log.trace("Sent message"); + + handleResult(future.get(5000, TimeUnit.MILLISECONDS)); + } catch (TimeoutException t) { + outputArea.append(" TIMEOUT waiting for result"); + } catch (Throwable t) { + outputArea.append(" SEND ERROR: "); + outputArea.append(t.getMessage() + "\n"); + t.printStackTrace(); + } + inputArea.setText(""); + }); + } + + /** + * Listener to detect returns at the end of the input box => send message + */ + private class InputListener implements DocumentListener, KeyListener { + + @Override + public void insertUpdate(DocumentEvent e) { + int len = e.getLength(); + int off = e.getOffset(); + String s = inputArea.getText(); + if ((len == 1) && (len + off == s.length()) && (s.charAt(off) == '\n')) { + sendMessage(s.trim()); + } + } + + @Override + public void removeUpdate(DocumentEvent e) { + // nothing special + } + + @Override + public void changedUpdate(DocumentEvent e) { + // nothing special + } + + @Override + public void keyTyped(KeyEvent e) { + + } + + @Override + public void keyPressed(KeyEvent e) { + // System.out.println(e); + if (e.isControlDown()) { + int code = e.getKeyCode(); + int hSize=history.size(); + if (code==KeyEvent.VK_UP) { + + if (historyPosition>0) { + if (historyPosition==hSize) { + // store current in history + String s=inputArea.getText(); + history.add(s); + } + historyPosition--; + setInput(history.get(historyPosition)); + } + e.consume(); // mark event consumed + } else if (code==KeyEvent.VK_DOWN) { + if (historyPosition { + btnRun.setEnabled(false); + SwingUtilities.invokeLater(() -> runStressTest()); + }); + + splitPane = new JSplitPane(); + add(splitPane, BorderLayout.CENTER); + + JPanel panel = new JPanel(); + splitPane.setLeftComponent(panel); + FlowLayout flowLayout = (FlowLayout) panel.getLayout(); + flowLayout.setAlignment(FlowLayout.LEFT); + flowLayout.setAlignOnBaseline(true); + + // ========================================= + // Option Panel + + JPanel optionPanel = new JPanel(); + panel.add(optionPanel); + optionPanel.setLayout(new GridLayout(0, 2, 0, 0)); + + JLabel lblNewLabel = new JLabel("Transactions per client"); + optionPanel.add(lblNewLabel); + transactionCountSpinner = new JSpinner(); + transactionCountSpinner.setModel(new SpinnerNumberModel(1000, 1, 1000000, 100)); + optionPanel.add(transactionCountSpinner); + + JLabel lblNewLabel2 = new JLabel("Ops per Transaction"); + optionPanel.add(lblNewLabel2); + opCountSpinner = new JSpinner(); + opCountSpinner.setModel(new SpinnerNumberModel(1, 1, 1000, 10)); + optionPanel.add(opCountSpinner); + + JLabel lblNewLabel3 = new JLabel("Clients"); + optionPanel.add(lblNewLabel3); + clientCountSpinner = new JSpinner(); + clientCountSpinner.setModel(new SpinnerNumberModel(1, 1, 100, 1)); + optionPanel.add(clientCountSpinner); + + // ========================================= + // Result Panel + + resultPanel = new JPanel(); + splitPane.setRightComponent(resultPanel); + resultPanel.setLayout(new BorderLayout(0, 0)); + + resultArea = new JTextArea(); + resultArea.setText("No results yet"); + resultArea.setLineWrap(true); + resultArea.setEditable(false); + resultPanel.add(resultArea); + resultArea.setFont(Toolkit.SMALL_MONO_FONT); + } + + long errors = 0; + long values = 0; + + private JSplitPane splitPane; + private JPanel resultPanel; + private JTextArea resultArea; + + NumberFormat formatter = new DecimalFormat("#0.000"); + + private synchronized void runStressTest() { + errors = 0; + values = 0; + Address address=PeerGUI.getGenesisAddress(); + + int transCount = (Integer) transactionCountSpinner.getValue(); + int opCount = (Integer) opCountSpinner.getValue(); + // TODO: enable multiple clients + int clientCount = (Integer) clientCountSpinner.getValue(); + + new SwingWorker() { + @Override + protected String doInBackground() throws Exception { + StringBuilder sb = new StringBuilder(); + try { + InetSocketAddress sa = peerView.peerServer.getHostAddress(); + + // Use client store + // Stores.setCurrent(Stores.CLIENT_STORE); + ArrayList> frs=new ArrayList<>(); + Convex pc = Convex.connect(sa, address,PeerGUI.getUserKeyPair(0)); + + ArrayList kps=new ArrayList<>(clientCount); + for (int i=0; i v=pc.transactSync(Invoke.create(address, -1, cmdsb.toString())).getValue(); + + ArrayList ccs=new ArrayList<>(clientCount); + for (int i=0; i> cfutures=Utils.futureMap (cc->{ + try { + for (int i = 0; i < transCount; i++) { + StringBuilder tsb = new StringBuilder(); + tsb.append("(def a (do "); + for (int j = 0; j < opCount; j++) { + tsb.append(" (* 10 " + i + ")"); + } + tsb.append("))"); + String source = tsb.toString(); + + ATransaction t = Invoke.create(cc.getAddress(),-1, Reader.read(source)); + CompletableFuture fr; + fr = cc.transact(t); + frs.add(fr); + } + } catch (IOException e) { + throw Utils.sneakyThrow(e); + } + return null; + },ccs); + // wait for everything to be sent + for (int i=0; i results = Utils.completeAll(frs).get(60, TimeUnit.SECONDS); + long endTime = Utils.getCurrentTimestamp(); + + for (Result r : results) { + if (r.isError()) { + errors++; + } else { + values++; + } + } + + for (int i=0; i implements TreeNode { + + private final T object; + private final boolean isContainer; + + public StateTreeNode(T o) { + this.object = o; + this.isContainer = Utils.refCount(o)>0; + } + + private static StateTreeNode create(R value) { + return new StateTreeNode(value); + } + + @Override + public TreeNode getChildAt(int childIndex) { + if (isContainer) { + ACell child = object.getRef(childIndex).getValue(); + return StateTreeNode.create(child); + } + return null; + } + + @Override + public int getChildCount() { + return isContainer ? ((ACell) object).getRefCount() : 0; + } + + @Override + public TreeNode getParent() { + return null; + } + + @Override + public int getIndex(TreeNode node) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public boolean getAllowsChildren() { + return isContainer; + } + + @Override + public boolean isLeaf() { + return getChildCount() == 0; + } + + @SuppressWarnings("unchecked") + @Override + public Enumeration children() { + + Ref[] childRefs = (isContainer ? ((ACell) object).getChildRefs() : new Ref[0]); + ArrayList> tns = new ArrayList<>(); + for (Ref r : childRefs) { + tns.add(StateTreeNode.create(r.getValue())); + } + return Collections.enumeration(tns); + } + + @Override + public String toString() { + return Utils.getClassName(object); // +" : "+Utils.toString(object); + } + +} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/state/StateTreePanel.java b/convex-gui/src/main/java/convex/gui/manager/windows/state/StateTreePanel.java new file mode 100644 index 000000000..3a1d18b29 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/windows/state/StateTreePanel.java @@ -0,0 +1,129 @@ +package convex.gui.manager.windows.state; + +import java.awt.BorderLayout; +import java.awt.Dimension; + +import javax.swing.JPanel; +import javax.swing.JTree; +import javax.swing.event.TreeExpansionEvent; +import javax.swing.event.TreeWillExpandListener; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.ExpandVetoException; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; + +import convex.core.data.ACell; +import convex.core.data.ARecord; +import convex.core.data.Keyword; +import convex.core.data.MapEntry; +import convex.core.data.MapLeaf; +import convex.core.util.Utils; + +@SuppressWarnings("serial") +public class StateTreePanel extends JPanel { + + private final ACell state; + + private static class Node extends DefaultMutableTreeNode { + private final boolean container; + private boolean loaded = false; + private final String name; + + public Node(String name, ACell val) { + super(val); + this.name = name; + container = Utils.refCount(val)>0; + } + + public Node(ACell val) { + this(null, val); + } + + @Override + public boolean isLeaf() { + return !container; + } + + private static String getString(Object val) { + if (val instanceof ACell) { + ACell r = (ACell) val; + return r.getClass().getSimpleName() + " [" + r.getHash().toHexString(6) + "...]"; + } + + return Utils.getClassName(val); + } + + @Override + public String toString() { + if (name != null) return name; + return getString(this.userObject); + } + + @SuppressWarnings("rawtypes") + public void loadChildren() { + if (loaded) return; + loaded = true; + + if (userObject instanceof ARecord) { + ARecord r = (ARecord) userObject; + for (Keyword k : r.getKeys()) { + ACell c = r.get(k); + add(new Node(k + " = " + getString(c), c)); + } + return; + } else if (userObject instanceof MapLeaf) { + MapLeaf m = (MapLeaf) userObject; + for (Object oe : m.entrySet()) { + MapEntry e = (MapEntry) oe; + ACell c = e.getValue(); + add(new Node(getString(e.getKey()) + " = " + getString(c), c)); + } + return; + } + + if (!container) return; + ACell rc = (ACell) userObject; + int n = rc.getRefCount(); + for (int i = 0; i < n; i++) { + ACell child = rc.getRef(i).getValue(); + add(new Node(child)); + } + + } + + } + + private static final TreeWillExpandListener expandListener = new TreeWillExpandListener() { + @Override + public void treeWillExpand(TreeExpansionEvent tee) throws ExpandVetoException { + TreePath path = tee.getPath(); + Node tn = (Node) path.getLastPathComponent(); + tn.loadChildren(); + } + + @Override + public void treeWillCollapse(TreeExpansionEvent tee) throws ExpandVetoException { + /* Nothing to do */ + } + }; + + public StateTreePanel(ACell state) { + this.state = state; + setLayout(new BorderLayout()); + setPreferredSize(new Dimension(600, 400)); + + // StateTreeNode root=new StateTreeNode<>(this.state); + + Node tNode = new Node(this.state); + + tNode.setAllowsChildren(true); + tNode.loadChildren(); + TreeModel tModel = new DefaultTreeModel(tNode); + JTree tree = new JTree(tModel); + tree.addTreeWillExpandListener(expandListener); + tree.expandPath(new TreePath(tNode.getPath())); + + add(tree, BorderLayout.CENTER); + } +} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/state/StateWindow.java b/convex-gui/src/main/java/convex/gui/manager/windows/state/StateWindow.java new file mode 100644 index 000000000..d67491350 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/manager/windows/state/StateWindow.java @@ -0,0 +1,30 @@ +package convex.gui.manager.windows.state; + +import java.awt.BorderLayout; + +import javax.swing.JTabbedPane; + +import convex.core.data.ACell; +import convex.gui.manager.PeerGUI; +import convex.gui.manager.windows.BaseWindow; + +@SuppressWarnings("serial") +public class StateWindow extends BaseWindow { + + JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.TOP); + + public StateWindow(PeerGUI manager, ACell state) { + super(manager); + + add(tabbedPane, BorderLayout.CENTER); + + tabbedPane.addTab("State Tree", null, new StateTreePanel(state), null); + + } + + @Override + public String getTitle() { + return "State explorer"; + } + +} diff --git a/convex-gui/src/main/java/convex/gui/utils/RobinsonProjection.java b/convex-gui/src/main/java/convex/gui/utils/RobinsonProjection.java new file mode 100644 index 000000000..23192f9a7 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/utils/RobinsonProjection.java @@ -0,0 +1,112 @@ +package convex.gui.utils; + +import java.awt.geom.Point2D; + +public class RobinsonProjection { + public static final double HALFPI = Math.PI * 0.5; + + private final static double X[] = { 1, -5.67239e-12, -7.15511e-05, 3.11028e-06, 0.9986, -0.000482241, -2.4897e-05, + -1.33094e-06, 0.9954, -0.000831031, -4.4861e-05, -9.86588e-07, 0.99, -0.00135363, -5.96598e-05, 3.67749e-06, + 0.9822, -0.00167442, -4.4975e-06, -5.72394e-06, 0.973, -0.00214869, -9.03565e-05, 1.88767e-08, 0.96, + -0.00305084, -9.00732e-05, 1.64869e-06, 0.9427, -0.00382792, -6.53428e-05, -2.61493e-06, 0.9216, + -0.00467747, -0.000104566, 4.8122e-06, 0.8962, -0.00536222, -3.23834e-05, -5.43445e-06, 0.8679, -0.00609364, + -0.0001139, 3.32521e-06, 0.835, -0.00698325, -6.40219e-05, 9.34582e-07, 0.7986, -0.00755337, -5.00038e-05, + 9.35532e-07, 0.7597, -0.00798325, -3.59716e-05, -2.27604e-06, 0.7186, -0.00851366, -7.0112e-05, + -8.63072e-06, 0.6732, -0.00986209, -0.000199572, 1.91978e-05, 0.6213, -0.010418, 8.83948e-05, 6.24031e-06, + 0.5722, -0.00906601, 0.000181999, 6.24033e-06, 0.5322, 0., 0., 0. }; + + private final static double Y[] = { 0, 0.0124, 3.72529e-10, 1.15484e-09, 0.062, 0.0124001, 1.76951e-08, + -5.92321e-09, 0.124, 0.0123998, -7.09668e-08, 2.25753e-08, 0.186, 0.0124008, 2.66917e-07, -8.44523e-08, + 0.248, 0.0123971, -9.99682e-07, 3.15569e-07, 0.31, 0.0124108, 3.73349e-06, -1.1779e-06, 0.372, 0.0123598, + -1.3935e-05, 4.39588e-06, 0.434, 0.0125501, 5.20034e-05, -1.00051e-05, 0.4968, 0.0123198, -9.80735e-05, + 9.22397e-06, 0.5571, 0.0120308, 4.02857e-05, -5.2901e-06, 0.6176, 0.0120369, -3.90662e-05, 7.36117e-07, + 0.6769, 0.0117015, -2.80246e-05, -8.54283e-07, 0.7346, 0.0113572, -4.08389e-05, -5.18524e-07, 0.7903, + 0.0109099, -4.86169e-05, -1.0718e-06, 0.8435, 0.0103433, -6.46934e-05, 5.36384e-09, 0.8936, 0.00969679, + -6.46129e-05, -8.54894e-06, 0.9394, 0.00840949, -0.000192847, -4.21023e-06, 0.9761, 0.00616525, + -0.000256001, -4.21021e-06, 1., 0., 0., 0 }; + + private final int NODES = 18; + private final static double FXC = 0.8487; + private final static double FYC = 1.3523; + private final static double C1 = 11.45915590261646417544; + private final static double RC1 = 0.08726646259971647884; + private final static double EPS = 1e-8; + + private RobinsonProjection() { + } + + private static final RobinsonProjection INSTANCE = new RobinsonProjection(); + + public static Point2D getPoint(double latitude, double longitude) { + Point2D.Double pt = new Point2D.Double(); + + double lam = longitude / 360.0 * Math.PI; + double phi = latitude / 90.0; + INSTANCE.project(lam, phi, pt); + pt.x = 0.45 + (pt.x * 0.357); + pt.y = 0.5 - (pt.y * 0.56); + return pt; + } + + private double poly(double[] array, int offset, double z) { + return (array[offset] + z * (array[offset + 1] + z * (array[offset + 2] + z * array[offset + 3]))); + } + + public Point2D.Double project(double lplam, double lpphi, Point2D.Double xy) { + double phi = Math.abs(lpphi); + int i = (int) Math.floor(phi * C1); + if (i >= NODES) i = NODES - 1; + phi = Math.toDegrees(phi - RC1 * i); + i *= 4; + xy.x = poly(X, i, phi) * FXC * lplam; + xy.y = poly(Y, i, phi) * FYC; + if (lpphi < 0.0) xy.y = -xy.y; + return xy; + } + + public Point2D.Double projectInverse(double x, double y, Point2D.Double lp) { + int i; + double t, t1; + + lp.x = x / FXC; + lp.y = Math.abs(y / FYC); + if (lp.y >= 1.0) { + if (lp.y > 1.000001) { + throw new Error("Out of range y?"); + } else { + lp.y = y < 0. ? -HALFPI : HALFPI; + lp.x /= X[4 * NODES]; + } + } else { + for (i = 4 * (int) Math.floor(lp.y * NODES);;) { + if (Y[i] > lp.y) i -= 4; + else if (Y[i + 4] <= lp.y) i += 4; + else break; + } + t = 5. * (lp.y - Y[i]) / (Y[i + 4] - Y[i]); + double Tc0 = Y[i]; + double Tc1 = Y[i + 1]; + double Tc2 = Y[i + 2]; + double Tc3 = Y[i + 3]; + t = 5. * (lp.y - Tc0) / (Y[i + 1] - Tc0); + Tc0 -= lp.y; + for (;;) { // Newton-Raphson + t -= t1 = (Tc0 + t * (Tc1 + t * (Tc2 + t * Tc3))) / (Tc1 + t * (Tc2 + Tc2 + t * 3. * Tc3)); + if (Math.abs(t1) < EPS) break; + } + lp.y = Math.toRadians(5 * i + t); + if (y < 0.) lp.y = -lp.y; + lp.x /= poly(X, i, t); + } + return lp; + } + + public boolean hasInverse() { + return true; + } + + public String toString() { + return "Robinson"; + } + +} diff --git a/convex-gui/src/main/java/convex/gui/utils/Toolkit.java b/convex-gui/src/main/java/convex/gui/utils/Toolkit.java new file mode 100644 index 000000000..8f16e2178 --- /dev/null +++ b/convex-gui/src/main/java/convex/gui/utils/Toolkit.java @@ -0,0 +1,139 @@ +package convex.gui.utils; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.net.URL; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.UIManager.LookAndFeelInfo; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.plaf.FontUIResource; + +import mdlaf.MaterialLookAndFeel; +import mdlaf.themes.AbstractMaterialTheme; +import mdlaf.themes.MaterialOceanicTheme; + +public class Toolkit { + + private static final Logger log = LoggerFactory.getLogger(Toolkit.class.getName()); + + static { + try { + UIManager.installLookAndFeel("Material", "mdlaf.MaterialLookAndFeel"); + Class.forName("mdlaf.MaterialLookAndFeel"); + // search for Nimbus look and feel if it is available + for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { + String name = info.getName(); + // log.info("Found L&F: " + name); + if (name.equals("Nimbus")) { // Nimbus + UIManager.setLookAndFeel(info.getClassName()); + // UIManager.put("nimbusBase", new Color(130,89,171)); + // UIManager.put("menu", new Color(61,89,171)); + // UIManager.put("control", new Color(200,180,160)); + } + } + + // prefer MaterialLookAndFeel if we have it + AbstractMaterialTheme theme = new MaterialOceanicTheme(); + MaterialLookAndFeel material = new MaterialLookAndFeel(theme); + UIManager.setLookAndFeel(material); + theme.setFontBold(new FontUIResource(theme.getFontBold().deriveFont(14.0f))); + theme.setFontItalic(new FontUIResource(theme.getFontItalic().deriveFont(14.0f))); + theme.setFontMedium(new FontUIResource(theme.getFontMedium().deriveFont(14.0f))); + theme.setFontRegular(new FontUIResource(theme.getFontRegular().deriveFont(14.0f))); + + UIManager.getLookAndFeelDefaults().put("TextField.caretForeground", Color.white); + } catch (Exception e) { + e.printStackTrace(); + log.warn("Unable to set look and feel: {}", e); + } + } + + // public static final ImageIcon LOCKED_ICON = + // scaledIcon(36,"/images/ic_lock_outline_black_36dp.png"); + // public static final ImageIcon UNLOCKED_ICON = + // scaledIcon(36,"/images/ic_lock_open_black_36dp.png"); + + public static final ImageIcon LOCKED_ICON = scaledIcon(36, "/images/padlock.png"); + public static final ImageIcon UNLOCKED_ICON = scaledIcon(36, "/images/padlock-open.png"); + public static final ImageIcon WARNING = scaledIcon(36, "/images/ic_priority_high_black_36dp.png"); + public static final ImageIcon CAKE = scaledIcon(36, "/images/ic_cake_black_36dp.png"); + public static final ImageIcon CONVEX = scaledIcon(36, "/images/Convex.png"); + public static final ImageIcon COG = scaledIcon(36, "/images/cog.png"); + + public static final Font DEFAULT_FONT = new JLabel().getFont(); + + public static final Font MONO_FONT = new Font(Font.MONOSPACED, Font.BOLD, 16); + + public static final Font SMALL_MONO_FONT = new Font(Font.MONOSPACED, Font.PLAIN, 10); + public static final Font SMALL_MONO_BOLD = SMALL_MONO_FONT.deriveFont(Font.BOLD); + + public static ImageIcon scaledIcon(int size, String resourcePath) { + java.net.URL imgURL = Toolkit.class.getResource(resourcePath); + if (imgURL == null) throw new Error("No image: " + resourcePath); + ImageIcon imageIcon = new ImageIcon(imgURL); + Image image = imageIcon.getImage(); + image = image.getScaledInstance(size, size, Image.SCALE_SMOOTH); + + return new ImageIcon(image); + } + + /** + * Scale an image with interpolation / AA for nicer effects + * + * @param src + * @param w + * @param h + * @return A new, resized image + */ + public static BufferedImage smoothResize(BufferedImage src, int w, int h) { + BufferedImage newImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = (Graphics2D) newImage.getGraphics(); + + // set up rendering hints + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + + graphics.drawImage(src, 0, 0, w, h, null); + return newImage; + } + + public static DocumentListener createDocumentListener(final Runnable a) { + return new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + SwingUtilities.invokeLater(a); + } + + @Override + public void removeUpdate(DocumentEvent e) { + SwingUtilities.invokeLater(a); + } + + @Override + public void changedUpdate(DocumentEvent e) { + SwingUtilities.invokeLater(a); + } + }; + } + + public static void init() { + // Empty method, just triggers static initialisation + } + + public static Image getImage(URL resourceURL) { + return java.awt.Toolkit.getDefaultToolkit().getImage(resourceURL); + } +} diff --git a/convex-gui/src/main/java/convex/wallet/Wallet.java b/convex-gui/src/main/java/convex/wallet/Wallet.java new file mode 100644 index 000000000..103e419c5 --- /dev/null +++ b/convex-gui/src/main/java/convex/wallet/Wallet.java @@ -0,0 +1,73 @@ +package convex.wallet; + +import java.awt.BorderLayout; +import java.awt.EventQueue; +import java.awt.Toolkit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; + +import convex.gui.manager.mainpanels.HomePanel; + +@SuppressWarnings("serial") +public class Wallet extends JPanel { + + private static final Logger log = LoggerFactory.getLogger(Wallet.class.getName()); + + private static JFrame frame; + + public static long maxBlock = 0; + + /** + * Launch the application. + * @param args Command line args + */ + public static void main(String[] args) { + // call to set up Look and Feel + convex.gui.utils.Toolkit.init(); + + EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + try { + Wallet.frame = new JFrame(); + frame.setTitle("Convex Secure Wallet"); + frame.setIconImage(Toolkit.getDefaultToolkit() + .getImage(Wallet.class.getResource("/images/ic_stars_black_36dp.png"))); + frame.setBounds(100, 100, 1024, 768); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + Wallet window = new Wallet(); + frame.getContentPane().add(window, BorderLayout.CENTER); + frame.pack(); + frame.setVisible(true); + log.debug("Wallet GUI launched"); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + /* + * Main component panel + */ + JPanel panel = new JPanel(); + + HomePanel homePanel = new HomePanel(); + JTabbedPane tabs = new JTabbedPane(); + + /** + * Create the application. + */ + public Wallet() { + setLayout(new BorderLayout()); + this.add(tabs, BorderLayout.CENTER); + + tabs.add("Home", homePanel); + + } +} diff --git a/convex-gui/src/main/resources/images/Convex.png b/convex-gui/src/main/resources/images/Convex.png new file mode 100644 index 0000000000000000000000000000000000000000..cee27b2c31ef93ef7feb6b2577966fc69535c258 GIT binary patch literal 10210 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 literal 0 HcmV?d00001 diff --git a/convex-gui/src/main/resources/images/cog.png b/convex-gui/src/main/resources/images/cog.png new file mode 100644 index 0000000000000000000000000000000000000000..d31d3cbc21751c902c14129b30efa116e5ed8d44 GIT binary patch literal 5007 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 literal 0 HcmV?d00001 diff --git a/convex-gui/src/main/resources/images/ic_lock_outline_black_36dp.png b/convex-gui/src/main/resources/images/ic_lock_outline_black_36dp.png new file mode 100644 index 0000000000000000000000000000000000000000..98b7144e49cc3793b2703234a60eb727e46b73a9 GIT binary patch literal 504 zcmV=w#@+P+(DZa=@TvN*eNsEC(;c4me)_AUy>P~} zgd^PJ8zaB`#yt+Rm~qsFS?u8lQo`@-Vg@y4CEt(=eqk9kV+<#d7#?9vZF0^cF+GKS zQi-FJ;W<}-`J4|IGg2xik(FmIT+%*ogQ~h`JCHjv(p=qS%J02QlHE{(%vu5ydQW z`u%q>LGLi06Nk6@{a-O*ME}55;&4m9e;5-)y+9nUAZlqm$b&p+B7Hnz2&s}oJf?>U zq+~v?5d&W`H{H?sm1E=!#*>&n#L#XM^Tagh5s4Wx4H_UZf@<*KkX8-yAP@2&57LRE z)1tmXI#I%DVIP!WqP$>y{et?EO$+;=C&{LTeb7+EwD6$EC4(N;FQ}(vP-p#uCh)Q- z=UF{(dzj0MFo!wyesxTsn@0@%@*$m!_m^u*JjjDQ$b(wo-!YF(gC3HY9@C&slA+VW uz=(+wH7yLRJW*mz3#pDeQR;}}I6nbt1rk?Ha-Y2b00004QV=_mX^l- YE?JIa-Ns3KfNo*%boFyt=akR{0DZzlvH$=8 literal 0 HcmV?d00001 diff --git a/convex-gui/src/main/resources/images/ic_stars_black_36dp.png b/convex-gui/src/main/resources/images/ic_stars_black_36dp.png new file mode 100644 index 0000000000000000000000000000000000000000..177745993033c2b1dc308b209f13ffffa6d00e7b GIT binary patch literal 951 zcmV;o14#UdP)7f#LxKuJI;^~lkSH_hU7L`q<`j^|xPnV6Kzu)is{7&MeH8y9Q@btB36|D2mjt%OcHc?s{9D5{V7k>>4%GZl5TlZf&Y= zvW=pGHfT{rclupa*m3hz#b|GcDz!?z3XRt#s@OJ7D$wG9sB&MJs7&J>6p8itJbg%- zlwov9lXbTRBDwAAmF#tq+%M{l@@q-UyF_=0^gIWi7wMTNok=C_JA)>H$p%Z>ms%uy zPo!rffK4Jj>m`~kN~#UO%rbs6L1{2S3>P{j(z9Hy6(T)@nichsNY6hmlIwp?i}XC8s82T;OMbSQx=~_{=mkl-6O9qGhzb|Ss)N7(>S4ps26zMkq zx!sQgf>t%*S?}!5Rf<`Z7>{$JlX#ht=f!0da z>V(MKTbS<$k+R*AgbtVFMh8n#l&>UD}5^|3l-80Sk- zgL*V7(*^d|U^wTiz_=<;+*XY$G^&crdxbhxG2aPMVSR2=MXlZzWp~;d&8lve-J*;- z%}_HlZ80bk8}PpAYOcj%-5F2hzjeCLL}yWhIhI;$pYQ#q-$_Ruvd3zVxXx(9h7CJM Z{R=Q^kyNaPu$2G+002ovPDHLkV1kG6%wYfk literal 0 HcmV?d00001 diff --git a/convex-gui/src/main/resources/images/padlock-open.png b/convex-gui/src/main/resources/images/padlock-open.png new file mode 100644 index 0000000000000000000000000000000000000000..eddf1e66a9229058673de5527e7220b135294844 GIT binary patch literal 1946 zcmV;L2W9w)P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2QNuPK~#8N?VEdS z6h#=oKaRH7*Fo=E9>vn47Q_IW8mnyp%S))Fij|0j_-IUkAcDmREd+yDl$elUc=Qia zq^JP|s+5;-gz!)-MggUi0!6VcebNWl$Cc~j>i5k}_pXIrw|l$WYuaD(%gk)ycJ`Z{ zncdml0iX`!(KvJxIu;#<`k{{dH=~tk9-4)oKrf)JXs?70?oIR$lnz3Q|DYeD-e|Al zX|%2NpM(^{gL6jM74g!4v$n8W`ZP_l8C*l-zHI zYlV$)`dU4lyjl;ICOUQjE4mKdz)CV9cK|*>x1t&<&;c%(I0%wn3xp?zdeSD*RMxh@ z)|1t+4Smo)5Zh7e7P25`0Op~4QFidbdJovOI2b04^`W9<@=6=w-P97uF0k+|jL*>Z zwybN&6@W45CDfl4g1z9#$`E+W;6X(Owc@cWb$EK4;Z{i_)HGP3wbkCT)FmFrvxK@z ze560<{RZ;C0MtLQ!u+kpa3+T)3I=tDS5PA>xquu2px-!+PNl-I!5(mKT`2Z*`@w(v ziwV{rtb{UCGvDGI3P$2=J$w>p0MCK8D%M-9Fzd@g_$yyH6aGWTp+Z!oAV&aZqsJ&i zKXw>fe(E@1Dne6BD=bbch5cEztlV9qM*F~l#1X>T@Nan&M6JySGY*)XmFOm0RvK~y z;0!v2GIL+l!R{r&lw~a3Q3`v1tKlUEMPd(n9xX%Lj)dxAKqJw&(Ge))g(tn?#OjgY z`6i;wQRZViciaXbUU#0JL*D2sU5;z9Fo&E`1C$ZP9SH&B3GS5VR|3)ZBs! z|954aLHOOG)bLj9B96a16O{}bv0We*Wla+?${WUoXekTLcd7+5Y1DVKHk>vi8I&DP*~Z-OKgx@h<5ih zU5(N}$7YZN7wuiiabHLjPbJ}M0BHJSJQHebed2sBHKM=~l=^CSq2WH=mIX-Ka}PfL zt{gTUt%Bm}W(Xhd#sA`309@rJ%KD;l%}Al2&UxP7v5&fP)%G&#_*^6j?;m&Xevs#-F37yn`wNwS=(xWE$?72__7j8Cmd=e~)4s{b%Q?X+}G0L8JWTk|`gptzD z0(um<>zoNT9jS8cs|;H1UN|iPR=sLqgRB&?3oQ`+X+AHp=~{Te(it+er#NV7d;XSU zSe{v z%-vihOP3IXDgmH5`iCt0;&6O?Jmlo$!13e9;n1N&aO1`eNJ>hA0sNJbs|6O=W~^p; zrBNjSsi*7*LoJ?`mIi@=_9=_bhUDaANK8!RTL5;PzR$)CrBOTpR=hYnpKHHyp_bNk zGL@`Yv4RDID5`3P+hxKHm0~C!fXccSXlS%IWkf^-&pnJzr-ShDa9#qqi$PEt#RK5& z*?y)qo1MSV;rMqge zKP4pv^7Dm@6#;7bWy|b$u~FeZ5TsN7Zc~*2ERWU0AUqR@^73++F=Gas`r0s=Ob{Cz z3zsg@i)J>G=d`=AQ4CcAFg(Bmw!c4&y)jBuR8&AlMux3`7>!2A&d%l~oDF6hI=%#< z7^(z-h>z64dohD~381OTemU%zpZJsyq|6KACFM~q0EEuF>)p|ysV3^~%?;@f08jfY z{Q=-9`s>J+hEfZo~`zR*g*ySk+_^{Ombh=E#uyRjP5+XsL? z&^T<%%ELaCRyTWt^x?JG#uW{%Q2dBKYKQJ5eUPO`6@j8U7d?s6AoHjS^n~<@3Vm|F g7`1<}*d+k}17iIVh$9lS9{>OV07*qoM6N<$g0Rn`xBvhE literal 0 HcmV?d00001 diff --git a/convex-gui/src/main/resources/images/padlock.png b/convex-gui/src/main/resources/images/padlock.png new file mode 100644 index 0000000000000000000000000000000000000000..9cfd949dece2f3f740badbe0bc91b27a8983bc4f GIT binary patch literal 1948 zcmV;N2V?k&P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGh*8l(w*8xH(n|J^K2Qf)RK~#8N?VEW_ z6jvO_f3DzixC#p%SOnXmwNcy9gchO}Z_&_NYHE$?L7J$sR*Oj$Y;3H>BTY}0ecv_j zy_tTX4dYR32@1g!gQ6c2$AJk_kFI11> zWt6@4ln}LwX(+`}qYErVrJ$&ZGjw>D$zy$B_N%@SK1vV4Mo;k7>!6?L?u|`WD1Fod zw~CtJ?5zekbF%>~%~W;)D{4JzBTFfS(gu8r+JVwhf(p23;vh(TI{;o7>PeR*Q&rar zJI>U=PSj&(BX*&vTPT8(1}s1wM6tpL`Fp^=r9m)htT!cPlULRRAMGxM>_Q8_!uT4s z!4`E7r3@H@x{4ae5-E^Y&2X=@32Gay(AMUtS?UtcV=JMq z5*INL{C)fLzW_8mw!*?4C2%f>CJF|1hdHQBmU06n44~gQi<(M_VS_#3{Q3~==Z?bv z@Vf~%9Ib+KQwzW18Zskcu0MPcYXr~!wk$SStT20X5&T^sw1j_A<4}Sta!|s6xu}yA zp&vVjH9vKn4<(_wwGEc0l)>SwI+pG%k)yrgNWuuAH9V+jhRAgVV8#KHvl_M47L|t* z2Ao4pp~(C<46uJ$5Jee__msiG%e6ekASVu>E}+U$_L5LOjHn3I2dEJ!#_KP7!RfUl zVStA%t6MUvVAC)3yu>I*g`!$nstcf-Qz6)V{l~`8w))gDOesr-vuh%qKn+241dW>8 zQNsUS9cvVRw|IA08+H-#@6JNWhK<;65QAb>6F$ld#s%vs3N6_5H>3%*MEr`Hhx(7D zT+@hJiQ2|e2u+6-wYEwg?4yI&*VtHVhXzC0xNATZ4`KRvTfZmH-8Mr}RWnbqL25Co zv(0oZiUvA1gB-cy=t|;yA(1?kg}VmO^v8H5#8&&ngz8B(;l3fmW4GI6fBwU2P6D#&*(p1tcCk!!g}B+>bfo3X@XJ9DB|ba9}?!! zV?RJsOB-xXcUo?5Lft^ovb&xxsKxuJQj~Z@qC1K98$Lv-0@Z}D`_s*}0woElU0^gS z2gOb|eDpfDDPa_CU3Vqb4Xtdu-E0w7jI?&91KE2l_pMNBFnQYq|O3w@(|X&8_?|vPf(sHZz3l1&?Qn07|KIoLcyh4fFmJ>?BP%%+ zCXAHV3g{?s-+2>kJzg#Ds|?!iUOdeY*1Tn8gRC5~i!2cJRRK@2=~_5oc@_%Pr!;75 zd*SvHSeaV>MBxdO*$P`vRKt{SilDSc+8Lq}XlB6j@5|ZgupPZ#4^dH35FH&2etv%Z zD!}bx3(VhEtVoxTf+hyg9Q{+4V{5P#e_^u;RnngQ#Et%tq2Yr-hAbXkq{zXYcxC7<*%ssI07nw6ru^0?EwGgzW5Wp2FE+uCe_~ z5UQbx0YqGc0X~i%%u@i(&5p-maeU%S-jKW?kf+o~GXn^NSI4)bK~qb#y<0b=&j23w zTKWv&VXvjj49LsNgN%#}rfb)(@vGX?Wd_iiAU-~xY46^>{Hpe7YQT^|9Y5O#4%GIv zN$Pr2kzHYW+40Lmm2kJ@=^GuiGyLtMArLC_;6=`^u%-s6jhfry4LpR(x-R=Rp+nl` zP0}m6QVsZvhp_*=@cM4Iqg~!^^3WwxyTDA8@P$@9uIiT0^4Da;LJCy#yE1Da)!6|0 zK;xJ#stkuvw7c08L?2#Dtz5~_4#m%?r`@nSi9X1(y-cJ618h(0Hw iPwtnZoW7=T4Z#0a+W-jsK7jWC0000m1zT0!*7imel#~uBL6ns45-CNbTS@5@=|)f*Q7Mt`E)hh!k!~qzk&terzOl|Z z@2~i-$LCTJ_FikvImftb>=0!|Ib1ACEEEcb`}mQxDhhRN3Wd6Ia2*4_BC#Q6i$bBj zv6PZhek>(L_wtp!nWc>>3dQun`-9k{mIqXA25K@NLIY%EsJPYdVf!pHX|Vaf9n!QI zZTdaLVKMYRvq})F_!_1JX#w8-zG~qrEdM#o;HZqK^swmThRgx__JBp7+M1&(Pt5BT zglfH&Tn|Rs7ziSQS9|iucd#V>4F)+VJn0#E(b+~U+A${PSp18w%k~?pY6WvBqu?Zz!>A9RlIL;WoHphriH%=CO#P;Wzww;kC zr}_f(!uxcqXpT%hO+kAH%_k&3tM2(4(G?Wk`L5`AU4`v-prG`R+|*z~wl|iekGMXh zo(Bi{4(xPvGDa0#mwk6FI_=@BvpL`GnrA!$Li2a8(LXp1v-QP6r>=QWXEin3QK=_^ zcGlk*^K(DWx^ey9>S#8p@D-2B*I|FN?{1;Ri8MAw-KY4-aAJw|ood@hb~YN#!~S(K z-?wk=;)=9>#Lg97=u}i!7~H$`kKA0%t?e(9E%%SmQ4_D`+PuSQVOcPYpU6q0E|LFI z>vE#tD>q&~(s4wg?lU0&qq${CxWE@NogP1x!TgO*MuvX107C_xP!@)@wX66p9Y@SXxruEpa``)lFUI^k~a=_1edqR*eI1 z2AHUFbWYp}enj}kQO}dn^Ke8+ZS(uG$kENmcL+nI=|4RB|LC_}w>0+=0Xn$nYiWZP z*Zp?&3PxFGCk!fa{njqL9Oq9`l*0=ODAZ*A{{8z0 z*^JvY(~1}Yui@{Oyp%($P9|mAkKjed#{M3d@FzUZ7<{i^axbA*)}u~eNH$|oaJoP- zhk>5HX1e&yu6!bTqmATZM&~mcU6JM!;iPx)GVUH8s&;nwzt_}6P1xM6(=W*scp{O| zo1J;XQ*NC))`Xa8C+dmhpZ}0cqu!Fht(_{X?|bO@XNSM6!uAVd?+pK9jtu;k> z`npB2J!a(}i}7Fi;-8*`s!Hru5}e{au)L*BVYrc{S-=PnYb26C;pt{ zmSTemM}lc@A0;IvcvZzQvxATc8<#Ia@OW4MQ~kqPrdxzO26(^UfBwkIW~i(lyL);P zZLOR4H~E;4u@?(c;oS>J5em2AzeVt2=&=#CP`W4e^{}>wu-vPyRj{Nru$hQ9j9de0 z`D@D+V0$VO%ycS9%jjl=ZVrq4&glG>oH#benCx~-cx-n7Fk{BEu99fB__!5E|ssu@Lg4 zmhgj1fBt-oiz7QZK4v;C<^NPM7M_mN7<1O*2Y;N%w7ig8dlaFChxx6@E&C5wnw;IhKEPD2h>Hht9hwBp= zOzm#yaZT#%Jv#luY_%^Y;Cw+XNv}zI0abzgL|WRcJe~>#b$!a85Qm@8^5%zbr z&$%$;ojU_f0hr8it7m6tu!tYpjBmpN)8m-_q%aDsAGOjDu4U_W3(6g|P#?+1rQ}Vm z_dfmD&>#U%Ue(5ijau;KgU2lkn-=%6(g{E0#Z1HU&nny{f}*oFS;2a+Ih|lPT`lwM zn_brN@v-ri!r8s(egzrxGVSPB(NR%S9CiDI6&8d3KIaEolV4237NSk44Ya6LcFrO{ zHW&BflUr2iX00bc-Ll{vDbV7u8Y`yuKALuQ+L+{9nwUWS49Fj&sd!bvd9jyrd6Qsp zcd0um`eOZ8n_j%`_|BP#xot$J2|W}8cHOV37w4yHPENc@f)4&T7y-k6+_&Dlzd!XH zCtfy8C z;bY<9{ikA1ZG3gz(yw;epiB~RefQ z`M6|EZ8-aBp2K3hdX0-MQf|59E&7wizS_@<|2sdLUG0hIvHNB}Tge?H(cOSEvpG+F zKGX?&^*>d0J-|Sn=%5U@Wk-$c7pKA(Gf!Dq{ zo$yeX7}nv*^1WGRBRr|fBKN&NpTs=*`;x`Npioc=J6(5N?vttWIB48_=G88@ai`W` zWHYW#ga%p%CAZ0Sew%T>1OfYnkvz5I?G}8M1|RQIC)TH>%jQ;G6})-RzjJ3lO$Zcm z-8394*1L|6&%q_e=%ZUdnZwi(c6kd2!(%Z*!0_8tnfspE)vH%W80qij@UpVvdz>%$ z>DHU4IUa7!MlNeqQZ zdh$d{U7fhKGOa2o&O=#SZz5d=n@Ylm%60RXWR1&44;SYuVEvqwY{C5l@ zaldl50)>=`$sGVHZzV3zh&nqve{^*e`b(3CA1XX5YufVWI?~?2+Do9{i44gr14X!p11cp~7-?{j63kTq{18 zTC3dbX*dNBhTJ=<*VA?#8&ls1h=>Bjz5n@9G!;{gSY487jbL0K3BOz$EqF1M`B>2L zPx!9>-KU@5Kyh4hWDCQ~9m44T2AyO7aLS&6fdL)0z0*OP!Rzc47W!#9M==Qtb$oHU zHNU*PyjwN&N1LcywM4%Tg(AAEfND5*py%f&b#QRVdPVn6t~cbN^jEuSVG|RRxz2kj zZ;m&gcz)*0SI_fje^!Y?>3ghZc`4=&z5DFw)aZv+1;-0zxU()$gNXYA6vw%U1)~*=V9jGA82N>9LcPAOMuGMc?Ojq-17U{l)ozP@e zL4W%;8m;#}fz{8#8}waq7{W=#BV&wgtv3g!&xR^(pD82?5&*WBgZ77-YYid##Ai(^ z?7E3#U|_H$n0{uvcRp@j%jqYVGpzFc)skkV^%$O7wn9^1lE{bTWIPn?(iZsh4m^j9 zCvo??&bJkP^ha7!YQ8o+`5&zBl(_1su$yliM5*t&!Dlm7iL0ZdgYx4s?=ys! zvoc=B;N|VDG1|5+5@5S5QB3@%w)yQ!GS>zmj}IR{9M1>QzK@N?KncG3eO>(D3d2ag z#uJsH^1q?7H5QFw*d8Pu_?PcSlIKJ2B{wDtIlesq=ZtEEPQ>MP{1Q2fOCs021h0zQ z(jM%ItpvIHlpYliujYCjtigjsL$$WGr5QB%+@xlV{5KQ!?}GNKc*F6(f2vU2QHHBS zS%wZ8ZCas)AnzC$s%Yn|w=sUYr9tH>#3k!?#wBhm`AH``uBU;K|mFg(AW`254 z-0N27nrQ46t7c)aOc-esR5>9bik2XJ%oMY%+qkV>bXYXflmd3)TlJ@k4!WC@71+=O z(91bSMm8Bw82YvWnTN8IP*R4v&bVWG9gKzGpj_uU@?(POxbYr?42#jxdf3={ic!j&$>Op*rnuXaS61G}riA z`>O?kVzTIsu(m5gwuCL8_{k^H=IG{#oy@45FKM$Ku|MTx^gC=6*#}V4yL4eZ0X}0zl5t z@Nk*as_Opc^eugTed)~NA+xCwsv~JexwQ0bJNwD6%zt{~1CVZD_wB`0r7bzH#m(Bf zIz4d?k$PSi!ecKUs}ZI<_wKRgw}z6i3g?~jQ9dn;9KBF#+O5jTxmWc3J0YsnYLw-k zjL4SZp9V70lIlCR|C|Kk-dcb*sa@~&U9IDv!{?m`()*`d4U5oYxA*t&qz~mhTAHUe zTNm^6^ch;*qv87ojmuow*m7~PNm)N6^=dE@dIK}$A{W}o45TN%n4$+K zwrob5v^vetV_LMdw3NM^e<-v6@=p)N+3e*xEkKx}-0TUitlN6e)9#MXw{g$U&wmni z=f3#&m&4T07(gEKy1HL2UN;amN9dKO9zN@dv{5;R#y(&4ZRVkZLip|`H1bmCznTj1 z+}CWUDrMriO>x$RFV!XF{zuGr$>;z6eWF+G)RZn0_7c$2CxyG2IXN%j`rIx~mZ1dZ z2(kq4c0664r;e|EsQN}LA{fsF8mrZCHZCcpEEaDW8tS`Bss6cb~H{r z=Z{`AkS*g!Y8GU{X`D~6ub1@j5C%q3X*1!!Ib9>w+f+V%-WX zydd>W@y{FMY}2Ep;nxDBu;K1ZGj1J$5Q4KA4OY_QpEh z?r*yQddzD($veK`f#_S(f-mV(2+P0bPV!NX8L^L7?kskoqyFu$ zMsXw#z^nT%OklHoTi4_9^74WT>x@cm{lf9oJuS7iDJRZi>?&Lvpw2OrA95%LQYAxi zRX<9sQ;bk;8vCfIabaG&7LmkhbOmZ9G%okuJ2PH;Pp`rk*>P7}h9s%N2^z|sZXk7G zx0_?JU*hu3s8t(4tWu8yE2pg)y?C+Kt;^h$4M~Y&v$8MTcP4+W*OZiSybTI^0oNED z85z_v@7HP4j1)bG1!=fRVw-Fmqb=EPfO=>b91eLS{Ybau%LVSk6OK* zwv&7K@Jj4`y-?W640rBawOW4vzor{a?1*-hAp+z$U*RaA8leTwude20PD}8WXQWn;UgXM84H{XS`o@8*W6kut+*~Hc zSP>;j+qbN@A3V;nnfQVRN4FiG3<^cYq2F4wRUh_hsjGGGp*>4i9=M|5k zppDdfdrHg6A-KY&h8lnZIgc666?l4tODPVk`M68NC3I8Y@1&1ffs_#iEjj$V)6-rLis zK7~M(Z?itPYFxJhCu{=SjGGX6w^_554iHtDM_=!@%SX3Gwe`e;HX8F&UYqfo!#PTF zs;Y#jx%G*11b+j%!W56qKW1yAu8&Fu3P?u4YJrx0FU5<{z~_V!Dvx2AcB#vTuC%nY zD8HhMLlX;^l81-KPzSAVcx|m{W_I?%sFuMsm5bvP{7_wYmPZFC_Vf9_%0+q2`>0Tj zUrf7q*#rM6xs10h*VCFmmJD!Se{O6ZYw$iIMfQHOmhA(h-p4{FJnpmh@@z`ZfT{%R98PcXSYEyvYUu#r;ZfzMY+@tpF9*4vO>Q3?WWnBA zL@FViOw=6wMgH=X^Wdm?DF#d>|`} z0h^>RMIsVtQyLtXP_%B5?l7yK0*Wf*b!V;->_NQ)C4jd`K{7Elg)=c9DRI$QYSeju_=mAit?+$l?OOz@(s{;G~Qg|7uAzS!o+KG&fgc@LbFhwk*Hhuj?q- z%9sFV&LZ(2mcE;qRjIC?Y$(ketc}XfEmaiv?TKbqL!q9;vZ)xa5-xE>;x|itpl!o5 zm$YQJ(_)n~u4B4?)*;*z!>Scn@Ha+QE|4;^raVc^8WoUwQ7(ko-@1S)jbPd zSF%Sgj*c0CsnAgM|JGQy!y<`dXq;^&OV8#3Vj==7C6}~x049F>!QU~X3f&z5=?|VS zHplF3`TRZn=(j?5w7s1Q#r4H(y*J7akgn0MDu-OW}F)3$y>fIc z118;*C`1J8Mr-DtuYQ@5hVDnJW^7-iv>8(W%Ji6F>CB2@1g+AI!qN@S@r$eH7iaj zc9&{5s!Nf}xRQIvj$7V>+fM6rBW8K=~MiqWf}f zgc*=~Y%c#F$$4rwFR_PCW8c4;bP}Q(e|>kP?J7+B7%H2Q25n^_&cFvK6vACpO7z95 zKC84epT}hHwv}8qB_?}K?CkGn8npy6%O4?YeRjNa9ago#MLgDpG3#unGI3l#WCQ)O z4rHBA0`@e>Qz+AZx%>NZIIV=f16@I{KT|}dfh3-}IL9>%j6<(K@#Z(FsUrXdfock> z&$U^vJzS6dl@A=PnUkn`T?+~yT9t9N_g9iP)QX>fj{%jU8HmhHGBV7J!RrO1q0m)~ z5vKI(JCA=r0Fg|MWUoG<4BG!r^~a#_&x8TDg+FQE8wbQ4h({gTn;^g-B65pA0k`8F zbxW1QqB79ow$4^zc`L5wu+T|$Es(L&yTXEVT{b2^0Wtw02p9E8L7~+=#XGRHwA8E5 zL_ce5ZAM-`N2B)<*{~?IdWYsWH$qj-0Bo;74{Tjs_6n-Ybb3U4b&#uY)M_=1qaKBN zUhkz7-^`&{Lz*P(OIE@BOQzRVMy2F3s+5;f*ompfj0cZeIFsqdiFHW1!NudvquK>J zW0l>nlZ}A|1L(%oCxddd%-9*bTMv45RLA$CqyOd5WtN}QK*2*t?XQh?oBjwJ3e?6A z;aLja#KYjwuO)~6>G-F|y|8(m^Y~EiIn|6I=Kw%<{EWe1U}?3xU95`e?pb0*Bgv6M zK|0f`OG`2gvf+&Yh0svueThw;TXmQc5)yINXwh%DS6HsbPi|SAs^_U*Bf6(3?d2tk znrn}^^-0L_?q_~mtojWV>xL&$FJfIE)TqYsw_iI3WCiMgVe@%>g#1(V`kKCI6(lbMf+ zyiS(l9?8r5x6D6y4i5nUrYYJ_f2znc7gyIKLZP>@u2*;4zk@D#^6%eJu1ZF9n;1Tg zXmFZT&_e}ZKFsq)R0;-^zmAzYPbh&PmPFY*2y-=Ds3oby9$Hxz|5W)WFpEozubnZd z3=r)@Qc?uySt4x$-r5c?-DTKPzJ+~0ye6dH`juq1q^3qhqxd-m0Re%tqnO}tj=W7%M?(aJl8Z_u@=;>0b zlrxOfpB^6%08#{jjmv%4B>JwLfn@IJ)|mnSqntsDT<_CE!IyJ>UrO23zV4Q%(ENPx zH$z%|ty&cagt^_Foitb;3WR=XvkCf~2||aSH%W=*eU3YnUu4jFNH)a>wB6%y!h8qz z4LXXOn|pgPQX(DHu({#Lk`j5M;;y$$cj)PJnu?KB&+k7K z{3`Ol1#i2;wV7&>iB#deeu>CWpU8n_joa|APuGxb)$ZZ3XyivL8D(X!-HbRo<@zHd z6{D3cCoOH*7D^&73XS~R3t;G>FXVW6<39)FElhmZYZKtOA)B}I+hjFk>~Eg^Y&|?A z6!dvG>k(u6p6muc@OPeDg$;`1A@meT6%h2}m5j(}uU@4i4eahcrPS;T;-velO{G=qdc|5EVf@wO3gB6Yr29m`kw-?4ntDoIf-fYoL^{<0@Z_|lk_ zQ=A@dNI~0b8v1ev*K8MW+W%(Krd4H?ehF|TVWWe!QRvZcKurN?z5UCvpZO&B>+f6i z2ixp86%=mi1Hl_xK#@VM(yn@W+is>duO~4HT0^zdYI=`4I9)%El3T@NS+T4x= z6~#cw1M38s635Gxf!o?_q}oNAiNelUpbR`r5%=C@*YEFR{+|Ns{v)^TuH8T?+FixO zU;x;lf6oKz)2?;n5_Va?KJ!}r6IX%D=BcE5GC;ny=^6ok`xzWiXh8TbeD)0ru9IKE zI}G1iC~TK;zwzY${wykUSd@ik1TY3&Ek{vfVVNk8HBEkZPn;?0?o+5CjZ58egXQMd zaaP^`mjzJDx%N|v>%p`jy^lhgV* zQ~&~tUsYTnR7AkRrsw1=HGlBn!8G7@jN{YOC}6SRD@K9u1{O5fZ(8+{pw76vx!qJd zC3y?2nx2b`0Fb1`&q9h|rj#qEM!0>Yy!PXzx9d;-+)5O5xN=W1v2id%F6L6tdtq_W z0lM(n$#RO@R^5?umOM!`hXIZEzx}4)olyX6!hqs&etID$AV3Do+6<73({=N?7ibvT z-(N8Tph81+$8iLWKQ{PgNB!~R$6a>W<@tF@(BL|tMt^w!-tYL>4e=5h&UUd8@p*MP zXK~tfwryBRB4S`b+0DZvmeP41h=sZPo0up7T}^i17QyQ9wMp!hUYlQQov;Fn=gO5U zYlwUY?Kwj>g5?06(CRSiGm(N#N=GZ}Z;1>`U4 z{ZBMnxH9un?KI*xrG0u=4{+FO*R6M34VN_k6#@~JZ^rl1TfIb|+6rqpeI|v{E9X%n z4V&;LsNAa)<|&8~f$<+?e0H)o2uh_!h3aER6Jn=^;)S1Kfv&xu`M6;5OsoHbgK(3Q zGCwoY&C4NROR!p3!tc_+HpRcokG!S=;1+8}Mg|%RK)KxczXNP_>%FR1G$CU}%4t{V z85nk&FlpzyIT~7BXT5`<{~-_`1Qj_eE0*2e-PAx3R$As?zPeJrvETzp1L~BerDc`J z0SovS4xl1EuW`YR<1ml~(hD2rXjeQ?L!_*g+{; zUA^W52IyO8j=;f|9CJme@StoU4Fc&f2=0Sgl?H~)_Rfw9uFf_%JPXG=izq)p9SCC~ z7qFu;>-qE=UI3{zNLRV5@X4?2MTc?EC$9f$HXR^4y^oH5^LMO-ftmRRGcz-EO~tJt zP@~}_Lgi=yZZ`-nq*t29$Zm%2jM{0E0q5pyF2fW5^19Sh0!*>zqa0TA52SmP;VFuOq7NM4-{A(fctdDe9Ie2y;3h1rnz4f~ z7_;)VW*7!aL_#9FG8!cX?@6uPm3v)o+J0ZSQZrUs%88K7`1p9J|41opZf>TVDPSeQ zIMq)~-bi@>FvY@x2`QPN$$?0`2+kgYbXhbDb1C~m`gy$cviNy2z7B)2<^{bUk*1+J z0+=tenc#L>9l8cGjUtch^WI151^qOw;^s@gdV3>04%b5_%FP*W-tbKdN|n)wUaQz6YTtTnt1(le3+Eo##ff`2|i( zDG@7n9kEJsfORPOtccqqX_cTiXTndx=_+3VflU5i=^~Qn5X#xhXrG==Vj~S3-YfUY z7_~~;#W=mc`NWB%r2HKY>XjhX$!_$hRTe-pV@Q#Vq)MWHmhct#-fqIA1d#JQPH$@d zC1j9t01NP1jRb!dut)Mr;N?TQLK%+MIasVtC%dK*@I3%*QlV0gWIZ9X0$JaAW#IL? zOU=iAG-Z8#nx&q(sODCRm1`jsml7+0^_j6%)|!c*IrusFbDR-J^a-FDIg zmt^>Vbku_#paviT;LQ&k{wUb^@K_@}jy9hHB#@SNnrlRZXFzif@*-uDVoq(2KkVAy zu#8YmLm()C;|)7-6~r>c8?Sz~gtakU69+hy_iohVP7yD0c;Y6Uc~JrxOkz(zyd<*qV- zaM@(~Zj$J926ergN=1r){u2M;G&)gHrW&GhmVsh9pW+gDE&T!a zef;)KKn(7Ee>uh1>X>(a>dO$0_0%zc$hke}8iInLiS?uc-yVcKhy`HB==bka=Z95G z#+I#l+*>``dV8OCj;(u~`JB0m_lGXC+HF08)4lhnmkTU~wc#9mNW}aDm)3H=`7ITvnm-ma;9P|pzW`dcm_UaWkyMFB_pW}Hht+2%a9Rj0ZmxM+}<{&2%@YX7{-xDuhW8JcSQ=X?fQ)=W^wm9XlH(W$4 zD|B~{j_@r8)7y&R7F417Bgu*1F9o3`N_~CNfj3#@z>p;5*xrfpr@ynov#f+sU#lqP z-hTm}sEvyft`4v}3Gw+YD&H<|^M z5{d9mu%!@J4K|GU*|wDEPAgG<7&j^L!)(Ef?CZ>r=GiwvI+36fb-Rf|aHc3Mg8aW= zcS-f1#AMPkN-DdWk`BEmwqCV82sjfJr2=JN6mg--C&FQurGrpqF;go9d-ol*l{C0M z#9xLJ611{nu|D+;9|iI)v4)VE4*COmc`QWihutig*JlrX3L+X8Hp7-v~#q=VYc&-Wqa)qA+9q21^?-M0)bj{;nSC|Myrjan^*i`t21K<{R0;l@D-=Y zkDG=1b(GMh0X8|xYqit*p1saV_Q0}QN}|;RyGIF>k+yw}k& za2|kvHrU|ni(Hi(1XelSwheKBt-wDu!lnv?Zk&by3?d?;J?04=g=0{xO*d}DjzLq4 zWz)GLu36v~X?4`S#qbB)rP!BM;pt=LN}iE5qeHEyA&;Cm+U+HsW3^Mpz)1!TOS{bY zZTGcLVQS}Rr4CvC8<2TYRaJcnWwPz=?XKBaF}$kJqq0r{zDS+rj8_00C<|!icHs5J zzi1UV?^szs%B5y4mP&JpSUB_<8JPf^pcSYW7D~`{lSyd1E-_!@zeC}6dXPGS&Oz;wLu{Rcz_N-TY1PH%KggE zqFabzZ=OaX)2Xj)&=iBq7V8-yE9K+kv){whZwQ(>NJ($N08&)MLss(Jj0ZNK^-rfY zDdL|J%yvr%Mx-JAk{$Kv&RdozVA~?G26E7coVR9@py{=>w-149YXJbW4xaIWgpaSj zLwk7Zsmea*5@yL)x>uSQJ1p_=Eyi%d_9v}NR3a$(RzM{oCLxgl5jYF9Hr$fHrP1bE z|CH$$k2NN?16kFocKe@7k9@UMQ~1RB8d+v}Ma68`@rhu8<#+b>GG^a5lNVIMz0cA_ z%b1Q8t@VY7r0_n;w=zX+KQA6e1S4*9rZAGuf~S`XV(Ksw*aCPT2RNgk+u`BE zn5ki{ur!_si7Wmm#ybE_nalboTfS;m=x`yt3^5e-+F=K_2smFd0Pmd^+i|S=# z=U*yALL(QDCWKloVOw1FtF5gC(Ks5^m*~Cc)lMN`-xZILkcPbl&Gj+l1JVE(iA)Hr zpM-uiI}^7$=@=E#)+hc;de~D-|1H%cB1QyQH27H3S@JQ_dpr3S&>%4aBvmT8mc{|` ztOERy7c!$Q7?p<$ZjH8(K3bF1JSyAU>9T}0?3pjfv6N8Rr7EbyYQE@g=(`=17I=?~ z5R$a8&2EPpR0yxb^N|KQF%P8q=+B??nRCy??{Ba6jf^UZ9S}n?Q39DD69g$WUER;w zxuN|-sBTXy9#p)ZA%8+h@Cw8in_=_jfvv)8_e=2Kp9FheVE@dQ>u!9W4{^0zt{Bj0 zd*7>EESz>5S$bZJ>7$tnfP;WRcCPMdCIh+wvK$r=dO$q> zDB6E;4#;p2Gx!LpHO1w=q~Y!5`6+A{@%r_UbAQb5=4J-btXg)a(2)ZVwq{c%zu7C@ zzI{77CPqfYWjz(t^>~-{aR+90_9EenOPu)hzHs)ZOH%(KSDi0#N`mLejtOE2l|*;0 z0+NdaLV=EgeOT(YW6WzmLp3uqBmKJ&gY?C{@{RQTEuT4!_A`F^bii7OdPxMx z^Y&Zzf_V|gf2(zw3g|}7AE$CUnZVEN{Y7u{HQ+9tF zE-2CzQ?1w|RcxkPx@BZ!gm6qyGIhV%MIgQ&ti6w3il)hoeI?S0EF!UD}u0Gh*hCmVAW05jNAi!2~R(*SKKk*J_TBJ z@uYi(Dp!!7*G{V0Hi^SEfSUIZMwN*i`X~#oTE^nm!UN%ITWN^;iQgT3JF1 zMfrjF-vXKZ;1P}FEP^t-R~pM@qH5pl^wvRg08RuiaeZA~&gC4tuU?IShQkj{VC{%hzh6?2Gsyys2sIqV?CY-@B2Wbn5uQeK% z(Lt!NK(pu}Y~{wBjLE{3j%)h!MP?;HxG>@TH^%drCmPqdu8rJ)C6vyrmqi6=m0l)! z<+xClKOqNbinQ4>vtG(yweD*1Dz)I)z)^Y}BH8~fVj=vN4{0(?h(zF( z=h5aUXH3Z_Su2OYkkFuXy*CY7;-rP--C6~ss)mMhU5$;6MxZ*TgLLu%q+gAE^$=jC zh{cjQXn}MHJX&!D0AxAP6Nivj2la9v)FGk|<>^Vm!M)cFv1ksbfR3*~_>wZV(wH)z z`QJEC@JUSM+sB#uK3E=d>u2j1pC}{p4q$x-i}geRciw~0J&-FPYaf#laS0tg&+BAY z9mFrJ8#mIB8-@cHZ4DY@n~*Ipr6u4BUdUd^KYFCROX0iqmbn9h*0~SgVi~R)eVbxt zWqlHrlvK3d;-ls{R=l7?CKJy*6Ga~NjNSW0^mgBao3qRYf5yw!>qpHvtMXp}J6eln ze|8uR(P)@BK@P5Cxzl`P`GgH*fcbQb_#YIERysL4POWWhJcZ+~4m%O)VuT_ypuhs` zl!fP%2Ho-_BK*0#tHAO&!}7P7(TlRlfBZ5Xlm5IX4tfhyy=X4;uM-M_B*MXejH+>cNl{5Ki zZUq^XM_$K23bhcEasph8gm)1NDBpH>@+mZQ7(B=YDFuPP&YQpD0sa>E+&AQL0&v(f5`Z_^ z4Vo7@zs;v~Vsor(T3B~fno_e>ne&_dwfw($;^5-8L2mTJ$B$Pa5K~np7*5XBoYf^I zN;cIILs-GYvbFGLWo6icx2whyLdib=R_{Ul>Uq^mG{nme9(kcaB-CMIdRnE*CT7+8 zDe~-cT?ny@WMyTI2h(K`p-tg#6ya1t+X66$7EpKn!CSr!N;IfDor?>ny+3|5g0NHuX|$zjahuSZFG*VEdncj3_iQ`? zbaTC&d(e|2K^ufmD{E{_FMjfysP=5P+hR1I1e*F)Ffd{2g%PwJP%Y{Ctrcu5TOdMV z$V&-TqQ+}0T`+4f!@mw4ARG-1%~!9JS70qEeCEAjmA^I=n#+GrHHD%Lav>J$<7GfC zs=Q9PL6!Tf9=BIE5&!B0{3ZFCmypVWpd1R)O5lIBAhO=}oaCLmcUf%Tf@AO&n~W1K zvecT8AY^BOmT!TLxjuqm^a6SXJw5%+|HcZyA2l2=HBwG^szrHoii3}_;qx>h_@Y23 ze=jXHnQ(&)fC~}S-WRJnOB*&m&m_yakn#jy{={RB_aB?O%58^U{PN5ZiMbUfUL;MB zSn)@&dKRXd8?zh$H+`}DyHgMbWI&{WU&zpamcIKnB%FD&yJ_{a4Cy8q@^anKO*XOh z+vdQL1Z3Xv%)U+r;*X3`ozk}i>@wksCciu8$UYTfOMarRAG-FYAw$!!J-62^19-ZWtbTIDp4W`bDMU?(4l$-4AB5ImD5UR=U<(pf$mU zqoUJo{JE%j=xAx?{mHzr-1(4#N!M0XQp$mcCg}X0ofLbz-a7$Q)<@tCwPF;5 z7-d=Ey6CkGN?yx+DnKG+egxDr?sx^K{S_5q(0~!X2Y1G2d@=VIrG@vC|9%m&eC5#)U5`JQs(BAfUhnncJ^m zXN`P!G{?fmRwLeqA;J3w^$M_KoetM^5V{AR7>xf!CnP*U=+jac35@2wz8l5dV!?Z1 zonO9CY;&)xGqcti;yW?&eSYKgmpErI!G2o-9iwi4 z=m`>egdw1>wp0ANWyaUeVd#U>%rjRn;M-HI>Xc^Vl|4L<9j|o?L=FJSjT}TpL_~#T zrlNq|uT)^1!(gN-`JNxdala_bJZk&zct|w;$jDMH<@2ZB&6l(=IqL`lBI5F+=pa3h z*r_0C(2Nf`WN|KSNvbb|=CguQfdsIS#O3|xRVdW)-;xHz(cxrdf^|z|psq zmEMPp)WUxiwqvjq5xAAxMxd#d6KwUAk8k%pSLylE>*hlZwejG5XRWbMyp|u+zW{k7 zN1htYdr-2`WEbq^FEIjf0I3C;BUuD5E7ac;Nf5ky2i{U6fKbHSa&vP-+A=T_eAugs zEZe~UbV5|}V=>O0PdrUl=eD+#5bgo@GEizHHB$6!Y?X+VG!T-t^8g?_055(>Of1Ze z=FqEt194g?xL1&SoQr8zSh9q&zR(&1;gBmYwN_KW#YCtX#JMHG>jl-ol(Ws&lHHN< zMp-zgOac&O5ZR1jXK8~8`lY-)X%}9d43eYvsUSHrX%+vHuP@v)ombhjXPS~31#UwI zDET@zDcb|cvmtSB#Crtc?IFlFD!?601IcDJVG!aLXbvhvZr~w7GPH8{9Q0Jgk%AN; z4(YobVAO8l`Eb2x$FccUMF7vS5nysDh^IEQ_0%~^DVjf7k$zup#zu+_wTqCD@Fp=a zZ|o>*8{>?#gMU^hk&@_ZkTe*8gFy;j@h~DGA)#g(3bhFkj0fiN2S6vlbr6p`rz&Mqf!u*VjWr?bPD^qc5bEI2(1$KA z{BY%(CHfJ7;C?3%G}1C$TvjztUBCclJ3Nm64AmfGE^umtpso9*aOGBJlS-|ph%wbt zcNae^GnNs(Jlh-pVv5bp!-IwbG|-;;IO--TY4%9>wcg-A8!P@@|2KIvE89Dm9ypFR zw(3L-Qz6W1*>iw|QGQV7U%>L_YL}(we>hD&x@Sj5onV}c1Lq%7O1#jcklSsXeybFO zLy^wbrK`;pG=yppHD4Y0zbrs%*5`~*N*%ftR!B4gVxEs4KW>5snw3eSa{jZX|fPtgkhEU@$n&m%mmpjeq0)i6Hh`G zyUnQSVf8ko8cajQQHU=K4uKSeDWZ3KmKiT7CMvhLU$SYJqG0%_O9;k>!Nt_tUe(hU zmcO7repgt<4yi_f)M`}qz>;AjbwC;tIh@-`20P;m%?%{7fhD)VP`u7}yc_%Zb0~O6 zJxL;D;8(Ul5EjBMNG1vi;=m3EaN5=UwXhgjsbrsjmXI8-a$?%vpewiqBLulR6sQxM)-j+-#q_v z4k)Smq)5e=72s9y&4@4$#~xx*)?gyh0Knvi){Cgn=`{z#?{7)Tpb1v+yC)9UtUF-*T#cNY>&`8ECE}6TRi|oS-e|+_SoSjSQwQpJZgZ=2s14tgmJ>inwrzI8Y11{@%ZsY>?oQ|G0tq}Qa^YqO}iXvnvswMie~WFh%s_92BwYZ<1DOD-U}A2* zn3Lku3`U9y+&oI=Yq-)HcQcVPTHtWO;3!lbkr9YbTij}59^Kv-#M=7$ zdiK!W>c2QX2cueu$q(1T21!HcN*XxU;(-ja^TbK(R_`zeLt50dCkv8FK!J3F{9PFg z6d}c{r0$SXT1v->JkL2?PlysuDPB;^YosjJew14mMW*953!Eu4jaSotewgc)MKb$P z_~sz@jU-i))Lj<}UxUMqC@4@{{Zdj=zR+%=UXD5#jaiT6ejt=^1p(`mi0geMKBy#d zPK@X*$WZ5u=avyj;wz&CWFSML?+Blr!$(9Q891OYbYSqnay0`LjsUpDaR0ud!+ju| zq@Z>LBH5eeC}Ixj4tEf8O2Gq1069dBAVF}lrbK`6sjTg-?~{qXlqg(9918BM0C!|z z6lSo_(-rYVVAQt-GU36HnnTX5BAy%EG6=iO&N`5-Dg%5BCljdF9K5>>6hagt8-b+K z22r4G@I~SCDXu*I%quA^eH9s30?`h!n}UOb9{@>(2v7)Mb2Jo;GXT`M3iFA`7+nIK zLg<+_Y+I2c;K~ARPweUd%^U)ZWkxL+xkb!7f^AM5EC}6)zJh{(!W;mX>822?X4_%3 zEkpIKFr*Cu<3T1LAQDFLo7~+)D1jg>8i6nl(F>3)$7|hrfQ%K4Un%66XHm+l z%Q#J9R~+=jH#BFuQSL5)fzwKO?0;rITZCrgpWH&{477j*7R*ij31E_4cq{Z2Fp-n9h%SHlPqI z*V!;{L-sdOV8#s{qJ^N%2Ng()1H3>Ia}ZW-gDppLEe$eU078=162Tf{vYmj9tf8^z z^$C}za)p~U1A!Z(`7u$8Mj%{NIWFVDd5*FlP+bU@b+q5vixmHN9oiJIiWm^XuIOuY z9rycQAnFLx`2gI#u966#tuDHS3vhytp)2j- zDvS0xCl|cdsEDVuo&4Ga^fwHI83;Ea*7fB{zc11QOTC}Jy~ECwXezmV8qDJ)vqQom zoTpzWj0_d8&(z(5N3B^vf&j;00tV$gRmOV`VNhnjbJqT+OAvOVs|=`%o2?+RL=eGc zAYOvZP{8n!)8?;8cn)1XS6Na;DMYcmqbXH2 zY^ztD9t>+I(C^$X&yQjRFW{13onSb>39+34 zgrzE_h%<#6brh++1%I~cIO0Z^n30kw4T0WtAIQvZF4LQn#g6Xn3uc&Yk9 zw@4GJ%KESGMDUNYL3q@SN~fD($3n6ODkEaBprK==Le3TsL7SI_ zI1~mRRWKj}LI^{LE}j6cM^XkLA)<5P|2^>3=4c zVd{&eUjzadRWN@9dizZxq6|T{@kWq-loy1vD?k+rM=l%AH&RfMNE)=pGb+l_dqQp8fjS=r-QxQjceX{vBXd0655y za)c4TZyy`73#>6=m>Y4$K{|;9yti#h1q8qaSh1pW93-Ba#Ub^A1mYlv%jJdaD`VSoN z^Y(Llh~Ii#JQY{_al~l!vzrgSa?= z6fjVT47_%(!6&nzRm2BJN*$Ba)YtvdIA>#*2?Eikf(yo9sYG0G8mB9y#n1QEV1~Wh z@}^dANU!<|zDI*-F-9~%bNFPUwShGBKYd9BIQj6=G9*B7veYggL=35l|F4}qZ0*Bo zc%%!T=COl}vO}U9ejz1m;tQj6Tlr^(d84N?T0Ei8Dy-h(-r{eB{M}`l>$H)|^Xb#g zp%3(QbWHSyQYxi#5sU=4jY|f#)5qJC?!=ONDy185wT4pLkhcYK!5APdM(3W?<5Cjz=AM!y~@tKDh@g+auYu}U= zZ}7pz)Gp;@WYRzt(j9g8GOeuKXL=Joy$2N$q4qtJ>`Y9Jpk#8wg8>!n8VENEAryp! zs%d3UA|GGB4Ryj)p8wgiXSg}4a&kdu%MzDOpwbe+I1eaUvV}^YIMLqRd<7pl@RBMB zKIx>XyBiys?g96-V~9I<^grXf0bfDh7l3$Z7s3A5>&OHa$H= zsRZA>z!K*&rE0)^yTRgRa5suTOG^v&z{uz}3O?rq6SlJ~%DVxOEJDn|>;2?72!m!W zwonHAb&F)(+yudh354%Na;3L`plz&Dzsf9`X){U+?6EdEkgK6K;wd*x2aG+4a4h zokqV60O7&MvUEppu*6^iUm%B1U$G!C=32Vz%g)Kqe-}(KL>zhjx~buvjR%8yYKNDX=!PJjkZ*0BH_&i24Frszsl)#5$D_09_rH* zOizw_ai`?x%Nj%OA9C^HUL&I*h$)}GdX+WN^gB3fcK)vv=bLxfPoF-m1vQuxa7Kpp zMjKS?a|8qum{h*yuG(Wz?kzB8=HRgG_pe_B{xNo7_`T@F*APNy%8Ga=W)3$Q>n>e; zIXYwbouPvA@8TygoBsd$$iG zH_LHZnUb_>HRLCo7D%`~`LMYfggK5N)b1=^m-e=v9UU<=HBD-0Xjq9h=2?9`FJ8iP zG!g&GCRkEsNY3&BU~Pf1TirE(T3US2CtPUTWOekY1?I;b#Tq4u1kyMfh~afK1~ij` zMCAm9B;Z6gxz9!=(u*& zylHIQ($Lf-(Cjs_25>gHBal@NQ6=DSHifI&*hm<{9S5oC8BRXBjAwDOmPY3lvKDpT z&o=5VZrI)u`08E10KMIDeE>x2u()XwY}vFBDRUG#(9F&*n@e6`=~CA4 zHnZc$Zz-2xFp=<`#J;d{sqtS_I(s&iHe?AJB;R% z!$7R4_1Ldle06)DxSiP^t>cd%MJd*8_wL)9^fxQurs9sBbaqw+KWHrLBzwp}Cib$_f!o4s*K%}rcK*H& zkFdivh=-R~@2LdOOBNFZ<-tD1D&Zh<#p@!jY_Xx+ejSfg4mqr7oC{)#Ka=s~@#Ey| z>_fLbv#!>*J2Xz58rl_t z;Bb&9fh~7`zPEaGcyL_w2CMHsg3=~B4}Qpzy}4a=&z=a~1bKF#pp}5Y_{KD~mDxZ~hFJn4 zU4o~AI(?pqAG&hC%!Z`x?g3Dc?m)VtDjh_%+CO_AJf4n_9U+;gz`umFw_82-n&QW3 z%=z%}g|c{=yfP*}2Xp6EIgyJKer@06m^1g1C2~;*r5E`{-VNhaRaLb(eq3vGM7`nW zNN+O-{XRDO4W6JbnqK6(Qwg`;QAoOy}Vm$EPO+6szADE?EFb21Yw|B`mGw{eOGyzK19ZQ` zIggi{+YD$(s(_(|RT|#F-c0>XPT&0kENZK&=HWs!e8vX5?u|9p)o~$7SZA(7w)Cfa zgXTl(NB7U4AvjDDGY=R;iY)Wb&sSOxDd|5DVS`qK_tpZguakeD0dv4aP>_Sp`-7_+ zSBOb9KJB>dQ#LYYW80m7-~RJ#WKmo}vu}^j5%5-dRWn6ja__-|A-E}K2#C7Lh$32v z*RIv!?tun)`sq`V$&ry6W(()&qBa2mfy)>mf!$fO!x1B6<1@UA{%{*)c-!8SDEOF^ zYH5s69J+ivKU~4gnV)Kg^37^Aoo2VnHhQ&6<<*3nMQ^bBX*A+XP$F6~>*O7N6aewt z(+?l6c>c+=S2IXA)unH{$jr>&Kh8bW#?Dh)`@<|0-c{W$~p+Y*cgHQkt8 z4>^!`B>#d671PM2&t~4BOJYJ=4%nP786Cp&I-}V4-o1OqxAk@2VHAnzWzH=XlXlrJ zp4NZ-D29_LiokgC)F~BoCFB-|DJ_*;Ls3}B0SvohL&G9NpacO z*qFG4gfch{z^WU#TT!aqDJVG26Loyj>jzRc=Cm;6fBcp{hmazm7ZA=gnvW*??ym!q z(GBq4@T=?Lr(h`f50O)=jL4DrQxyO0Y@s9GdS*sI@N)!f zS#h*drJ$f-Ey7kqV`Gfex}3B$)#DE(!rN&4hGtw^ZYqz(Q86-!uA6_aptr(k$T#wY zk&?RTz!bveQ`X;vF$~62AYQLBa!;Nx zGDjWoA%4ZE?&QWy~fZS>vPSR+9K3&!LlvfZ{NN} zuu(?Fq|1_nau5T-ErCK_PYEQ7ib~CQS;EWP1{WJrI)m&9!t*l&w@HIR!15MA7ZJuC zIAk(f=ik^l+rie`JOR~9E8e$W_D2jpsiQWQ{LzWhxU!IeFbb zep~{u!G(q`=0Z1GQB=|aCm#p@eRB56YKvQv--YtMK7tq~d>A9N^y`9ypCLmTzgq6_ z>*6{$)lD%H-wx=kbW4I(MjL=nKZXTGpf{X(kQ<1xJs{@`Vc*_Uac59#X@@ToRwR&={@ADtI>Nr{mY7!K`3aYvj%_U*1i&vQe3Bv)t zqoH^Tza2t(FwS#u{&lZiOGr2*6T4Z&`c?nar#r~82EKMkDdiwC$wH^Rs;Ui$HCerY zx2=18&N(hF?#ld~8C~L?i9sFlUA#8!Nj1RVjMx^0l$EIv&W4JPkDq@LTm^HC@@GSr z5o&c~bA!-!H$P;7Hh>PU)03M~xW7HcJ3nk3VSfDuH-I9?F&u=lT>9FzYtNhnaVL$s zA0qvb79S;M-|x@siG#pV=@4{q>%looCzb^>18BT9yy(Txl-1NkaM?kE@?q1$Wd|%c z*AKmU^X4TE!KJu37KZqmHQ+hdMkXcY?OhPvwh1*hjoEgib#rd4__K88ThJSIm3O9P zfq_Q5_m?cLUd`CGYgce_@#cJlg~Gx@b%c{zJb}@%u~r-u>FvS$GSv_lUv*ed@%76_ z_V`^1n|Ian)AQ%PJmHF9`SuTlaD|}#vld7of$*(~9aT}exoc>ooMFwibJna`adOsd zR4#9G9}b<`jrb)Z7(3}Eh$tCwFqcG)8~4rCzQuQ1a48Z6`T|JApH^4zEO1>lv$==E z!Y-+SUt4RbgnsV9jTY!MOsHU#lFI%=P2ceFFu#CAw9P#~?_$v zJEY#rs-Rf@@7s)yu8T5#9WXRF_#Cz8=c|dav4JS9zvH#TCT%lyEC6G|&RlUc-3$4B zD}uMtBbPPcSS#Oc3ziu6?@I0AFPDE%6(&#>qVlI9e~ycbOHM@Xu5+z7epD@qAG+QAybPGj#znceY}rzd!WNIm zwXbE?PKO5;6>MO{@0-Z}2}up~tD27VEI=ZsGnty2n!GQQU}VUI2@}pKnp0Y|bjDj` zv@m4UwVr(`>whI*nbHD>^oMczsd;&OhK7b-fckU#^=S>d0RZ_V8N=}j(0enW7$lp* zC4JN^i0c(MP@qlT)7w7SZUn6b6SR3417Y_)xMxpZ;=^B_*Ji4G?k}~rQ1=WC4Sj*C z#pu%O)YKK}85y!X_f%4L0N8@S`#i7*3)o8$4tJ!)gEeDc@b~W)7^rOlHVR6wP{UlQ z^&G4A2lOuHkPs58hVfzHqbui6T)ll;hS1%Ti9cJRVm!b3vBkn}B7vTBC?$K(Op|;u zCpXtu!f6}G57JHFxqm+p2Moq6A$Wtc8P)f%#>KrTGnqesem-@Gj~=P!ltO#UN?kH) zPbWzDaim&67D4iI^z9%H-pQ`F%NS3A)f*k^XLm4nQplfp<+vEd;X7ap_TL^Gx7}y7 zfU$G4TX=MQxr1iDA`S&}cpJ!irq!!gN$C^Sz<@lCxtW>XH*KD^Nmv5aH<1=qHkbe(K0EJ=r(r`+k5=% z?&btiBdF-XCs@^rL@ON?s-%Eacz3L!re=?5NWh*GsOc6tJ3HTnWlh*Qm5hB5q@H(p zZ6n}(`tc`_ZI0g_Kf~`;pRc4-yHKZPBOI+@F|q?*Oh9M%pZLlxprFEhv+sc)r-hBp zkDqOAzDV!j(H5c!Bnu`Oo{@XR&yP}m{{HzY4HjG{grr96Z{NJBiRlpr1G+g?uxqaz zeg=OoYTS3Iwtlot-TwV`FO5w{el!rF=W};<1%jO~#IR)AnbMS%5K@oPl|Gxd|e zdTMhIDGb55>E5xLN(3yB&g>|q5w~%=aKU+qUqPBFyphKlRbo-b{qWQKk)QQ>5l`Gl z(1&k3H{kb|L$;$-{@tkM+?j){5~Z>t1?&B%*?Zh4i*gU-EeFdwJ7eF=-_MbBf|s!7 zbr**GzMEw=6ZF}=Y^>_Viy)lkGlsG%|EC3*3q^Avq7lmaJ177EGhA?7J*dDxmSErb zVseJ3F+3$ZdnYbI5R@X6n~?@?=}Xd>H1sF$f_A4=wchu=V>)2!ZkpxsfA_JO$$@Ee zvpBVxB(H#_0xRQxcCluTj%%-44tRD-f6!nFS^8|K4$U^81YeyD%4uGc{hCCadhFYC zTIVvTvnON@VQ&tIXUS8^?vf5JDfDjjh{5P zwbh~7VYJq=JnO~kQ4cqa00v@B*}v*bdx|FsYS0;e&^z64AgxjxmV$omZZCi+-D_KT z{5ZdV|9$~Vn5TGa4<9~khrlRcU#3-3$EJnI`qCe@fj=9u;EGPgKeWduM~^ZG+AoIA zZnUBi61ua5<>VHjM=i&Q3KuuG9}Ia;S&1?T6(;@`29@!rLcRU~6>NBTMhb2Zp@6Q< zP#_RhNb(Y(IA%~5pcI}%GkIuk&2f5uCytH8DDH;p6FO_gt9S4FSnr3taBR&lpGHk5 z%@9UOj5#GAKz$%-SaDBWq|IM!>y53v)OVF!!vLE+L#u}OCH*ymPekR}>bZ1+UcY`l5~FcDOEo75 zrYCb!^u$R4qr9@eYrm{qfLFlu4}=0cyI(E9W%92IUhz`|Cy0vSB)EJ+{SZ)mftW(O z9pF|hcfNTE9WhkmecvnQLp*V0Lxg-!Q{%patUU+Kue;uyS$p&dYTHw14vtoxb$0ee z^aieUQFrkoyRzoi``ef0O%C4A&)4q8;1B7a0NJFbr!(0WUR96lX)YDQdGbZo63`>| z*ig1f?{}kS7{7m4O_FM+z6IX?ox68`$`nnr1=qY{lJ(yp(kS|;yrg<&H<~u5-wm?3 zE7Ypb=au;&F274bVYDi_*6_d%d)1qgK8MMli2LuG9Lyt+!XZ0m7KxrNWFITrr zWS6~JKb>ZjRgO!zVoSHDJs>KAh*4(E5nue9Y3|%IjMiq)yjChIVp;XSo2Mc+%llcE zz;>AMS;5INr_Tob2PSKB%{RR3oge&b#F{fCb7#RNC358^w-eAl)S@H2lzsiW%mZoq z{)!d(cg3Bj+58qhymxOs9;_blQG}-}F)?!AH<9}WfDBB^Qj7W6V=Rj2skDqtC3Fn> zGB-Dw{+-5su|N>~T}X8XPWy24ehm+nwhflEI=6ns88Cvx-4O?OoB6-{ef8r1t6V_Mewd(|EnG~g%zH6 zsC^z#b{`-J`Kae`gQ%hqn5fXMD6@QVG@ zTv{{GJI7kU2`Dt-nwSRTIDGi{vSPhl;7^5yg^nG%M#h|fz9(qo0mfW%Am2a=oPzat7hwn*U2A( zhL~Z7SJCk<>P5$?@130i7+ZlKC?EW`DT6w}a%cD~Mk@q2pYqG~hGz!JjsbHRGp%@% zhXOF!whx#$i}ur3vs*iFW0(PYw*K+IcA$)}+&f^?%zNXsaC%Od&6*HY@dU6Dr#wB{ z#<9(FoEwn_)w()>Q$Q8aKlk^ws?zI3HpdVxGb#3=^!N@12P1i}P`5+ZZqv3Tp_z?t z;X{R}OH*-I8cIC3qHK#lOwc{begdZ<@5?`~v)Ek3y@YXLkM5SOTSf7W7=2J$wSciS zOPDMWf7Ec!>vwHkod_C?a)6s;hm8)J-teZ)n{7BB;Jd*oSd9`j3L=tXp)?J+eED)f zP@v*Xu5;hMeUsi*4^03Kujew~_xI9PFMgOk`*YT=jMhiz6|r{s5hgFJpbpHr(}9u~ z+W0xLqC2}E2T3GIUcT2S!#UqzITRE=O7GK<>=o01f+ z;ZoT>*S7epoQIfoDcb#yEb;JEeGWkIv*(Yxw0Wa*MMeDLu3n9T%=bJh6OU0&05znx z##_ZDnw*-d!i!q#w&SYkgE+&nNc%9_!=ns5)-(w7d17<64>xP`C#Hf{jh)6;m`W9(M!wNIi{c7zjsd+ zIdmNy4D1{3%!1&AfGE(7q?d&5<#_v#o42H`zA5c0x+pQ^x6<$W_2v2Z?ma(X>gk`H zEWE@415Kfb4AI%K%0a6>i!V5Qlk?Ex<|UW(&^kVW;D-$U^z&CMFIKTgXjH)GUx`wM znVFg3_D6v@+ysQ_OGa0E?5TGYfk}?6^($`uY{M|leVo~l!phJ5=Av0)Ya?i>S|#?aUG`K;8SVKy>156U?emK z1{8e5?cK0DlHKbfs)3G_dI6O_53FNJu3G6%J68R<6yI7~&!ZeW-f(Y+@$M;9HmT5X z@NIW1&^XO9b5F2JL`;l1KR*TPwVu zNOqo>$u5&lX-UOoAs}s%1>*`6mP3g{0YoR*C_a~o0qUfj zrh4q`=C>XzRFBgBkM3KfcuLRLKW{FesY;;yv#w;elkO8ZX5aH#iGob2@+(^~B&r|H zJ>)@Sn`q;~r%@oq{iwg{qSllzUf|Dt;3x}E`%oeDAyFCjDZ8!lu~0L`Np2Apg;I-P z1A?F+czBgKh-$>FJcvtr?fr0YwW+>Qs*o|vV}uHBUWDGr(J=xlJu|FT2o*%-d?7x5 zDPbg2aMjxobfCm{5P?1{6;3e*qCj=$>fI202!YeYZipbz8#2f1YCsHo0Yw72UjBxl zsppl-R?PdcU*#xIZ2$MEkJ$%a$KRxMy|ovx z?w5yW=EH@IM#%tcIl)c=K*t)hNPR&JE!W07qKHmBj>oArPF#_s-X}|Hu${$q=Tc7am z*M8?Nhq3lgy>J`b8FTQ~-OPS8y4N`0Y`ogsTOl9DDshMx{R#Wlty}wox3lGs88;mi z-zGohnUZyFW(qAp@`DF)>NmA&!$VGOZ2J6J1br%s5!D?#XrOoL4|f0@gsu{_;p;o+ zcTZ1iOA7-%`iC6j#i#@PG&bPuze`b_Cv93{BH9y=k{RATxTtf1hGK(G#NXIgMe&)t zMwPXUZPVe2jQWlS2ZYn@bDe-4gG-3p5>vEBZwc#36tdC>3s?}yHNe%GSz2B;J9<<@ zJb3jgy-X^(u3cM(=^ct&1fO9X|Cpt6!|l`3Pu6S?F1jklld(PB@*~Jw1h+D1qBqFT zA?y-QOx4xHE!;!J^ue<(@HI%6hU-(&CU>ZDM>8W2&DM)41MlGi$@MB|N(n%);rz+w zKN}@v71}G3e~~)~bz8t<;M4j>jrp`aRuWmjL_s=B4>X#z$pcR40_^9s1M}*J-^_vq z3xWW?AR0@Yb@U%TX#M$xD=*9bhb*4eXM+n;rwS24jh?t|L|yUbmnW$ zCOr{90!K)ljITn#89Cq}I0-8$3E?u(<_x2izG(eWO2iL3O;4Ufc#i0h{MgA<>v972 zdnn^Mg@lNhgb?HMrAq7uqdf<@yLK5Xp#~ErOc4lu8A_Yay{KrDczC~OKc#aI{ww*` zEf)L>xL9( zDG?cz2|?D_s(HMyvaYDW1Mx?PKuc|K=^zx{fmY?nW7qS<;v zAh}>y&5_@2F%o5i7XSC}-)1LH>?Roj{a*whe;*$P6nW>b-NcN1alWKrn#EM=in3k5 z2g`c*b^rRc7+o=8Kejl%_V)IRj_1oeq>PufEgI;GgMLDB|B-ikkWhx=^m_6oLImhuZLiJZ_S|3;&|p&U|;#p-_(~R?Jijw zrZU@&&1~JW#R96=hC(~uonrgZrh#mx=?;LW1P@Bxc)5D2P<^XcsB6Oa%O%Yyhp558 zqkM|C3sEyAzfuWx8M>)*K%%Yf?MwhU>xPy%y2}VEG};Oqq+8wYRt(B0ZN2=(B2#wM zIVdfSwz-fG^lcQv$0jTrW%-8R zY|sn6gK;EzT9cw3yhz+FKhA!3T}@3*LROH)Eh@Inf~ym!Tzn{oE-(Dud~l~G*gOyx z5TiXuB~MHM2>@u*hAbqOFlD9^{ODf+zrk|`JJf%L74-`j2>rN0i??XU2ptv1NeF^Q z$H&VW8sa!p<2$=leZ1e6h4qAdI*1qk9dsWeE!rjF)vMdx}!mTh@3Q$3y%uR(nE+ZXyUQAf|U8BCl@vYbO?@^W1j|F>UBN1IsGd@7u%v8v1=Rc`ueKtY0sRT#moz8y#(MHs5PdRyCnbjYWhKhm(^mtZP`o5Ds7-PZe(+ z>y60zkmz%G(1Ci=dRsNaa7g)7&^u7Y_e2R<%<4Z0ty#iJ{rm&y^-#n9MsQNU3FWRC zB#}8Mk6Onk&Dh+XvJ7nS+^}K8N|Yp(aH_nWzjSBNQK8F?gL_yM)+6jxvJ9YQrTtZ) zPV9dNT}ndZoopEufwyM{=?s;?RFEq%8H>8Ct?+6q4c7Xe$K_aUD&rC1X!>s3s9;q} zK>;_}A47Xshrul!?M0!h^H3{67(Gkb?%Zax-d zh7ErxIyYE!nolJ`v%4*5*#YScP4&p?j&!^yV9RqD+q@?EFlz|58|BL!3Q+WkGJ}#4 zD^I$(1YEp09}lawyZgm*9Oy9Axd4QjNEwbM*4TDCV-zMj!m-sa(e*H{@We#G3KND5 z=*F%qd57bR3ZsZXAm`6=LvxDyJq@zbj(wIPVXP%<_3LizHJIbA`Wd~cE2j5>*ZS!9-9fc0-r{rc zS6qUEp}>s3JaIoD$_pnJa`!|_POS_TOelWUAf^U6|6DMFWB`?w-%*L^wqD0^77(OUqlpza6Fa$YCew-MKJ9+kOAY8pkdPb$&?Zb!C8n{HP z%ljcyH5S?X@Er6e1du|p3J$CVoFQ?dWE_n11jgt2>(`-&d7B@|eXTA81XK3^v#t99o_@eUDRh(>Pb5`L~;E zgtr}=&Ih6pjySsl3WERivvHf8QZ;%uOrMn~f7|QeeB-|xBiy=%bH6v|z|AOsnMz<% zbi|+;1{(^m+z^n9c7B@g^p^|qPZq2jGO@Y7SLIbGTrjqO$9V$~U>JHFVVmE7F>Bq1 z$5|n=FITUq&^qH_>*Cfcy=Kkf9`p*Rwo&9@_Z|y4jZt*eb&!s9o@9%dlBrCG0vFew z{NF^sAc2y~4!maPt=h+(-qk`nD)#Q$cc!TXu3AG2xNOL_)K8;FiY$i0ZB zwhf2Jb)cP<(Sp1h8ku+NnxadeYRf;-4|;(;oA17b^b~SNh7Y6)7jL}Hw6CAuquOxf z!;dycE0Af6pl~3b7)Au4K*2T_=juEeb#!^%$g-zVfS!?r!qR%^T##;bkq4<0ADgeucd8Sm~brBKgkw z{sj*23_nzT`#7&<#1|3D96dW!c1%D)`~X{F@*7Msy1H(rN&}mb#^r=GT4Zy_e*Yk0RW) zELC0xE53}?F4?8ZB0`lk+Cq!sy56sk0x@B8T+Q!Y&EQ0Yyy-8`!lX#Eu8F=O1+vuu z>736J3OT$vgiI$-o~%9Se7h#hwHSE)b6e~R}l zJDQ^WQo)Es?eJ1*>FCHXH`-swJCjJXFwUVRs#)9!`Tbj9SVXw!!88pD4CMHVQsWsKS4drnsU(#U)fhS%QU0a- z_s!tdQokz7DJgnMEaqW3IKmyq*Y~STpAHxka7H^9pps%HAb#amUdYOqflOyU6l~5c zD}0JiL-w}V+_h0Y9#|i_xuX>=`}nc&i|67j9;ux(S6&Q zsnfEsNzGdEd9c~%?RnIL?Et1Rr|_wPuk(H`7->jl%PYS@ItIJ@J^v(@qBh+lGr{Nj zJwpdWQh6%gb=B3p+js}x5t&*~G8zaa?Nro*lJHYeBg;t03J=M}eoSlqL3P34Ye9G$ z{Ik=!l$BZP?o>;Tw+SU^=+W=xU}LjLH>1YE)bg9bg1!S2;c>Oyc{{YS*t#}2*t(^# zzw|zwu%@zkN6Y1Bq-RDWL9$tmhe@C;p=;v5#OElg)9mucfJa41TQ}JXhv7$WZ_SE+ zJQb=<*fSoToNlk*%q6v8Mp19Cq%<3b4g4TyZ$rFg_`voXNmZ_N*C8Po34Z=h)#+Et zlEt}7?ftss+WPv{D9n#Bi;#IMf!8olP0gB@-zCI0+1w%^3yv%u0ZiKZHBGXCnpHmT z!}304o%iL-|EC4eSFZ?&b1HA%0dZ}eUOA{s!;jNA1iBfEL1Po|$hfZP8G_QJjr1Aw zwu-eMdvldp=hl1!UO#<%Zo?~!V}=!4-G?@oL`557U8k?G)BQY8neF<4&w`llWzKdA8^y~OeaZ+bC1#vnLN&ezPiCvDq9W|Tl zXrBm`)!n})`**R3Anp^Uaq;5CfK%bU>von;JUXvbLSrAGCjNF0hJ5nDe1#08J;o2q9=e(31IOWmh5r_tpxlN)2KRt0SLw?@HM%2Br$XML+euP~r~Cjj2hGeK zd7=U(7S(4lsTwFdQ?{!qJ19CZ6+)<))B9wn>4&4M`+BlMJi!c;IE=7HP4gse*B^`jL;j*84<{ZN&-GqKI*sT_bB8m4mx%V zf##}J`&e)iY*)e#c9p_|yL{w?RPdQ0k_XKdfsxNkSoC@c1??r&CDl4QIvYx|4YN0f z--O_co2{bRZQ74B)i664T@%JAcSAI)3!j`9pmSd0@>rui;jh<7nqQN|pC}pq({bwz z2XpJ0*=93`aD))t*A0Ro6b4Tr2G^sN*rm?R&CThrqKwwu+TY(_?ZAOpoWvEe)xqb_ z&x(gK4dN0lJbD&JnUkO~c2>gZQw;7q!Z_(G@%%)gosN`<);Zcz*PP+tU}m-hC1v$B z(d~}mZ9(8{pk(j(6HQeS+%utvR)ZzwP8=asq&Y2O`oN9Ku3IMzSXl%;&nlc;xsrRk z^3-z;7lRjo4TcXis(~bqlHF1B1K}?navz-W^Cty0QDtc&=d1zJoel*Bg=-ry@FISZ zknRkCYhA?4tBG%>t*6HcN#;thCu%8rk(m670_-1&X%p~1i{-GX#{w7|W=^z`@pS;{ zuUI_12i^NSD3r;9m;QQD0v^WZa|7wR8T+`X{sh$mA;1*^AAy_r0CKwwHGb6f>$@@9 z1Hmi@N#3Bv7D04sM;+UaLa`nF!AdytYcvHE74OTOjQl$A#CHn;HD!>V20=>|9268; zS0beb>>KGx9eCwSpwn8et`9=nHmSTw+O2KCH{-29^#_2^TFlkaM+bz{x+(n9S=6ZI z2p8hqY^i$ULHjaSA{wjZ8eYcKr5r5f1^~XqH(XT-o;|*$Cr@ZS0N>gB_wOIT;Lj_( zw$IyEweG2PeYVYeI7}%4gE{2~8I{JW$;V7@WMzT!12*@i?+o48OBNCPCo@>y zK!Z%FXshTT0{;xg3i+HncOtvHyYCD$J^f@i?IHgA{*|~1Q-^K&?%EEFlH3(83wvin zIDkVX8V*h=#qKPWzFNrRgg|Qf&Sh^Hcf?^L3n+*j;GD}(|JfR@l7a#6%`4$iiCw0b z!Ra0!y?(kU*=4VR4@!A_e=Deeumw#R#gu6D z)vFcp@$uN(8;C%C3CAreDQOQBCfJ#|-2Hk?j0XL89i5Op2)wQ+P#-Nv&F`e3tF6xI2d>%71H)^Y>^HT}1|2I51p!cmBk=W|bXPb`+uz7B6b+_=0ygq|nYIDH=(h*T%5kcE11_vY&WNo>U z-m2cT=r>CLAvaUB(FM2dZL|@DUHj;o; z7*$SGOoJ`ia3vc^UdKCsPL&P%dC}UN$K;RiAE-xf1aZ+k2E>^0nK@(U^2VAMXW3G9 zhN(^;AVoB8niF)jq(m`gsuLIk@rIxvXdC6;>bolpBK556fdmsgiHS+<9nsCuH28}U z0`7Sp11~2HW0)Q9(rPu)e7t`1hSeZrrI?sG#04bspvo5vIT0G91Fk;*wx+kItE=kW z&m1gW#_-+w_;};u+9Ts8cs;Z;6yUNNFd2q0Vn6yn@;@Tk>R}U;mY$w41RxQ>y&$}G z#r7DSVQ?;Az7Tac?h2BW{SMH=6YzJFb?N;U2KxA(ICFB2&nJ+SA?W(PBQ{8vZp8xw z2!mmCHZK`NFVnHOX9tEN_+)1@Xe=^}o6W*@3ly+B4{pc;tIuCJZ)L^XN2<53XOOuf zaHQ(-r3_MFK_B!yN^B?1X9Jj?i@*L9B=tXb0pdESS(7w|xWxYY`7f2K7F;9IBo%OH zm!Et&no?{IV&F5g^T^KhH!s|BzXu{{MlJlr!U`czDkAwDBN-JwF@k{F@c)}T{UFt4 znfJG@(ixGEiT=D+hoAdeyP7v%JHIYtj))&HE(6gOYe9UF8Dn^OCE(G9HnNWd+)VQq zqX58-`%jsxX&CIv-7yh^9Au3igS_wwR+5nQrHk;kHP#fi~54$ z@H-57&xlWrDH84|kZ36k;vKQ6m^MAfZ58>U$*@C!HNJ0 z6_PI=Ij)smd-!UA>=_uw+JlIu~JcKX|+6jl&ggJY@GovKyxwFx`=aA)25*j5HKgxFq>w65nqwh zG-_jGVhF$_?L`v$*9u^#{cvQVRYC+VBV*cM7KX+FiuabSt}-$*3U}08w~x6{d{1Xuc9}n=Z^6{7f$XIYgs%Ab=q5pGCF~b2*kTfN7QVp`rzFMN`JW zKXk?$%9%Zy^f82r7fADwD0m7!jh%}vsy(W}G7-=);$Kxex8(3Qj2BXOgdQG7s^J<3 z#q$S6{UR|Y)P{eN@k&hq?}NFtW`A5l0)cVhQuJ;5&li2zreYpmRkf!RHF1zoTEtmj!{=kSR4()+MijCiIORUD>{w(u62f-;$<`FSBbtXzxt4- zW*{HNTgX}uXAp!Ec)L*h35CralV1iWEw=eh+At0Ocs~ySF9S;tw&^Br%1=C~H<}nZ zaPEl&+y`XkljB1+on~UIR#`xk zinga5Fu_wN!QI!lv8L?Zjy(=HLQ`KI!_SJ_cwr(BdckPt6SsagLnRK*20A9|@FM`i zNx%(Vz&&|Mh+?60qKOMsWtO0@`CwOs@efidd^o~g@Ztw?E9h#G4|pa`h6}c1w1bWD z&#e;IihUry*Nns*7^YVSi{o?mg4LRX1>*H<{VLl1rqu1PpEKn=5fHpANn<}`ruPNw zWL7Gp7}A2u17b9zBWI_NiA}OTjA0SAWT~^yM!0Q5GtXZrfYK819uuP1zWXN*LqBmf z%pdH1vYguP$vj#|gZANYxP9z+{Hg*jesZn^jD#kDZF<5W`b7?lTzrn8$Tx`lisaFj zwZiwT&ZrvFX(o_L(9Iq%FvV->#{@F|DMg{w;dg03)H?QS(2pv2x+1l%mf|hQ>*7Ap2c$jPAcB&$0r_F-|$d(R%|}u)>L_~HXt7A+Q5SW=PTPKN`iBIcoI~gU8LsQ9FrT@PF#ilWXW375~lxVs{#M@V`pFr5)99{LD_MR zRy&LO!*#NLU3*F=WZz6R{z*Vg$cHGr1ar{xzyM9}F>00zZ z>G&Na!OlM5)cb8a_9?DczS8I6-z78DnlYyTFWu?u1YOeMS!1iBsRB?DHCZc&U4Ibn zI63_tv8FN3N6H1aD~^_q6)o!x2|7I8+pj9|L$rC*4=fv&^5Mtq4tnUI%u88v1rqDK zXDDt$B!ZUr>6H)Q1J$6h*%5q#&HU0{S?hRMO7!>jJ%v<&g6BW}%z%mg&D%F`D$rID z->VEz0FWaT24)N2p!Udj?%It`Xf7&a^YCHNu5ZJ6mM;%SOn?zV8iYKl3kyM7LLA4! zaP9xj4Bgi&!P#KINc9C20~H?a5`=e3dm&^pfF!Y4d6sOTgs`yBB|eO6;e&^`Ib94dQ5M_U~E(ac3EdX7UPNFCtUiIBH;U``=m2T8Gb*DjGuYrZG< zh)D0&eE!AXj#ca%2313#Z7!>=jU-S}3B~dU+rrADrUw3MMF$q-%T}5?BZ_I`gVMYd zT6sI}Gt&w_j#WWCU*?luo|e7h?C?9M;h4b$SsAxoN=Ca&0mK`kATz`(kW1JW{F>O! zBdUMy7r;~$qzJwKxlIsQBTUh1#sFw+mUKkJfRDsWIENitS?E#k{T%}N$uDuMe4NMg z4W{&voK-PyL265s8kl2T0N03v6=I7dSNnNt*e{gdxKRw=RwVT>%HN588?2wDVP3NS239%m+WhXTkDy^7i9VJFNbo)(8WOLL zNG=3t@>Rfui0kC2ETaX-j)j%=87_DH6BB)XKd5h@Oi3J@d%CCo)3?bkkC}qhZkF+L z4qP8jFPv!b*w%VR?@=*I5xUBk@J)d`{S%g8YwIhX151`)L5Wg{QDlE9z!o?fn zC;0T%$g9&RD_BUmy$KDng$^tFXa4hL@za@AqVr_MKp5w)*#_u+3cwA4@~3eUAn#Bk zlQK!?MUo$zQh{EAZM376t$R{$JFB*dgLz5W2yjMeX*3C8IE=U(mLP(3U#7g-k^72s zy&-zh3&dBr-sONS(f=%Q?+G7IxNCPgPUIx0?3(a&bQPA7EbVFj!@t8TR+YX zCS~hOBHNe)glrj^H}1}DvAZLX&z&c;s{PbvipJAm${xG+Ur^`oYv%3zd>RAkxZHrV zv+`exxY)B=w{)%S-1-TBoqzWVhxNd+NXnE5(+f}}Io=iJ z@0eoR<${ckM_yA~7$mgi|9hm{qX%PWNH3xwJ|HT>-9|t%8>}Gbt-X5>e;twY3E}3- zH#=};g?tW+;e{3a_+(-1zc)TBHw#|r@+^E~1CM*Jj=sf#J5m;!`4=w;JH*A`_x8T? zXX?+FbL*ofUys-%K8e|RG7^Q8zAV*p8vT3Ta<>yZ}J3BiJ zqf|y6K3-OV>dWY4_%J`>Zy7Y(39gJ?CyXg)nQ+|H0})6#&&Bo zJG;14BF$ccP#!l7eAl(V(W%X8mNy;0aZAlpWL{vfurF`t74cSHs6$YesYItzKaMZEU)6iL34JhsDLRpSiW8Xbmi7No-}F=e^R*wVhX_vqXn@OYxhcX zVidODfL`G^S8dcv5g51P@diMILZdhsp#!denN%3oef4-_i1(eSx9lI07z-0+3?E1s zQo$h~+%4cxsU1;h^)gC;-X;xuAgZMr?0Nntf{4CbzdZJ#!gU z?YZs5EF>8pBI95X?EK_4c6U8xoTMmUYU*jV-rr>H zO3#;?cxaejEr)_dxyL4dc3k9S$WCll)dh1yLI#|;(@|2lrgVc{LZBz>v($*FC@Z@_ zqUjx88+C9w3uhnV_7-EwiiqUxVVRx?ShQ}sDKa}Ia;pZUA@Uk%56TdTFGWYw!YpXb z6ScR8u&NJjj1Ecf_11aux2AP*`zS1CQoU!UNlCF&?&`L4ayw(b*CfpAz3=hL=Qi#f zUA)P(mX}*RnKm9ygk{_d6e7@_FfnM>2iswdi}((ouIPRUd^u;9m`sdCWq^r%a0#S0iwpFm%QsbyqS-q!y{g;_VBpR9ILS zV#?iP8-$*pn@Kl_vqVduY2pb+rssWqeUB{ii~V~^Dy+Q}QU!ow_NGxQNwo$qnGbU7 z;C9vb=j+!083pc~IV|(Y4fxXQ&ke=StKk5jE_#_Hpyc=&wXzj$4b8U^ZBHKkP&Vc~ zO9Lem!n^iFEaDb*(km~HD14npr;1skHwDMoH}d>}acoTIU#Q!SPWmW$=U*}YuwOy{ zmGp9sfVyOzWA z-QuD9*!cC7D(_F%5urSJqS!U=eqC4Y?(_D{ip@p9&|r~3c4k{V)_Uvm%?nBu;-qp8AU&f(##cVBd8Il8%ab9s`)wJaKKw1NrJf zx0l(K$Ij0m-;?x2CFrq3Csf>k(Bb+813pVg0B@NT-`(A!qq}2~QtVC;#TGH5ia*x> z`15BW4VXVi@rt*iSs5#HdS`bE_UW#{Ia|lVhlf5S67<#h9SD^ z%=C6Ril&IgT0@Trm1@E+@%UcZZl)xO0R-MJ%7vMfxb%@07$Zg6vTY)SEl zO;!B=rLBtd3gNDIU*eadHjRpI`UXQ4ScKKtY&sew9@`Z0V6A6!N?t|t-nLJXA{lz( zTAqE8?B5TB{S3RG5x!$K79VX zlt%8*JL+M(C`RiF9*BfU%RrE(YyWKIwkk*#Tk$xhq@=Fd?PRyxyxJgX zFlTMkO~8`W>r=Ih$otDm2i@Iel!2mH_6LZ_*L9{}V~zKeltET`;tjmsGWcFyku>;O zhe&4ZqnuV}hSz}6dSRRaVCbLt_grQF3J!!pvk(o_9de1fx&vpdVWe0nsW}?DntGY% z0L=2SqdDi${oR^3W3I=Ryk5R}o}S=YHn%sUzon;LHNpj1an68CkdFoiwwNOGnu@V+ z3Cumysra>%B}!+GZY_lii_|H=MUUM_c1uYGd1+pG3WcW#E`&M?cp&Sf_ul3`O{lot zryOO#LJTvMrl!|q3PggO??GjcaD9^}6l-jr;gvd#{5cFxoPqh_gQpfudXj7uB^T29 zP9 z+Rc;&L{&RQ#eZm3L2YlJ%LdOqx9`SrItXalNB#Nrt2&+4($bPDiEBUidA5oCsjj`Z zs_Xr$C)1`XranGi8bOceZmqxuNna2hpPFp(cQ>8+$qiqYmy!z+Y<+R!mcAC8c~m8H zJy1~1SYX;%!sNj^nq_U{#pGZZVEbn{+TZ_9@LL}VTUW*+8ai4gA_IV^^EHdCg2HN~ zbIYq95V*(5JMaTBH0fa3#C3Xa2d}PEUB~_XcrcLisM2~#(r{6YCp$Rxav2qxfj=ZCgK;oPl-eGvaeuy9dc1ls2&4ys4U2w9E(&JL>mvTR<$gBCsd6rTD#*)%8!a%%|-L1LgQp*_F`7ZN10LY*4>O$@HL7=70cXs8m!R9%N*@s`B;_Y?f z7mmr!5m`C2d?_?MqF-mVK+eI30amh)Y-wS@IB?m!>&@XNm5&m0LB`k}fq^}eBm%+shHBRIAG>$ zR}4>X#L3wCAwjH?L(MeWrxb%v41|t+i4Z?AI6fBzDej0r_HTR2P3RYBmuDqi7=7>2 zasv(qG@l9Zpl`V65K2e?zrEi_Mv4zBiH82WC$;m7b(vKLUZw%1X~R2>T*-=-P6dg5 zXbb--wL-y^Ts?7?HcYQ7r@K9FlNbv{z#lEm&*-k&V|wqW-uJ#(YVdeEd30<4mVwH6v{A6nG#glz z>Lk_mqOa-3!(2pCQa__3!l!-3UjTK5>_8u+q}XutQO@>95!AHFb;n`G&)PrQeHN3^ z@i#=4oz;%QVu(PX$b?t4usb^aR$Iz_$!Fb_*0-vroyA&rtFLt-AI@sG8*q1Z^;up; z(F%RT`YPJGeNb#7Nv@@$nsYBEuGwLgstKAFc6lr!jr&#)d3!EBtWK>{OUS?RHNV9notzbFvUN zk#_nl6=nZj`f%`|=jWx!)odNd$W`PYwxqXT4VHKoh|Hp03dKM@X0ixt}I-#x!8Z-X_xLIK$coadhwr{@uUi^6vU~~ z$5!taOwtC0hL&T@=p;7t#q?;ECcq0Ft`yr``xuCNM^|nU8M}A}2nvl&fWjev54f-# zT)EZzOKR)Q2$T9_!nFkST$YvwU%j58C^bfEyZV#M05|JK%B(=nNmY z(=48h3##AuXRLGmz3!4xhZkR+1FqKaFhK}Fo2z$Q8lw<<2QA5sbA(U=)l~Vjg}dER zl`|MhvE!(po>o=eeEN3DnSBJMczSvg7Egi;j^*2)B1xT`7M%oc`#&s>?B_v3(pqr+yd%rd5|^XxWwgWf6pMgOl}g|t zMaG;3R%a9y_ zwt`Ddmktn3uZE~zA| zB}F28Bnjz_j1aPt8Q*z%|KEFj-*Nok5zq7c-S;)l>pag(@||ye*tERfuR$d7K|0;K|s{W^98N?z#*oGh7C^nLvUnrWl+zvBP@rPc2K4Vq0 z5}Coiab`}^d3a&k<-%zsJp8Q>N{8>rc9;K7$e^c^P6sG z--+|CE6fF!I0bosgXSwDb>W9&i#A1Yb8vI}VrvC0Vj&7ZyvWL1p`llB_h>-Z)tHr> zzagetNDq+KvHRWAn2lku9y&7iv2)&XVS)LelNhYLRW#m&B6rI>--8Pz?;QQqbhjTZ z3@airP&_JvycVs1I!dC4;ir8`WvF8gJnj52LTwvX=8B5 zfm(aKdE@H)_52Q5*|!h~V1AvCZiG-!jQ!$xRv;){gXs(v$~bzhh%NYiFR`>1Qxp;= zVPwn`9?YJ9Bjl6Z=g#>b+c;Q7B(JJ{LXM~-6S-IQmjSzIG(r4hD?RGa|$cebo zrSF4|;w)tA`1z7=77%`gJd7oTL5OB#j*FF+%ke~`ILhi-B=rgukAeQr#DIKOfB+Y8 zAj@t^Tt_CQ(VA^^R%YhY4YA~Q07A0MO4G^g8+|5k4;nI$+$`#Gc00wyp2xax-@Z&( zgg@uRwkQTJ~b>o)xCtklijwId!P6;~TM6@1O zz|Y9c4%I)UbW$>-00DgNMHYeR7b()B4!X(eHgsi^oa>*Nm17!dcxSY$is}>~tilj` z()&R~B4pq(7zKHM|E!{k9ty<-ECr!8m+))62qsWLjDQY6m$lWSG2XxuxaL)90PoE?iR?w~opEzys;Mi!3JG zarY^V;2gh&=LnYCKY$p7q78mn0-*Y_!D0bP*a;C83*UfDoXM-28V4%i3I&Q$2$Ior zY1ogQsPGo8aRw?+g`>t;iDt-$VLsSMLB$lcLMuzV>sdmT{K3}iL8#A~o~<77v|{*I z?fmZ(z%qrDx*l3^o4k$}&0d?3YL2v#bpll_03hQzcJy#(qO zcCgM+rGv_Wc)rMB24?pa7GX>Dy55c-H(`+R9ePNp)C+L^zW4Q|9?Kg}<1T$|iYR{o zQYtSzLAYp+E{<%`Oa}7#0w%LVA|w5iG{B3h-Zu& zo&fyQ3cH42d;uR|ANuuR7j=O_LsKP8&d|+lf$^0~z|(}u@KplSFV*KhLK94YZ7f{f z5fxMIYmf20ztKA% z>+bGaZ1YUKrYv`{^$Z_C z6jV|66jfgkcz9)DwhsAe=zu6TLMh#XJX;Y}XSJ2i21HYmP?7`GgNLg6#vS^bR5nK_ zZ&cBGa&J$lFr6$4XgH47&>SWLcOwxc=FQ(bI*6}CwtJ{TKWun$&mD`$GxnbZCJQi@ zCh+Lu=)WYx2teoY0P_b12Wx6BcLWMr|K{1CSU(yiyA9EoSaj@MT*bfDM%X{O)Omb$ zrhzK9NT9=I*#@b(=L%?TPtFfQwqF0wb8$SdWw2s82IU9l@hd29Vge7%#D|Z09cdWP z@PNH$KQcH-D?ZO-^EnF8EJ>|Bd)}h@B21a=GHAsf^^~wrq`!rW;5$4PvUEXN%Zwog z$>7Sj)o#DlI_5VT+|Yi*eJMe>X!1#u0Qn~UWX3F=X4|SYhr9YQ2JpvE6p@$b#h0oO z+kEWFYMVc$HxyelY9~!GkEI1Cc*!82JRFHZy{!ru4v}a-b1qIOJyw_^Xk(im=sP;O zE{-&;XAnUW3TOz%`4A%BLx_2fwhG{hkq2q*UwrwExCK6ljUrea2%Aj|Z5cx2L2nTT zz~Onk@&VSDFUjfbkP=?<(d`0zKcbrVOLFuw#YNgsrboIh<`I0BqF}CZ}j%~OWcjNs}9@mwcK!vCkTk_e* z+dC0;2dx!AHHJ0qOLF(_4(j?bsY8$B<++n~Af%u>1&+TIYI7<Nj@LTJ%|F{@W*mQTA7P>P}^nM9u&uM*g+V@;?T+d|R%v|pu`7_lMo5G%;BlD6H z8XsQytP@}Z{J9DgYPlAEdI87$zpYMdCJN2V!j&Pb#63qaqe>iMkmMn>?F`dZ;wcj= z3*?;#utB1K0tV`lRD+dM8D*ir@vYxs-~}%VVk&~N88#x3SbX-uLtEu}JaLO%%;nx# zK)y`;fc?+m1KP82=~8d<+&bsg%$c{8XLdeyvTz-9-WWCm@Y8}{91=WOg!}tPy?6UQ zSH;!8hAK)jn?=ybavZDTccT)4)5(~2@e$FTj>}INIX;inRMX_0&e_>z^R00Msx{J* zk((m|9XYr(JUz97yhXr&qupZkw;-}s`d*4{PiP;(&ZvRD7tQZrur>! zOJlkJESAXp542cOO$yy0a;Ia@J@zGJxr6Ocgl=#ERV_`qkQmH)CIPd0f-nK|{Kbe& z=um%y&<`6f1_QQ}ci6AtF=XW?Jlw9WR!fduH-p4l@k^T!_5gO}3;qa*O?uZPv{;9K0_Nsy%lcjv#qw>87n#L!ZceO4$&$jga$@PGxtMqM_Nh+_)V15 z1;|?qtolLB(8<=yBuh^fOLkmPZIlz!d?d_z0H5qKvi}QZ!PX zSMBL}$zjA*<9m0v9~y(t?R|hJ4D8t5cT11#dmo|ne0Ywd+rM?kyf;4)La{*!+r#By zCQ?ay{7q3!SgC|+6(uMP#G!i)taJymRS4vx;nn>bE9@ol!~`7fg@L6$AF4g3S}ww- zvPVwYZ=vfIdv@WIYu)}di++9hdTC7Y`o*{ZT3bzZ+)Kn34In#LP$Jo|D^Y0)@y2X& zP;`qnuPR3AL1qeIeUcaM|C66EhAkgx4-PF;{jed8*fp3eg!~gXmc4uDAD}(y&x%k7 zpopKtz@CI-yRDUX?l!f!#|~1=u%ANWt+YcmozkVJjrQA)845;ZX!)M`p!BqDym{Dt zc#b|lsH&sdh;+~0ot-B`kSXB92A>6>;pC%BUbkp91cPLDtwo8eb8ip(+R4Kyx)R8( zZ*gSs(5xWHLF>VQ6b{|eTs(X4m#p8mC)DWyYaqtN047VRYLafQt&<^`~pC6@j}Ql?mvRo$--vums=LhpWgy7fRoxPb888rCbv18;;E$ zjPvh@K5fe)Zl~AYna>^w?e$Z`!;_dNfyp*todrT(M0!W#i`p*W1NlpU1{44c;X4Xv zgFt^siX>ge&Jpz=pWc8+#H)S#;axa1h)x(vpq8WH{O34y9uD0fneYeI(H=b&H7RWI zy?h)*jbP9|Tk*5x=S#`RV`dtau%}ov=qoUv!rIy!noR?|Pm$ZaAgm?104y%ISKOAl z&LoKJKyy-}zc7EB?{N43xd6-f&F%$=&KjiC%2UKwGVKE*_jvqK7o)MM$%nKlN^V8& zP@RJ;?F)MrXwKMHty&_-vr3L98ND+poWN?t0I#I9^a@t1AkJb?{Av5Xc=PD z2+XpTa(~FaNyF(00{prq*|S=T*#l+OY<$dGIU=r>z=2Ae478y(QjFH{*ae}a(=8QRwd{If~=Fbgr3*fGKj13-?VD;|o-#|h>S zb&~i=b%ba^&?BBa9U_4AB0G0R%fu_d!6t|6ii_N98YbZhiRovyFPQ$?Ew?IDyr-#m_xK)3-n*wbptX!^kKAxoBK-TF|(R%Z6hV+ip4j}9g;^?z&}88!>N5A`<+MH zB3-g$x}Dj$)rsTUL>ik}89ZMoYd0Gq6zTQ`*F$6fzAp}c4lW0r#r8;=9) z4sAHmmbRwFvQTl!d99F?`P+W~lf9R1f~739$F9yhZeDw45G$Xhvgh%gp%O0+xb@-G znSlpW!F6|72cEQq=EK5;+?QWo9uy{@RYK{2ds3eQ-3rUvoPm{}2)Bh~h|(^t5Wqe% zTdg5bkCAYI%P8$}yQvLkQ4NT|6>uA*KH1Dnl;$j0>qoS4RGG9r3PoZtO0gdsHNy)3 z?Cbo3cIP|FX>xu-58#Uk2qFpHdmM)PFw0u50kM*22L}vKG($H^hOZnIfauz_FtLi^d5E-b+;$IJ@t)i2m++iwbp?g{0#NmEo^-@F zk`$BOjgkA`m5>+}!W_U0>j04?V7GZ<-#2xZN6@N;;51|vT_?2xup63iA!kIJS2Mvy zZ2kI02+F9D-x2vF+s^#Zco+hGC}=Am#JPds|6zGwTr46X!HQ|q)t6_TQWpxfW4RcE z@!EZc9}6sK`#K^GZ8w2!!5lOqA5SgaxO_FHMBW;A^FgpYgqjH(NPWPsh$lKLfVg?E zDGxwD1Hglcfkf@czLG{L04kJlh-pIxpuMijw@(fw%*Fqj&p<=lXjJuSTli^nOUq8{ zvu6dcvgyEsPqpw>c>3$;#J_nq+#5L-e|&Yp2G{i@##IIf%$MAb*;}1oP(WOLYBhj+ zp~fb29Q-Ly-LDuTG4lp$q>T5~vpDssjW2r?!3%%{CwiLzuu!~u!VY_RT4Of4y_w)0 zAe7~^iDcI)3yb?nHE|IUAV^$A6?4BhdNBCx@E+q z6A!g-*R6&LtBFZs(C<~d)}k4%M=_Kce;QZdpA>jW8$;hnM*<~4rrS0~$gNx6K=Mh? z7l}2tWa$i8aIHD47s`G(x$10eZJyzP&Yi(PRv?O~e%f;ISTJxRP+;AdSng*o!Pvg^ctG(J3a`pAYdk&Hh&nNoO!%kgF9dK*LcmS z!E4Ko&q_7LQcu&WPqD6)DVo{P{z$j*9-G^V8%tC^IOY?L2M7f@$77}Tb);1hpt&d# zd~_aq;iBQSfnwxGwIVo`h-F2nK{F@}si_&m(*sjid6GQ?+#=Bj<5`qcmcZ45t5$Ext4p>}W(Y9smUUy-6L6m@vvov}bGx#F z&sN33PhIk`zERZOXz2;bt2%L2IeW@V5S+qb7?V-AYS;0gH4?41F+wj107X*f5VS`+e6rpo$tm{T(V!HEhnOogHzn;1 zPrJ!HZG{iY)yY7F^|I~O*aTh1l3YYxE19}y_`-uDBkxIGPunX{&Abl5@)cZM27`9Ir9Ni%M+#53cZS;>Z3C)#dAC?o`nT2Rc4*4b-=700AYMLlB zCXPQkP};n%)xH+?CbOou)T34aF9v>7WK_E0))HQDxk50M!Nn1Tqb?}Nz^XEb19B&G z1sCKcC9Uy;ogSv4Q9pJGx$jE378FQ$nC@e48hh~7y}B0%!tE+RLg?!>FqJXCy4Og0 zZ^WiRTqhIk`5~mx`7?p@pp;R|i{N8!@AY+;gvFtmra?p>4wE0*I5QBoZU?egbo&iv z%ADbQC<~Cw6{_0Qx1!fTe}sV!W#3bk*FnsFd8wZtMt}eV7F@b?$#-&_wx&Hhh80j(wjb7CLya{?s2Fv_R-POu$@U zxMWGerDj(jn&ePiZ*OOxUNxEq4zwd2v_X$8|1pEi(+289KuAm8!Qzi)W&YG)LeIPZ z$rJFP7IgjvmzgZ5Dwa;5C?c8|aWw#mAzXngA_1t=2Zuv%!W3RtM-T9!XuA+Bvj88P{{j}vuU-=ae)+!vNGJyUcfUX+L zMA@1Peoaa3=ZFzO%Yvz>gz_52H{ApPktEt=jIZ3~A{891p!jca?l9+RXO*Eo1Xz{0h+!c z&YIsS_#b%e$(EQC@4MvH%gF0R>L3Ub!bt@yt`(%8dAEMahor3lD&d4F&^7auXlhIW ze-XE)u*lSCISbDjm;1mTD@{(dl>$!V&|3hL;t;%lQ+z?fEC1*8C<1Fp6!0c$eyc;r zvQ<~VpkRW=BU<*nO3J5IC7ZQfz~2C43Y9RKr$4o>TZ|qIg{C@mae$?Q{>S=#^5n?_ zKn`$2{Q%w{_HbNb=K`8STfP=^ZQ74;eHF1Be5mOU*_UK^S;^lI1OdopxfqfAS=MA- z0_?N3XWfC@_l_No`DFtf3rGy@)!vpuQ_p`hXP6t9~i2?Ck#oM2BCQG@o_<0!Un{gsCwPNQ*E=R&uy?NTM( z#C;wb%P+GW1*v45b6b8+ z8B4%Q`gdOdCdslJLk~`p8S=-OJVQk4tgL6EyTkV;>1uof)rAcMOjMp&QHK`zYUvej zYdg@Hbs6KzBY*&{W&Cesaj6ZxL7TIXg{2S}0%c($U13;Uqt7KRB}F4F;FtxdxTutH z=xv`HZQ^lLLKiVVq4Rw?k$KuluKcVy~igyRn@{ zo}!u$bdvU7S+vUO+szF}$yVg4=N8aW8eX0kaf1^B9a6==zgG}T9%PSH=QBukVmkMH zzZJ5n44tNR4-3Idv ztb8Nu1bmo5Sl*J)B3LcG4FHBghzZ6XQY_gE7%!XpvWDwr%hi|(+e_1{SzQmFQFIA2 zLa|5IxcDqMQKm@Uww2%O4S5;gU6*F7$((;3&~ugeWrcYQO%^E#jW5}px6n7q93ROH z5H-HBD&)$vT?cs3Tn6=m*dB2b^){g(BFZ?^82quq>NU$dIq^Q<`-sejl2Es)xqGYA zQ0(qcaRHL*?kCNeC<3(&8r;g1Ypb%nG*z@ZHSWPZtfw=$B8d1x7Crb+ zN$6CW&}zRgDWPzK__RGc_320Enq%-nTt@sIHq93J!ZVhGfFAY)iK^`Hr3V%DmqQ zxKsncB<(Dq(tTGaN|vrK&r&|~mQ-g!{XP2$P`u!geHI@uZP7e0{{v&2d_0Ep2&Jh% zw^$n;(%FNpgOnc+C3tGF85-SB(>8faLWQ%WkD+!Befn;gx56j(8cDMOq8cb`Dnzv( z9&7e~l;jwckN++lRT<56uvzOotV~kh4&ix|rMfQb{uk?#2RB9RFPKT&eC3eVQVp#( zqx~-l^t7_8#TlbXgKoz3vA*AVZL0s%%SN@Cu6Pyw%Mo)OC7BR_TmisX+Au*x!yyYN zbyRTkRi@isQ>a35`&jO7?*5~$p`F!ycKR7p*|Z}M)fB@69Z0vCf;-FW6?@8~#{c^3 zh#GxtU>9x<4+?QHxw1W+noj^N=#el}hcj^^JQ5nRESDh*EI{i?fGG~QI<6U^8*uw4 zMJX(E$eHK{J9aMc0?I*NA?OEFh>Qx`#qvSuhOeQML(_;ii9y0c^JtwIBNc~L7e4k! zMn#3ENl!3iP{DY+HYqQu9Jm#Uj*_v}kc$ZZ=L9W6%ic=~9b)r};ag17Ki zf}8*lP9c6~4cL?tc2Ujs#e2@9G^Vi;5Dk)_kR!%ZQS1asKlfdw0fn1RVyOG`v5dCS z_dA$TT+U-a!KA7Cs5NcR_kjcai-hjs8xzZ0WN94aJ-9DFWz~(A7fyPS{R3^kMVn8C z(YN3Es|f_-60F{Fk%hF`M02e*R>6WKSM%RSI}|Qu#_WCm!Mn%BBW!sbCR6iWHAefI zm^H%&CPr+CPD$2D_(RmjBZ|hOVF!b!#`^LBZKxQq&m*?=)v zH55n)AP%A*#b__h^HGGjL-;egZ$>4Z%MabZBj#6>*AIF%!Y3^{dFl4|)n{3tKV-j( zm;5lu3Qu5m}sL8F_S}8uutfINvMgYPkqSNG!hIM&6?Ia^v87ae#^Y_)(q_r zZCjC)*X#`+7#N>3d+9-tvgBnAi<#2gG4Y3eh zbZGMt`@-vPtW|Tr7hV0cwR!U6Leq@p9wAdqPADQ6L;K!1|L#gS`s5fJ_lE7sLD}5* z#@3ZFW=;)s<}-6)eh1K*H9<$61QYn^3s|`0YrCFdrvq6z0feTx5Ir>51y*H?c|r`@aroNI$RA*$zEOR zUy4^xKq~|xw6E1|vTwUa3k)LOqNF2%9~i6Ys75iRgt*xhIMM$GlCZnNw-$2da}efL0|UbY`_5=qnAL1gE;+srL$!~I>Pt|)x1wcX z4Bg@U$THz+_M-MLQknn=%S37@d=!v(W2Kt=VRBVZ8f`g4sfUTO27uBQn6M%wW_qyAtR5H^ZdN;NRCI$q zPO(u-8}q%7cXuzNl^19WXBDopNK`xUA|o9>Qvhsfi5ICA`m#-UaR1Exuys#EFQ7a5 z$7bw8>@ufeFD9Y!o<#!Oxc~yl?+Ifq-zze%^Izp!Y6LxHyU@{t=7?}4VH@#XkUBx;e=7P0rQkw_0Z-`E_%Wm|P}+>|jf0^^V?tkLMFmtycs1~ z5k?rWog2jbC^LW`(s*L^?+Gg`G5fYCv}f+Y(b63kTP0n)t9(($LA-I&>jb`?L+1S@ zneji%|I~LHPOnN@PJM{jxUBUjv2YkZT*W>ua&V^Ng6uNE;6MZZOp5=?j9D=JWJNHx zhgji1>O5h}@=|NOxT|iT!LkK(npIn!9T%;dxLrS!6bGRUW*p>f*E)^%X^w}?1=!`* zm3`_EI$=6J=4cwW?i$sAjK`0^L4<{lfVPa$vqujdQ{i}ir&uGF1T6uejFWS9yV`q} zbFcU&nvejL!bHLB^etUR7&;*X4DxxIWfH{u0AZqSw$M!g6?j&{?`qE}^eTTr=)z=E zOH9S40pk)$4V417oSi63Xos-S3!qE1&l|0PQ`%u=rQl@56Y3wtojZYBcYRD+(zWws zSG3}#5+>vBzpvoe#GpN&4!rAc<6Z-f$0vf+=u~M?cezQc;tHXl?-BB`am(X&G;TT} zf-BhyYpH{xerFODxXgOjZ~1IxxK6ky&tV5qtbX)T%JnTE%eyvBORfp zdHMO(R=l8?mRO-XK2<3bf7XPpXtkt<66U}XHosl5q)_+$8jG>(QrpQP2*y@OH%eJ8 z7zZt8Kun*cd+syjRRMfEZk?JJ2jT;2w))VDw&F2n#jC2-D?j)vRiiuyPpPr+-;2 zTXRotEu76xW&)u^wMHb0&}es`uk6Laya(-WGK><#1Goc1rPBeijrjhYw%N*?T4ct7 zA5CJVTbqvilTSQQICRI0X`l^{Epo7A&>|3Blm5=mhM#6m@9@QI%RaX7xlO;k zoXsnz8ajO(VNzvP!7uo{g03uHo`K#o^CqXa=T=m&jG+q%3P=^X)Wv3+hZJ1 zO`u%CKE5lrG!%utn7J#*tQr|b52TiS;#bl>0^kV@A!x&5((#xauUcdl=yf9>c$=?C9J|DOvmyX(ztp2Jl1 z+K4xs+zviv?Hcrh=M@(wF9HlwBOQ5g=t^kE#6llHJS5YHy0A9~l`UAh*!XQ}+6Qhu z>g2g4{cU9KdNs;>%3iB_#ME!@6z}8y9QPMKfrjyVzhtf0JoxhzRQ9mwt};mZub$E5 z&b+#?=2`NLpPGdHOsQt_2iLMU>mqDbg3|z_pbIwl+shz!8w>B&J^S3kUMyOoqyJ=c zxnbN{u@?)XU>^r?2jgKOoMUtxZFKQS4j9&2K?+pP-aP){ss^b zVAc6z%{S>O*>J?JG~_QDxHGm$sh@EfV{w>>LESI3e!b>KB|CgAoKn?_6ZaYiNSq_8 zNsTwOBj)pTnONHgo4NcrUd5sNBQ|AAR&qInqx`#^(nlc>EJ5B21-Aj*38nD$;~R@r zf;(r$Z*ul<+vpgsZ{HWWY~;!^H}Xd}Ukh@0M7O~ZnYhvfoA{?_j|CO(cvAi zb(i$mn&6J)!cMK{!>{U5)!qZNsro9q1nP*iTvTA<+ zyMz=I7|!{C)|G9S6x3IsIQsjn{6XJ0YtT!S0z7~$7VMS$=+my%my~UpTpI_xeVtJJ zBO6lqMI;PkZz%^vi~EX=FMu6)RYOHaf6jhdVXcBw%0z}5`_EBG!tWqCEW z{zwvJKq;j{6IH+V8+*3+U-w(BnN%G*_4VzJ128!vXGtDEGjsE#?elul%Jy?hDO_g$ zW%}h@oCpj1Jg*YV0KZY6XvxBvTfD~rB(_NNjr@Dv&g*^g-c77AR}DX}{yU91J<%{o zT{!W}*;W2uJD&A+K`y9v$n*(wd(Oy~>ccxNkceqUj#AYdK}Q6QiBhh36}p;dyes^g z*cWL}~!V7hpC&w>emes7vuFvl8 zaWYC9&FsmE4^a7uvJ}M~w#6ICnrX{A|0uM%=iqBma(1OnBBUX7(U$ z4p z2US(u)CXk0e`(wqe#d4{(#y&}Yxs{{oOF&hJ^XZ-#7>Z|o&V<~G|%MiOTM|NMqqo- z1AF9r+S?AxL1M}QWmHA((tfO$amcbb@i9X%_RiQ2%jeno#_`}u3@vDL|2x99dF6Vy z>AaWT^&dWb7*h<)a?thGaap`@;ae=UFwSZOBg=dAoAWXJq=*Myet&4)K--UoT|4wx`1x zOHT>XLre8VJptj24$ zDG``xdeD0TP;FnKgiPmd%mLV7Q3p_S@Ku45ZlG!6WBGo}kP2YZ*T?(o$9zlEe!R%B zk-w;0GoXY8<#-i%DQ}5t3Z(&AiNL1+yziIF)1Ovzw<_Fh%f6kZc7Wu{ycpsVA4D%z zmo#V!3JOc3R+iOcBcfNOg(~{zgy$iSVO>Xs%brhXNoO2z+;81~O#X7FbDlwCa6~Ck zz4Nf?Y%h=KPpUX3cisu_j88XZWu|x=rX9q1_VwK^FoPYGM~d%7HwcvV>-9;)?0XIa z*`*+APnkl$_}ze!vlv5P6))F&~WiRXqb1gFUJXL$+ z{X*?~XW)4ov;8D-;;O~j`)j}uAb*9+aHBjA%Cep~Cj`9(PfEdAmBp(?s79;JEIes< z;`ZOWDBuzXUu)Z^Pfkpj;MZK-_1bpZs>iw;an~FNLK-ah-7+Uy0Fs*q+*x})nd_wH z#(Lv-5UYT=LyQ(8S;Mzjb!J6}oL`UN=`(dWVGYp+e>3 zHMX{T%@UoI=j`M(!6|mg!?*W6Hwu^A&~t9z3o?yAXbB03=u6FaMxJ}9yH4-G&bsc^ zch+R(9mNX)W>)}R1%nY474=#p$G{*@YDjITH%?p)e$VCOZWc;DK0Zx#=ggY4Ps9P< zG{v8w1Z$@6jZ**~ov$y^JQ&1CdHQN8E(0&#g9{AnYmt0*l^v3koYA(lL zM7kkH7RgIc`D?@P$H&yTSUW+53Eid;)D%j(Z3ASajWFP1d+EZ3B;1(cFFhu0XBni( zKK>^VssSdfowMsG`_S#s-r5>=cW5W?>|fPoZJGH28X_3cEJG_wHq9rT`(y|~MfU|L zO_k!s8|O>J?g!HT4%5c-Gt;gLoom*uGsVo~;@<It1@G1d8F?`qJ_Kla0+(9Xz=GZ&KJ#nf))#*aGquuT-me;iw97KJ^;+rE+9jC zk50qU*k?Pa)R1H;A#M^_ipHd%ynJJrOf!CQ?Z#D&5{n)1JQXH;rAsnduD|z$-usJs zwYB+@ttaM}i7yc@9$JWhPquQj!31P5@{6TSg2>fGM86D5Z3a9t`1=}QaG7K9rAkdD zDI?`i)<>mJJtOVdB!ViY4MTB5FL+|H!wvJDh}g~0lGcljiWdz5>nP!o3{>stZ^6%I zc;KrVqXVQxhL3eqRJ#`p8aO&T-$l=7Sn&#rGH53rAr&YOcKa<~a}Lju5X!x~)h|8> zWX3BXO(;RIh?K~Dn02;+xIt+O@L5kZUB-NlU!SBh?SEIF#%C=nP zF=DI!1yV5=z~UGRm%iLP2NlNWGmhNJ+_7;ZPwi5GXq0etlLz6>x-}<+xDS+ga{J*%R%odW^nLZ5sYra>%Yt#8L>?xv&tqH1a zbs7$to#he1hR2T|y?Q>p|7d~L)*+<;OooXiZ0HaNJ7Mq$_xBFVRYsRTdCbC&e-(&? zzJNi4yW}`GIDjuZd?JHg1`D9K+Id9&O0(6SsM#%m<&xyM5-ERS1Lmky;xm-csc(*m z4{$Mic72W@$fZD=L(F;LEOF(|ohDP8^uNarr3~&5zrcq@VoL=#%0wq=Z|u2eZME}Y zxH(s(*rTa`aQCynV>GnrPNg?qM~>A#!IbW-Y5C^k+Gj`?r| z!ecOU8)t$`rx1F>_t&IlG8%bku!4NS74Gr)#~uIzOzhe?z#$OtgYYe@7T7i7bQ@mv zDDui!zd_$5yLB9Er8zObLYfrx|9x<7lP;hh+Gjw{VlR4{e&tEpv>q%ActDzASO%Ri>Fd!Mw=j;95J4{tCo>8s75_6*Ira-B66< zmoZ4}LK0E%u((ixsHmyU=aP*wC3iMr)S&NWhT^jsO@IkXAO@9M!xn{&buMoDuWd7q zcfUOBmW&Y{69bjt$4!~NxP}N$XwbZ~CWVqmePoSNDn}7o8b%v|x5_p>F<7*+Be5Bc zHVPAbouxtREa$NIj?etc>1UX!kF8*@!u{0DDKkT#6rr=n5+GVS#1SG}@r?=nq)ngOBqii8pTp&ebM1o+mt zSLA!(>VdT@h+~Ko-D+M~7`D81B(u?$JXwOB%w<=E*ICh5ovo%;=q`dk! zI##hM7dXoS@WF9)&>^ZEIuvqzeLT|ETn1!){2MjHQakBgwvp1}`hyj^wn^~qzyF>mTCD+8%$ph-9&=ho2&C(iJN=|T`}a?@9GSvJ<0a=xfcG%$Kra?v{?;#fO; zBj{=n@`dRd8`o#+5nq|Dv0PQLl55jJ$Pd~+&!iW2x==&5df(03v&37cR&VhsS5D`u}ckCQV8aa?$hJzFvP65Bk8a5(ypo6Khu_3`&OhQmYOj11OLIaE{L`O zlj?y+(b%Pf`XnDDcFYvT^wI?t`3JuiW^R_tK4IG{4&66Dwh#lnmH$+F#D2((+1VXt z6=Q#!0eJg-+)6Al1`T%^1H;vdcadqJZe+KpjtvBlnGMIe<5c0^!~=id{N&7Q-^c@2Tez)2aAbIZRQMohopsGfZ$C1Smv}k3 z2o_pKXr~)1CkL%QuekPxAEjAv2higUEG6i)eo1prFcoHQK|N4_5XNATrwbIIN7osq1_Tsc zR~e7aoP^=zPh)n0{cIRs^M7;h-__-y&ngzxq5xT;PX){QJ9{(K#j_7(nrRYG41FU? zL^bdY*6xoa3)lvkDKqW0U~)rSNGb~`vt8d?1@{69R!cj`M73_6P zlg@~h3oM*GkF!GDcG`(X)_~aFs)SmlSEo5pL&Wt@qh)%dEtS*X+uH8lJ}&D2v$g-d z>EWDP0k0k6Y^?CtYXe{9Wsg}zdlLT8ZXiUiEm8ZAK%wX|HCRl_1h93M+^H<$>w*A{ zF4hOq-k?`!TFi0IddW>_;%BD~C*-&7_>@_vC&;nmOj4VHi#yWgtRsZJ`P!?Vo+nDn z=Fp9pIo0s?Ul?F0Sj`L+EW_An4milH3>s&oMNGY1cyZ7&L!-NGzR!$2A-X8a<}fg% zI{&~XRd@TJqw?;7ul>5de(ZKQf+CM<7bJk!jh5)`Fpn?k3CXGs$0x$cN;gP_Y<_{> z{0kSgehs|mlIEn=<*Q^Y8 z=L%krr4n($puBGP9&y@5kCP58t$!kWJRA?tFWG)%O}p(*V8m(TCa2{mbxzwF3%@#% zmW*)s&Lo5EXtYFB>I!?l@*Dkj+C@51v_NE>1F#N-^MM>YCAFYHo2-3-7cTav=dFKH zV13xq%#23JOW*yj^>yqu$Q$|m54=ebgz&AfNk$psUxPc97Iv2|B?D|+dI=-@mut)Z zTE39g**8+xGxB=VA5anc%Cg?ro_O9+^<-TRCtg=PQlk*W6$GM#&CZv}hMdZz8FpF3 z{5hXx)vI2%=FXo#_aa*pyMji68TEm70eIMjg@5p8<;WQ672n~Z=86RU`Cz)Cx?!c6AZlZhVRzz;JU-=NE5I~Rr@_O~n zZFgA7*aB^l!dT0^*pAJM+rikv2#8#g>$FYD}cjX$?BV&oFrTTO@aH&-|Z-%j+B>)-C06N7|tX4aZil)FXJ4nEDDCAxFvA= zx3}x!Ts$$Hq}@Vey;;??z=cGTsK<*yR}%6sDU;MX@Zl&R_=Ocf5(HtsqNFR*0OhQ# znt#eodG^uD=PR*44zM`dr_zK{%QK5^U`mDUj0;H6iqUO6RJ~$HA8{ZZXhXBO>3)W* zqE@?ZL6lPJT7S9&h5-vafQ=;(Hr1PD9hy_^In=P2Q+yA2!0$0qJGyE_q4LkZn;Wiy z9CtI~FGh<2$6c+p2ac{-S@%os*_Yu~M^K0lp^+qu+I>Tew^)5QU9JASQ&jprT+c6& zUz6 z5l%8a(X%uwlhl=eK^Sg(W>WeuejS)bv;U2M&HZ=$TH#|;v$C#uEe2-a_LPig** zeEB+L{6^(Cl+|y60ncH;>7fwzRV>Gj9m^RuHcMXNr{zvZmer15KUw#)vL<@ybZnqv z=(CQZe9%T&POD>fpK7w*M%H#T+D8`!TPWtuo747eW19pV2f;nw+O>Z@`*1_qM4Dz& zeCOW#(oVW8ej!xC=W^Cw;^oc0mK8qLTP70^nN~mux!v{8b zcRashEfpmO%n|~6C;-4kh8eRE21f&U35mcb+4j}5ggpFX)s*N(KaW5L41Z+EIuSLH5RJJqd z5>mUU-Gc^~mQkRRqb!AD4fq3OjiW2%B*Gh+t=T)({W(tkg|dHYq~2(0%{RD1-$x-z zHfHV3J%AA%SD0Qjdf=L7U5#S#KE^|HORO`WaPq_*FTDpgd<{I?7z~&U6$1q@?_^_P zVNpXA0o87-zo#x`maC3&N19qZD9GraQUaw?vQTedG$_O!BBw!)O@<9OO5A>7$y5T^ z`Pj2K4_;Elr@y3iy=mopOB7zHhYOKg$#dn~aG6Ec;Ij%`(?Nh%yGG@XoSLn>L2#r_ z5_bKwW2g1p5mrDU~btSwP#F{cMJ=!*&;E(T4~Mfkf~Q#ly$*8ItD-t z`H>!8V%arU_?#Ai{ZxTtOEn1^Hw8(etVM3UcFZK>{>5GTcf;XCGayzCtfpQl=J>vw zpG#Q7i|&2gqNCxSRgX{Ho|FI<#LI;a&~TAg(O%V57*}t_3d?Wl+PBPUHIK1M4@%WJ zw8{YF_KhT^mTW!dqDv^h6W%eSw|r&#pedkHVlkuPxrXC|{L4v8MhIFae^FGDz~sLI*Tue%!dW(O!HTY)!E7S^K8fjXEdfVvFtW8}2qVR-^bRMjFf(0X^Ov2>n$( zel>^i_G=}Hof6Me78NCq^?v_;5`Td6f69>@-XDmD@`MIF$Up{K-6)Wl{zN=`O=4|# z7lt05XuHrxEgR(i`}Z%+wqkkCx0kTL9w!-QnjG8#A}0ti_M8BH6{1L4+J+NbWcGff@_>??9`@a22< zRkGN3e+pnX!d?MtqCwEQhZj0*JGXHi7YOgx82*zx3(aY1{D6B%T`=**6{iN~p&eY3 zQb%%&!96v{)NOTbR7!YDH9+uzbO&)Fb4Bjft+5MgxJ|UX_P;&6c%WC|0`4RlhD{Yn z)a+Dy2|Bw8RXt_>#{+90%`FZ0%0K&hS|E4N<`@kP^3SD%u$V*4cwHvt>s(CFsibAJ z;C8x5M#QAYa&hjc#=K4Uo$v8BoEC81NCJ<|Ny?zBeSmK+c~_B56aqB1e3DEWd$Crc z?3X5+HC%j!n^{JNR{lX*kmwFAH$ zVF15`24HH1()$ZdSEj{WR?IlP_3XFk(Pm8;7K7#=2!fRIp5yG6;4E2EelTl6o|JSok0J zbh*P48~e=#f90Wm2ZasQuQFh4;-~ALsW#s7U;+1=-ZtVLl-NJ0tGClNNI+b}f5^;$ zkm&k#Z5H!NBkwP2Iv5nbp8Szo1R2< z6|s06O9%)mD!+Df!1w`A1npgd?CCtZFY1e}f(Ro+3?W#`hy|(_gm6YUrahBi=uZ{BXob&5;gi9L|yG+O>&5skN9S-kdmSku{!Cli2&l+I&04 zFx|kiI*Mt2(ed(KQEsrpgzxTO(&C&;C<@@!HOL${ZyNiVk_I9Y#luVdWw{)CQG273 z^T!4Gvgp3acO9U$e`qy z+2r%_)TFqtunDkBk|N@5{`+0wvOz=Q)zjdb zvw*-dL<7KX%;6GoLb28ncELsOm{v$#ow^2_t!5i5->oK%P@vy4CC8R5VfB8 z@&gGAlZ>mi72BTKt{xoieYP-XolV^%^n{e$eRQPuu8WHlN~mx_gw*ST_8Th_f&g&4 z@uli+#4`nX)AEJu>HJjc<3j7TE#_^Qr`^y4#6A(jMhpRIG3Ef8G1^x(hBb0DVgO3Z z&xDT15ib&|2`)f}OLs*T%H&Gl8Bl*W-{E|7uRE#?@LJyCIZ$y-Ar#z_)=tqUo3kck zsB)P(6|RzVn{5!V)OUOf@DtvGfzfBpvhmuMXA>s?&Y`Ti20k@891*5&l#UQxeJ11K z*Q%6vHfe9XG2de_Xm>}ZdAZ7I_SA&XJHEmpI`KV#43YB~W_2g?K%s#4A0BZ_G3YT8 zPDJNNq^jMBRVXIL>~kOwIngwLT6(AIRk0q&ONh|Ke$mn1f6vd~zcsY7B7Nn133G>g zS(&`HioK(1KWjcT0ccopKLHT?ctSM?^QQ;%b2!?%dk0jeMNeq5X1=5z8SyN&iv} z{VeLzJnObY9T~8bDtTT9?1rI#;6bCC-Mr^9fcFM}v^+Me28K3(jm(MY4hRG}UV9*E z_&!_*WF5NL@|dwinktg-C>+6iSr4;|(tfTVzDM28I;Of5Ul$V*@o%Cb9g-~`7F`LH zFc`nU@pn1=*a@Bb=Ge(cST=BWh>luE?~bu3mdcm~u_pDAc7|k@oY$GqHhu5#qCq<% z{=NJ6O)z4(80d7fI*B`JQ*73dYz$u-EYHjl5zw0f|&;zxy!5;Y= zzmG5}yvu8V50_6Sb4qsi^jrt`cJRU3BJflaIF|Ad%nMQq8P8A(B$p9%_gNC(6F59S z-F~T{6|(Z~R%RA%1_RrAZ(57K5o-wM+ri1W{K*mE3VJn~E1_m;T=X8V(0yvq=ihBn z#+w#*mOb5{QdhRhVS|Xt`{9ZMcLCO?GCVMr*kSh3w|?)}nH%9@Cp$N4JipZQne{&r z8;ZBi%0qQh$>E76XVhb_U%x{Bb?`s}^GjRe?(owt2YKq(po*jtYT{4dbzlm#J_xg} zxdUh0zWq<4O5h=?UvJ>oCZHnIq#Seo3O*xnGSorxITE)*#`5{?+8vw4onl`!oRNxp z0G?$5*iu2@;S5T41NeOjVI}z*fPyWAkOWVHy^l{0p$=tu>==*J71dp`P|Vo=% zVvgAM4x0|iA2*L&o^9X*dvT#yg&?;hUo>w3M;aU92a zoIZTGYX$4pQ{*BM=$}AZXlZw}?${@q(@%GYe;1O(5r8Kt(tFV&l{(W}%Eo^%QQ!QO z^{pU~gHiuVWKv8{0wp2IDrrLHVa8)GbpaO$`L!1&-F zc@+ekHCiwMZ8luhf2uVpc#wJUeg^4K z&co`NxcGweJ6`U!0r9l#ZgAoJC@QQr%%X`T=k^RI$ripApB$Lv>_)D0=s3QJHc1WF z;7-^PE31`Cfkg9aTXYB7bp0=WI;Z|>6aQ#5^(&F@fibQ@!Qd>i2};Vn*0#1E_xO$M zabR-qUv`*&AIy^~>Eubo7f+i=qQW%O^+M^!GyFGBGzj+F#nwF{Y4=XFu|dlT;hPOD z<}ANl$xOE-s9(?cjyMyqMkpBF2TUeF;Q;s<`yXi;{UeF}p~z?9c< z%Qq4R35lyM$5dwS&z~M6mz@KFo!h@erdwqL{-S(IIY

0KMI7<3;vof4=ljl1e6a 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 aW{V(XlkXtzu85hfEInEHrngBY;0 zd+GiVP8FvcXmJ6{!^!D$c_1g~k7tro`8_7K7`fsItF_3p%C6>CU}q(Dc+at^I)6N^ zUsLltN^BVgB-!ug z?XKNWm?3_dwfWxdIo7ieJAWz-^?$gWABZr?E08oo+Kpc}K_MX}fLk{N%e=FaKx>Q* zx^h_eM#LHVdutfWC~6Ww(G&)1j@GaO0Fk6Qg;=2sFQKA}x~l3TB*>B^4+YD%w5HKY z&z_^sYnh0mO|c%`>Hr-igU%ob^^7yO=8ZJz8(9r%*>f7A>Aa#H!HAv{GgDD+YodU= zQJ0IU--8*M@;qHzv2xVAuU#fflIiUcpZg(N=9SS&^IK*mA!ks6bD&(sy+`8XiHV78 z9^CDd!JKMnvmngUgj5Sh2s0C{qosgTGS`5GT<6xl?UzGPSeO>H`q-F7jzCNY6b}Q5 zjSm;vAztV#H3|HM#tb9He58MD!D~t@)cdx0z&mj6KuNF18K(o6-M_tzRk)bE4+=mo zhQ6np@8_a?UbrspSN$4ZT@fWEIF4Bi?;l!> zEDML~wkvPdc*=kKnI_Ylp^~~hd)|1!~7I*#V zpKtn+t4->+BplYf>7dk^&0Ermz2w*Nj#215Zm#S*CdFaPvKlgfT32wXOm8y1QywBb zje!G1v{rjr;^p{&fLZ#IvRJ!a2bCu9@htPm;Xk*L(Nu^29?h)p_`p6g8Q}&zzdA$M zCNB!@Wb7;18XnvE(Z63Wx?*(_-3E3f_()*{nFssnTDT#feX$DhaOJ%*a5pOW6`o1R z_Ir|ZP&ubXJkpJfIBbmx2J<>G{j?+>^(BBH7`OZ9%ZR<_;6F^vUj%P?wWe>~dvE|A;R%N?a_wB&&F9gtIJGwoWmrB?L!zW%C4ujtVG0&Tlg2eDnH^(Df0U(#j)n!5UWdpF~z zT!+|}HH%F~o05%0nidXZeSPHcc|lJL8lRd3wOElbq3`E}FSl>3O)b!9bHOxFbZoPs zAsIA|Sj_YX>rBQMm>MRib@|%XHVwUURlJG}JmF=9<@o z8Qvvdw?R^fexSU(v+K@-;IQe^_Lp$+QOY*jmy{H=SH);Bgx#;n+&+o7m|!hj@n~*o zDWvvWAo9&@)*Y|4`oA2$|8hFzhExiKf`%;IFbDbPncE(fyyM2=Dkzw(A#b4s_SCBl zeIMTH@U)>kBdG4PY<YN)v{RVA6WZ5^JU4pzbAQkK&@*{2#FFF`e$ElD~S zpt`BQu80o$h_@LhSF>_PKnO7m=4K9_I$WFdy^wo@IO6I=QQ)*b*vRWQF&mbO;91G3 z8}CV)BSt?A-+Ymh+qurl2Df7yt4EfF)q%cTf-EV33%T3oT1m;4g!x^+b&}P02%1e4 zGSJ78X88{_>rQ%ybnvl8O8_-3K@cI7%bNknh^1@hZJUqcS3bGbE;rjM2^+A!lpZgc zG45>~J<0DR(@Me*Ac-)k>HWPXYBEh^8hVJYrEYr-NpSTF_}vS>S>k(Y2++t$Nw=TL zI=@NU`Wh6`dXOqm77zXyk@yKcN|54^z!Bdo5Usui?y0Nm4=tcz1cH78bA&nA^%e5k zAyIV*S(|Ge{J+i3z$AJ7L(Vus&9)ioRHXXCz_8gd+d&~O`n;mn|I-54i<%zt6Ig2& zf`k30$eYSo3FW@ zA~{FFU=o+BB(@9!>6pn-yN)Hueow)ekgWzUEYTWx%YsAiOU@*ChBRv#HFsCFn#63) z(pN_;i-oR;wT`1tHjHI_k_`s;4Mt;Eu%H9sQJ zS5FU**jZLtxw87v(d_!gwaR`P#eohLRs}QB=7IqDkU7<+y?fWTZ5cvR5Z+~l-|G`< zxKy`xZr0WZ;F5gu^ME#JXy6BeT~)pE)ak&fcLSx?xGV2s2$aN5P5Le$Yis9|a}i_0 zD`bmESwC}P(c&#!qv>m(4Nb*dTMXDE6Y~gZn&~1#6HbvDAVX)O(tT-L_aycETHdVF z`$ANsJ!asn)Ba5ku0<>~MiIb%Jigb@xV&8NIj~U0u6Ru;bR&lWB<{L!4CV_+^j=i4 z#3#(3t#C9)lS-kybDO;&hB|ldoCQi11?tk?A*?1{qcDXwqJVVaAj%?UFh=RM5TXna zv}cV9h>PfK{)M+EIP$`Lfx{kn1|X*mmoVHGUkhX~?7Cklg0hw@{Lr#mfQm=TVsyZm z^?2bAjf_!S{?<{5qmji+t7(vcKt@Pd3|h5}hmH3;!jg6!)b$dqCP-HGZszWqeF|b= z)#tXzD^pe0^$fC`+3GJ42))^F2Y;YQ?%o406k5pciR-RSLl(C&b*8jduZkUh!st_h z(ajQgkr)Oo*H<2uzLD|Or&d%?!f?=c$8f)+qo+s6*p-Z1=2>?H&@$l(BTZKAWVH4Q zR%dW$J0I)uF5J{KGhpE?O`@h*Cc)Q%O1h|OY<9w&4uFdxU9p$9_i*rY@fEAGP0c1j z3*xHAc1}*yIXXKRoSWPrG_3)8b*!Of0UjVAn&8%ECoL$Nq$3C86wh~8JbGm{>L7eH zYi`M_Vq~R|WznbYIMR2p*z8{l9#}*&+|!Z};A;<2j*!wgq}7X74dwz({8Re3cL)=P z^Y8dz`gCZ#FA9Tk7|uS!fH@37kxw8X8(rFxPVX8nkvEf5-6|$uyP3+BM>(ZhbyDioCar`&;Nfnd3=zAO z3FAJB@J!4Iu;Xolwv1##c+l5@ULf~j6Tp()nO2$DU_72^|luyvRoIfd3Su%8kQ zNyHQ$FT^dO=DIIGPGmJMa~-EX4d4ruDZebJ+eJUfA<)qbRs6j-Q(KSkKXkO|>Kh&J z=M!shFTE71ohL5gG!STfQbXRMf+MAIid(NSPhNxR@Nppips6&`*J8pyb?Q`+_7FoPodQcB8vo98~fuh7ruS__J=XLG+J(dV(?ce094r5qeQ+zdro`! zknSmrr_q{v+{Dki1(9*nHTCIKvLZy>MmW-x-UGcNYy^5LlCEF|p`NSQuh<422;jAeD@sa4#YsyU9+!(V9R09ljPb`C45{KO-2pA9B8T;T$3g)Y<%wr!%!*8{P;i}> z6J@pQ+@iY{+F!#x7K7;obWQpI2xIkOcnEI&&K{Ro@rJK1sGeeBX@o;AE~-VCARAjd zR~WprEZ;-ALVR4JUIc2S(n_~U9Z-0(g>%__E-nrR;c>Cqf171A-tAbflEo#z9RB0# znDro1>uT)n1*?GkG+7DS%yg)1fU2AvXKkE+NjwXGa^LyefaOuCW)eGvhetL^hxd8X z6fgxIF_^m#T-1N{^T}?JOp{M?`o!wY)|1nH!q}|a zxW~YNdN}+%MDmz)+>hSVfbTmKhm{rtpbpatjWsD!}H zTf2Atjl(%D#&1L)G#jhtWok|>B=N1M#_=sc3Yun2yzEvs>apIl`vTQ_91ms=Oqg~06z|gAVCBfgYtqS; zw;@4&y~Vpqh89&8u-f?vdSb_Ut0G z$x4@1F~QMS#a;9Yv+S{+PoijNDfENfTeLRgf2@P*i@VZV`8Dh96CqP?2SW#--$-kn z_r>Si4gm^(0|5`BFygxQ;kUgR?}Ycp6P^XB-4{Dt%|H;rg?pvC8SJAq%0Hb~N`OKD z!Ea@y9rJO}<;ZpgU9{=jrn>L4^%Zo5;};~bVUt;O=0T2=zc9W2W`8CM0dFYzT}=6Q zHOP(>=!j}3@B;Spc|o}z7~!$IP;u@DusTOto`phd6#R_?C=P~ywhyvY(f0i&NyuuVYSfAt9e$&^k zEwXu>@L&MQl*fU_uOT(PUs>tN&y2Q!B6%s`z(o-H9mNm&PbcNp($1XOOY;Y`< z4?pBC7G) zp<4+YPdDE17h{kS8m6*1YwG@Si)yC!*Oj-PRgcq$R?4|JhnC|@Ct*^fjr5>Fr40_P zOV%Ov6grb-36rvg?`TH_LlCr|HatWn(8oQ@UMknjj{2qZ?7}S9iHxp8{Gq%B)#EG7 zbT{tmwjMs%nR6;gy6J~Hhii@PvIUz}cRKFchiFPr)BYk)W9pvbTOB=qyz|5sy1mAb zWx&YxC1PiRzMOQl(HD!q@uKDLNWF#2qYOrETBduyaoq)O_c@uhc&o^%VAQ7tz$hWP z<4wf-@%6<|Ayy#T6IfTP)*w1Xzz?t4YZEv!eK{R-3YoSi zCPrO8?Sd|bzB>&^uwPpM1|z3)`m!vns9@!uGLvs#&*CI?1OSgmzsLj+?Vb?mP{uva zZpFd#xyz^(LL+!|1S#PgL$xH{Vs{dA6l!I&>sOM{&cVezoq=voUAJvha`ye#MT=B0 z*mC@D_fLDdeP#2S>Ye+u_Km3}bFZ9qWSx}z8$T|9c?zR1alkNwpGb1J03EAa1dsL_ zvwRk?hlJ(3j@@jwEEkz8&toH?hdbpTM_g8RSRk!(w#D<8Mq)u2lxdKlxB_%&X?Z)+XS2&PZUkg zE%H71^>%dcmNj0{nA5uu#{PxlU*sf;bj-puAvsSZUeT^~z%v&R?oIWF@rT^s#A)Q} z890eQt_HYKFxD5$e>Q?8NuqRKUS48wp@sw;=C%y7r7+s2M|2yN$^0d<`dSfcG6vgx z?DqEzczBnuo!Y$Q7S!t)`+;crWILA^%-kPqFPk_C<8qS(s>b zQo zpMO45U8xNx3}C{=&}W>^{4yIR^MdNVzryz(6`e zg@^Wnw!u%b$;uBjm>iFCKvXX&DBhGWJRO&q7M!3khUFed8UTU za>k~8>yd_3VMuA%H=THOBa_;*kFC|vfEHmRYBnb6I=n>p+mW+vg|`{~BvgxJo+`ba zgIAvMM>}d#ae~8W(J0J2)2n%^KLrmhA~yeX;S@?Yz-OeG2qdzC6$&G%9yBF6vU~+$ zFC(wa{+mGfFN$5lgN86aL3VzoEz23p@@nj|HK#m9{U%w(hm4}8Zd2?1)m~z^r63&^ z!y?<~>CUn-M^Q&fgoMFM^<8*{{MHu&Hp75!oS^j=U$!j3B|IlC%c_MnDQI%HTSe|! z<}-^#z^xkiOe=vd;6^CH7zsXq4$Z5$ezDFYiDdLEXx;W<%g9Tj57;~hr7q;A#TDjl zoOE_j+HZT|0hm+ZqD+Nks$KB-c)mp=)+j*$51Q<0sd1L!WZ{|{EsHY#2x~;ICZCNp z1|bp*U9_?!S=KIz6}5)@!^JE-;WPD=`4H5lDVNe87 zVT3OQ7l6vC{`;{KT0X!n0yE?jXW-e)43B7gf?3M}+p7q?!?_{E{C8hpU!#>0uiE~1 zv7M*kgwj+uyt8`>YJ4eYn*yW@N+Kdi#vtN#pGK%9+rz>s5*?yceyIs29?f3;EMIT_4Y}X~9(`!KkPGDI z>A4&tgU1lNyZ{*hol5fP=d<%O;Ky`>5n*z?#}CUWWy#*NY}qn>Sg8(S>Bli#(oma4 za4}P^qv2$O!`+Jk-wphnU`)tf!zg^G?*JA_o*N~pZ`FaT^o&Vt>0oVHJN1n)SAuqf zMP(&K1o&P$04#2!72ORfl>?F}l%pLlg)U=r&e);Px8_O9-ioZtk`jk8zg|GRKAU&6 zChW?5X<>;UJ0S39c4IkY23);_CgUBxh?!Rtyw_*HU3z-AQ087?KJq~G8kvxAL?*8} zu5w2L6-Y3RyOF+$hq5=zX8W93vz9RU6FV^%-oV3ojUWMis8N2kW^YV{7FW%?@7%G+ zAwD@RzN6Cg*8y8Ppc_zT1Rh83Om52x&P}Xbb9*b{Ht>etKxd2>{0b&(=e=x=**dzq zH$eLC2HCuF)v6%e66KxwMHZi}CjhF!^+Gz`^^idAJHIjMzx|HyohlhtPQBm7}{?r4N!G;QM{PH8)$vDGMi+A_!lB0BWZ;a8&9H(H4y=hp(}9E+AUyn z^n)Gt{O?yJmUG^n95CD2B^)Tr-i9Z>89Mr90F!|b-NGee0PjN}s{(q(r@Ig<2q5DX zK7j{@H{aVzFm$nc0;U-|_t#6hL`-|kCwzD+diLbc=ZS!Fp&&iog+9a` zw2*&brHjC15c9%L85&n8( zsHEN1&M{_;lPWt-2{Z>GNKLt~yM4QucB=uwLC>>K{r8;EU3 z5a#aDsu=BzAjp?tmA}BnD57E4ggBpKu<9V|khbA|U34|=vu%ZdN_T}(M#uMK5HKSs ziYcTVKG&_Kzs*G!X3yFGQy!x60zW@+B_-m!Zb|2*i`LGRX^D$F9%hMjlK(-tc~$$a z#ZP7)h)^0>+-2RXV!cxq{(?&JF)68TR&wS?1nmVthu`rVcS01eaqULn2z#BYCtgu^& z-PZ7XF--85ivYyFG+NgjN1Olw5%9;gewQfm9>r5Da+0@3^BwndRn`I<4jb6Sd4z=n z(a-ficfk28|0j>QS&$LMUXq+DJt?nIyjY*Z`UhH5q(rBky7FhvlI){6!Dp#PW5^{! znYNVxR3!ixhzzAHfhR#|25Hu41t92zvk)_=9XFl!NO5tU`s3}0QhWs;IKdOOJ+OoT zDSLnB=aIQ?IpMSZ?qJsW3?03bxs^hyBtIuALsRyU8D9&fw7@Ht5ls z>Uy)l$H_yS;#b3}%DAMs)+EPz3kay|E-NbEeEYr0+7c6JU`uyqW$NBIM2kj^V=rKew~eR zgb%5scwOqDjs6a$>;sF#hh9X^_i zkef!}4`Qsf(v0@(`J{(YajqzGjZp;3ZDmzjuqO`+=P*Dj`rtF}py}aTxG?2)*8t9d zG%baeQkbmWnT)=}0G09>=zup6VpT043 z+bj-~GG%xs0aNxuO?MD5!8&L-`vwPBAgEYJM~4mM?WkQ|#SyE{_c02S{x9l4q+n}c z2%updFl~8VYUvC>Rg60ufPV*}hG^i@g$&7Fu(YJ)6)r|JIX_wo)KPs|0;`R$OJp7B z((snl>Z*8U^(eSANcIYA$OZp#6R55`kh0m}dI2vFG)GocLi~NKrd(kEAka7B7Vm)7 z#FV+zG{d5ZtOA~+)QG`Ako~X$-2(moBcr3WFy#>>T#rNg4d0awa_Bp{ODES_{QWct zKe`1(MDFS8>b8YQ|5e?=@gd)v7kZ^6z(L-~AuLCLgubciwbEQ;^Xj^NsXao~QNqUf z@f3oreDZJVnw2Xn&-AWHcb;4>bVD~^{YmoBf0Pu3FTWcJ9*5xje0=4te8g;1Jn}dY zdZcgpfy|7TkUL(we0f7hrsDmdUgKig>D=RT>3EgTLQJy<+9!lLx*$_=iO#ld*I@&A zg&%s%O@poI7dX-EDDyRqbHBd2TYG>{m|42tp}NIudGo}l8^_AM-ybZiE@csM8Hfgq zjIw9|f*_4flc@g`%W^*es$X8Vc}%uBxlctP{e`%M#<&6cTa4Jqf3Qa~{HjyPfmm-_ z#TV~$f0kPI%|uz<~!{c0CZh12HI_l#BXSE%+SavvgB?AoaJx&HrY0TM`> z_Yo+sE_m*nsPe9AF|6)XPZcwBR`*nP$aZxI8WNt!Kn7u?v^3Cd=T7(i3B%}BTEQIx zj!e+BoD;FZvOIXJ-Q9Ibt4^#x<>Nz%n54Z~_n^Dy^V_X|;sjUi1#CW>e9Xu@0S-MA zYy{b~$$r)}%Huv)PAjQq??3YucC(+)PQ*2M1_;k{a)aS5u>*SzMY!wt`ejX4i@vU3 zTOf_pi-`|t%5z!W1PxPQcCgiLjdwMI4cY{Vrlio)0I{r7?GtFq^St)qds4apWXI2I zW7k7ZLOtTq$h^t`L_ z;zdObxf*r}U~tt??16V|GU~YDC7pIc6(D&I>#J2CIV?yWhwN1fPR4d^lbRk(dc>ML zRt4uP@oZUO*rKG2y(q&68sae=i~s%PX5_+NN%C1sQ5ZmP>#WX zun1lKKLJzb%f>??tXQ&6^v>G^Ef@;P1x&77xYC>gvzky8Y2Oe!h^Y)vGI+)gXP)K} z4dU!}!JwuMkthN`R?AxxA&;xSX0Cxuz_w2g-;XINOrtpGndDf3?sp08?iia=Fz^diu3$z zzpqn7+WKx+Hay|8O{jCok-bzgXnA)N@+@5Vb&kwsZ`O|F0YcXv%ZA8>q>l8#aO{H6 z0<>(|iIcesE!#%0c@zeNJBEI}Z-w}`Hp3n9Hf_HkDNIVIQf?bUwhk2d##e7a4~e)3 zO1wm&(?SVPe}96*#IO4Dii??$AHtW(5AY3~2s()i_~D;X3%B49{r}F=ip1}R-+gy% zbbNXb8k!qu6sdzIjT6XcfHzxp!wNz?sntfKy#5xxZqxWGy&Zo9`OQT^?5ppX1Evk{dKt8K4dNvoEBN0lRCIgB3HUiPG$L%5PdtZ z+$eF)d-17@Z7!q;1d2&nJT}qMnS+n~8KC)&{0DJ|gCne|^&z=f>0s;U&%X_9Z=?-~ zW&83Bm_TfUr_T`{;TFL^E9!ks*gPaXK!OgH{n5Vm?P&KGF~peP8(1x6F{t#vX!6tw z2@T=#QDg&&fjc8n6gump;59PX#eLbzsQaLVvz?smoh=^w6O9V?Th(VMTxFtkpx5xH z^iMBkn}ZQguOK9go0I^svSQ${mJzU#-S0Wm-G*np-v{!?Lfrajv%#x4Z)kA87GKAu zk}#*v3mN<@JTXMeUK4r9$`MS6vtdQgcnypT`J~wL957}3fw-Su-pCP(R5^a+@u^Yy zmI}0j43w^ByV}+RELICw1z5-Q%SoA#?@(Md zvjp}+yh@sY*qvpiJM4Rq&H#^Ag?rB=rbaXgVNp{hE=uXY9Kf;;z=oJD+m+Ppu@eSY zNOudOjFUb#2t|bkww8s-oNi1tDGpU?T^^h!?Wu$uny~w3PZxp}J?id$GVSXo&oVF} zWa^+Oc&nXN=cnusIoy2p{)UUPa1-2R$(2_$W zIUUEoGgq!Xcl(i(z#NeaLjKujWx?fwQbwF8khJ2!z7ls#oCwF&USb!a1);bSO67X? zNkdc965pS=G1}H>>0@8p-O>ml@EGphI0XQjg~vRB8VIxH5E%9DGdp&us_&`M)9he zXJWo`*EP>kPRu&3=R$IJA%+QukV{>>Vmghx?0hJ)TZUp(2CG<5@7zV4>|jYt4A^dn54zmb8qB z)}o-R(}ybZ-nEm^+2nr@cbo|o)C`Q<5H;s5+^6(HUc9RY{vE2{SBv6hKKxV;5LH@e z%Dcx-4iMs0zsYe0LWOaHMjr(o8x!PC)Vn7+D`J^-76x|k2r#l{n0VEN*`KXhby;va zx&`14Ga2GtfuyTpOo7y1$^8g=Eh8}(&#}R9elyqperyCMT8+{}usOGSPYE;0XB-+B zSVV2Y4<3`dAR-k(xK+K5u5folPZc-Nlwv4#FI=u+`LF`JTcW_`c9h~SYN!5j?2kRV zh)JG%_Pl`gp2&}Q8#_<=B1Hzu)({HuXXu>uKy_B6S7#m>d&{n`~PVA?szQw_Wui6l_DC( z9ST_uvs8p4t3ncGuM(wHDhb&c*))++i4yH1qpYlGNCVlT$O;+1_tEqH{qekB&wc0C zb)DyD9LIYdj~qhAEf+K$Xv|0N0NH8)U9?rzJTgx{)YV1aI>aQODprUjo@_E=S$ zzg@(u#nf_qEvI6RxKVZ>VYm(`VG&^~P!Pz5cZ0#9i~$mQfGDLYL-fa|`s)zLPP`9P zwY~s%84R%awl~_L4P1ir18q?Lr!3j3hx2{%mal%Wt+cR``>*x<>qkJySPzqrfiwir zmr#a3@(!*gF0XIHKFJ9+5GaIK5dFi1yrlNr-G_^@NM55pIR@)h?deF2Ed zWC3`6Oc~|?RHkV8%{PB}5MGSN6jIIiclJ~r60RDV#U0IHAi*ON$T%|t-DI5jnWQ~K z@t4>x_O6ChJGY_bXWse)DUdAC95hgQl))d8?XmxF^R+o{ZUasjvW7&4WgAaOJ{)`Y zxZiV3&UO1^mMcZ9;ubbws}SVy!P@7~&KmLJxM>y_3|};N9<2Io)6OxIeHu{|9jX5`de}?Cwd< z#o&%XY#=kaY|OR5`ei4o9cW<@yRetd$?zVLP4FAR(nlgmb2-I_^R1eWeO)&pBg2i- zva5D_%h*t&Wex|QSzMy)6>Pz*Pm+6*Qkl~Vv;|*dTIOny}wldd_7m&)V&|$ zi3AQVs*2udY`)~tzPnG15O_?D?(ut__c&FTY&?L0taN#xDLc=ti2;u~P-|{8+Gb?L zLuC}iwpbCYXe5hq;T(fN7H=Eqx)4tNlI~=m1xuGETDlAT9{J=m*}F>Hm($O8YRc=h zr)LSq6ZEJmeP7F*jSlPd%#;e%h7B&WKFt5UfL73H4lY6-s8HzIhD3Mk+Lc$^l0Pxb z_wD=Pctc;|$0~R>L;8X8P?8Gqbx=f-juK;*=OHDlHEVm%sXg~BE{6-QzgPpx!%g9f z=7WMxze7{@hptLxQjtT22T%ElfIxa_1~tzn%h~Qc_YFASTvd>K12Jon4Ur;o8Vk3<+$fOj3Yt zDZsb@rTXo>wbj8P-O6U0bhNGP*_ey#)$ZHm4a}W$dNS=o?MC( znYMDa37qYxcr>uSA}d$2Fa}Y3zG`lM`TCGB@67VkSL;y+W?L67M@%JMe>B;F0u(bF829`p#h7 z3zwNoXLq%(FT?;Wl5?Qa_35dMxawvNBYHMANUmKfIfJ4k-hm`Jq$*JG&J?ILNJt{I{_tsG1xc9cd~(4T8lPdh@{5jys`}IAZeZ2)o+w)hG2+SN32m zGNTAD(ALpWiz;6%u$|d=@0!Od+PiD{u)AM=?Y>w$AtAv5Ob#Pda9UT_C@DW?xHQAz z-Bhtgg8KuU? zT!PBkN6%fI*^wV=p)|IcD}3+|rPCq#dXAwila8Y!NBzPzGlEeUv#>;NAZ`g_1HahV z*h0C~ix(%*LErvx$nfsS`ugd;l6(&EdU4NTLNoV%UjFc3a|H#3Hpl@|p4w#sJwPBD zZsdS~`z;B|AsvyHdJn56svFXiu+CW|Yg4-;zihKeSa{n+dI+zXMe?38s#%4>*^FWz z%{NPf+xq*3ULSa>FY|R`wu<$t!MYpO>Q{_(*#~e2-}rFoviGucZfB02PQR>DruP-4 z;=H@2VJM!jgp3)pxpkb`e*TEqLPT0HvNHt=*Tuz^?z1Yh(PA5TJ+$CyT3Xur56kEH z;hc3YU93|7tP4Z0BeeZ- zK|JW%TP9Mo0TZxR66XMJ5OLWVrw&_MSa?8=;E9H0dUxShp?ve3}ZI6z-28;ci)#67;z0RO8tE3i*!_OUIr4#v9V!aj1si1M| z)~!qoZ*OnU4l5j)C(X zc`65VK283p#MIe!%15xHkrQJ3XOpVxe<*^B!Dq{Xw!usrTd2M9*`Sqw1$TlG7@{Tl zF|oVlCF~uszrDx5v*Tc;E(3y|o1=H*M8$89Yquye-AVTr8F51qXqNC7rLCxV{KUZc z?BMr~GESd-_jEB1GevUgd~Lg5A1cb0p7j_EjY@U1&Ov|*-+1Nvvz zi=!Ai;pMx{vqkU9t_EM0b2(Tl&%CEM&Yd?8Gg-TR9Ju7ZCo^r)MvJ!~#mqf%%r$0G zvh$c_#>>0tnl-m>1(`bt+-RAJ&6SUHc;6`Mg(^oblVest_KFWH(qI1jkWEyld}{h+ z9hSod4fSHAIr25F7O#j{J86MNn~AS)TDLqd?r8S|kBROb<9|dF1A&WdB9XCr?qeE{ zC}&f|facB?HGphb%n!{&iUK5+UMQ^Q&@qIYU8JTp1z*VX^-JSSF&Z8q!a=zURw`oW zA}z{1%o(c6o)%!e{!Q~HKPJkuGN7qkiUWYXL-97>9N9R*MS(sQXxR<1ULbYwYcb$i zVePD$^AiP!FT&BV+Ob(ao{r=`<7ae)#m>n|o2*C7?1<-QgkJr8&n(w3{sN1-t*t1g z4@G?aYJp%U|3x^Kj!+;3p*-;gX^`)X*}N z1N9UU|2*+q;@cctuRN&YaM-e7y$Yl^`+!WG;;)R$ z?Ew|+92oe)(BQrg6~U#eS7Y&1N=i!|6R{pvj!&F*#tfzYpFc0)A9JCl;aE_iG^Sw6 zgzm#1*8?W{XmZcNT}iq~KwgwlfX3pJDyuL2WcO1Tg$Z$G`}W49M~sZ`kzV{8;yB4r z?7Tplod{iPI&K6gPM|223=It(xsq>ucp&fb&e^>h<#%#bv^bYP+IRSEcH*Q5`dNjF zZL*nq5r0;WGuq&h#3UEw$7Zccw9)KsJ*i-MrcxGXfH8srFh@BEQo%HEV?b%hv2!_U_rwt*HZB2g3%B%FV+3hM{ByP$nFEu>Cnj8EQvHMztx` zVWf~@?*nx3nwhc*G3kpB;y^yoZhdb^$8$lM!2I22W@b?mJFc{%R3N4VJ99qscZs;e zZ%0pIw?UUrVp6UXTjCE*9|P&yG#V^o48i-yMkT(Qx-%yRwc)N2#z_}B1& z;tE~tjfv2-G#wCL#SN=2?H z1dmNPPL0|M+~5fXG#UhO2E!vAmnN%7Kib_8p=nOj5b()S;)rlbpTR6~K<)UBB-6d^ z$~d=BylJ9U6COj{xirKuZL-{5;2CN;U&&B|L=|)$CTo1XP4z75?OPpLph&p0eL)7I znDM0UV`c~)p)>=AVx_p}P~ys}RqTO*fd{`mf20w+Yvzx^&JWfpjU6x=Az=7o@l%{o z{@3RFeH{jiiMiiEO&6MdG;%&)dqz*-?cZi}6I9}I>UzSWB~DIGufd=F1$g!pjT}^U1p{B7vEWWG zDbZZAE*~&t)mwBH3UlYqMdUe6rN&5}Priyb7w>T|M-V^77pxQ$3$PqUolOcB7-*XH zGGahHGhmWe$0$L(3w&sr%7L9b+g`cWI4>^veGWE@V3p?0*OO@F8UHu_4;-lj|0KSJ zmb&^s@B)nrj*pTzZ{9e(c(d+w>RU`D1>3R#Q!gQ-x(K}?-OhLyuOY?aMhrsl>Fx(d zC$G%rMP1vyF+BVl}ijU!&{ir>t}H&rdXc#rPuRzox-X1}kEAvGL4d zN|nSx@px>oy{MsKXx8!K?wD_8zUDvvzG2;tlaPkHF!=NxrmvrS1r4c4=nBwmu2Ciu zs>=Jg(oM6_>N7*Sh5(0MYop?EO(PgZ^GHoc-R0)>y#tj>@vuOx_q99h`A+SHm)~a{ z4epVc?VNIPyH_t9+r9Gwi5!WXL_{VacrN?Oy*E`;IUSdSna~EBg3@aA`2oSIosxYi zVGC5PJeQ-orVMM;TILF}nr|XSr58q3UCXG85K>vhAf37YqN6jz?&+D8Q3wb~vag`-T2$ib#53&;aE$(f1exoLrTXi7}QS^TQ8oLyP4&R zf`g*dNMjFr@~unK_ATq-lzpsw7U%~$n_H){6<=5$(l=0kzHDxw-={BMVsRZ3Ymjm# zjdND0L%H9bzc7G14+R|qVGONX65@5cLvGz#g_eqmZ+-j~g!^G4){VydOHvT7Ai~hy zreX55_Ved`buVc#lMEZA$-Jmqpc8l;=P}gPM0zpo-=SxaD?{o7*m?76w$vCx3R(uE z3y!mL*QfjLYNKxhjyX8gsgG6z5Rc$UncR}6QU18CR6P2ZF>J;Mzq2{))V;bkDJ1G; zV)A-I{Y|s90Ha{O{T#Fb@HmKqJpmWS@TCAaoX^!(%Qv3jfVt>jI7}+w0$738Z`c*6 zse$t@nBp9vo?ul93k~JQ$$&-fA7e41fD6xIM`o`|Rn2h2NL7Z%-ktY+{g&%PY++_7 zAuC(mI#p9$9diBpf4|4ZKAu&>X$ZQG4jY+Ayz9RElw`?U;Os^+4$Z%rFTE#yPI9iu zv#-PX-#QA*F;g8Bz3AGt1MxAZCGu6Q;yb}Mx!`=0e=*bFq<{xRHfTz-Bi}x;jVSY8 zVdc~#D`ZL3)RS1TaWQWD_?&<#rRJW~%Inv!qVr_qb;6D&_q+TF;aQ1m65B32ab(3R zQSC@*N^USh&;B29gh2t9GuImHi~)gni!s=oxdyzGGZ4+S1NPDa8VrtdBu{oC$a^!{3}I4VfkJ$(^~(UP095=g zPdTSDe(pEzQZ87491&mBtWq3WM(DH)WKBG?9~zkf9_Dh;9G|~rW+PgZ<&42^r&@Y> zi~+Q8igtjSL-m3(l&Y7Nkq0?teXKkEoW)gdwr)A#?5qo5H~`>d8gTX2M!{z>*1g@c zee3Yt!46c^)s;gaG2)qZX|PW7qRp)+<<(he4N>VG1BY+C_FdQEJAjP<5C)5IU^btt zzZ$G{k86QO3PX?Zn#f3$*)%UidwdBWUn{=0S%K#xEB`~!l%DOls!W^?t_Q$)JV@C` zO-joL$O@!mn()fdU|q86G}(R5Ij_7o-fGL_G*)T}Aft8oixgcyant&Nx#j3^*N%|X z5PZ+PqLreP=RJGn65l_Kf1)rlfSZts0kCD8fx&z{>r2zev6@}ru>#fdFAM(GJHn7e z(l5EbJTok@|2U998j^$Fk*41kPLByv=ZE|7;G=yYH5O5mgxUv}AE(;sm8!K6RWSKB zZ%$;(%IMQ}KF{VXnVwEZ8?3C9nZ>Yy3ov`1$)WO$cn;i*aN6)gnUFp7%As)PTm?@T z-}^3$r$?Ev$iaE}fBhS-nqazZu+B*5@PZv8+;h&?tmmCk>zMQ!Tx>jbB^3xB+6x8) zR!qm5Us9)A8q5}p7h28u^tW`B$;=A)Ty*E389BLkZ{r3Al{_7JrTTtYsPU;NtsGyc zH!A_D&@)GK?4l3_di)cR3fNJTakArF%BWQCpAz_9z`jM2`AVNHz)R# z*@lNbK(AQybqj*Mo6>`Z?!VOY|2wsTC>7#*08f9^zmcADk5gi*JxW_KW8+gV&JQ?` zL^+sYC$JEmzk0x6@al8mVj6r}xVk@zw8wJq?s9O}yq8;xqk)NmSI@jFg?DJ_8JFiX zi-pTVOlF#?H7^8_MuaLwCZP*&MdxKOut`CI7ZMA*_KLEyAW9K}|7shsC5=%r?4bD! zvjGsA)R!XryJ0_L5TI9*6s_5?l+yrzbh=0bR(^Qmv`7%G9XYUUwAyv+!zER)( zCQ2rnZ#R+;fHDkVa?iOfJ~gI z23GYZf3P+390bF%9JdYmTngXKEO=k0hF%|t1!yqRVbJVo4Q@jdlK^l*eXPIx`}-@- z4ir#S(Sug$4{L9AV+PlKpIL7VgSu}Y{lnvB?P&aG7w=zW$i^yzsFM-uXqnzQK~t0^ zLpOMh-?!4bfHS`r^$B@-fgOpes9a{faK0^E;j_9IK2DxTd}5O!05aTFDf?dfiL8^~ zcJY(+-I|*gkIuhOOe_Bc*S7LKpbo6PlbW{u!$@FM2PrsjFkja=QtrB^y^GR-{ou6z zc;gO1k5_%Gp1942%L?BdcUh^Jyq?R?=oiBH3c)zw0Ejv2im3?KEUT;&KA`)&v6DZ? zeqn~HSI5Zf)vH(Q!b6K{xa#J;kf_fal3ZtBT>gpw@&Kodnk~TIpieO~ddU|iw zJuhoN=jGS46NPAMN>Fe`MziRgBffXB!~{I zOwUw5MrYlP5gRn%XhDK-ZqSrDINdK!^@%$^yJRRQn#lP9s#-F{L%&7(Aiz3Aqi@sF z5=GLtmX?;Jw6rF^*{*pw+tMOIW4H=_t}uc7hfJDjr%dZv@1Q^%oC6BAf=81l}j>ga}3?~11k zkB7|Syb?3ruREH03@8}M`>WF-5%5}fWp`Dx$q_r8pc$N+{`FcLf3bK9S#O+X$z1u z`#Gm%ZTB6**%xu=?%nQv_`|mcQ8}g*yVP+tTX&Zy zSx8(vv3a<7{_c$Q&Et1CGu?I$^pa`;hz?fD%*r|0?-LP8Zlfy3Ay>_v5F?cz;3rpT zaPe9;$2y<}`;^4GR}Btyb_WLrvi?Sb3RWECXtGyROs$IlDR}?MlU3@+ ze7H5Oe7xTNq2zhC%Rk-z%^U!&KrufT@?JJ{cPt9+7~4NLP;=k`3TkyLtL5b8Brvq9 zW;RrQyEJo0S02Fl--CliAVARkUHTgO^>6y)+|A#l*eUh6g@JT+V`2J7!6SWyXcyL8&|e)GjJ9G5ATdXO&3Udoz>jfLX4o zu5O&>KxN@xU^5yX;@hKi<3!Py8(3o0Rmk?qBXz91F1Z|^gXCEGr++gu21j~#Qzrw( zrUCo4TH9wkMv>>Dt|NCo2A$WnYeI;TXiRxFcOX2!lI>c~wdk>+6v5?EJamDsZ}Gxv zyv$p-ep5#dmuTsZ1RVZVJON3XNK*&eP-%N+Oi9G82JUZ%ii*kyetvW5E4Sa94l4WC*2B|Nf*yN+hH*5o z3hXtn$=+L#6lhG1Vnh0BG) z%F)iqu-S{0IK}|U7YMGapx=`Lx%ERU#`$JmPgxuiE5NM-2dSv7EkjE0E3f5fC&=`K z#{9l|1qwYHSp#dW0Cz87N;U?9q)gUI%4|={@0}=IkglA8(_9wwt=^IB7oknplRTbS zcw9cQ(iKl88x#6qaG0Z^4&1w#eRcDkhB`&JW+7e)gCfX-=maFW14M8%k6YuY-|UWF z&uMFt6Oic!u#!Lp6Xte{ZrXHp&)0b69*ggP*Z{3Fd~xvpVExuH=H`B3>6D9cVP#24 zYKi!y4zK#SisYl8BaaA9hirtXZX7$8gqm=XbNLB%E_Of9fv-rl3*L<_s#;RG;c?Fg z)`%-~P<~Sm^SL^-9oPUP6=RK~^#>K20CkXn3Ke2dSXkAXq`|Jp?MJwL_IwBunpg&F zP3u}ty}7Y1)?+rZ#X*xhiMsJ^7*wrwk={WagO*r5${Ek8l{aOiUk zMFgxOsHmh6KR)%OsO|wyeYuA3P$g$!_j}Nf}LI`DBchs)8 z1uzn3MQ{f2=~qL^z|4R){&k41eT03w(O^ton%>6sGb$Js)c}^B4n+o%&{n@N>b$;> zAk0GVbD&B^HgC4bIe!NSQl`Z-c^pcmpl*p?Aa~4$vtzO-oflx;7Ptz)1A?^H~>xR*OXLlF0XQQD%f9kx_0I$hq3v86C* zlfgRU*8$zC;xoxksL9aYAd4waeow&E;&eq^*ENT_buBWCbt%3bw=a=Cc-!m@Vz2`E z)<4?`W*OR0hKRDVX^x7_tZD$CO9kwE%aNEPmxIJ4(v#pWfX2--(l7@N4;nKr2Fj#$ zNJSBOqT=@f2xJFM*tt1lp74nc%a)DZ=2B1@1SMz)&s#3e0}!dympS9{7;|2ucsQ0 zX^mb*P9GNxYyPf#RSLZD6_K!b5e+NVDM6-&kI8AC)%0hB=E zsL3*qJqctFZPt>5JCQXBwwXe*bRM+tcsXmGQXGY5Q2o|BQKVY|AKC^84hA`Y%w1UG zCxQlmC8~L#!OH)%dj%qg{6sK=sVhPt<4xr+{<9W5eygXNlewqUSD|czcSkShnaDXG z{00nt{dCO$7C=gFgduw0+E06V)XO)xwLq_VXO(#Zu5}|mI&KSWdi(4k{otPS-R;0* z-l4sYS(2e-$%fK+Zc?F0$A_`7$szHG!`_h(Ao6Dv!D`MGv%$b4&r2Q15qeEQP#ws- zubkI3?l)tTNs?;zS~5LxY=Kd;YFh zh8Cz*tOMoDu$gx=H!v-K+sGT1ahHBzoX9x9j9-dNVPi|y+;_%SvouKah6qkEf51DS zPid68jEszCg|$6t{ZLF&?~Dq7W(E@z2M84;4EyhaTZAEl#x26(F``Ue1`@4p3b!WT zDqk7+oZNGfb0S)dk(R9$GEp_q*4*+8}llpRHziruen znOv`^$j2B2nr{z-Bu#f9>K32*#4YqW#GLA%r_KYT5rtBco~kOPeyzJ z7Z>Z=h3Z>YC1yx|3-~R#7&Ag+ASr}GMiZAF#GTPtbDMGUAVWq2fpZVEXqXKe^WQWR z6RJWAyFCU443bj$sNTc&S$FueBGGDy#y#rpK7>k87Dxd)11`VYbmtHKzzx&tKWFOX z8B0O&?K8Uxq@%hQx3>mo7bI8Tkq0b$+V=3_!}pyA`{u5F=8jn)A*qsIDlVDw3!$>y zEZG~2X$BAq^+tznKDI(UNq=71mFi~XKm*V}>fx~SkoyI6kZvUdxVXeV{ zo(qV=vCMI+>{|hJ6k16Kd0mu%sN6RI(SuNNA@%_ZSzgGZC}a$34i*L~ZJPT)VQA^Y zw^sD~%my=6UqzEEufF zPVwWQavt0q=5s&CCH*Gpi5PKWRJ%cvY*D9%2@5>f?&b7Up~F&+#Go8bsvb~d5625H zc65_CunIVz4#)+x9-vYWpp=TYjKn!PyPESlytK>=W@hGQovz541oX$TRVhpi##1n_ za8@~}w+hDIiyPALN-?Va1%gEcv>MEIj(_7~yk7m@y8d7?+ID^n9n;V_ws2$W@m$ggj*{*{wMEHVnnt{=Az%j!}jSoiPA>%4z)b2 zp5Bwrq*{n&asN1+Q}&Kk53d9)YAI7p=M;V2)TAPbwE^8n|M396*hvhxD8p-HV&HEj zc@>CgF6mb*LsyVz@9B#deN88O2ja(`#(d$W5HK9vxZhc}1f`qDj(lcAj4jT?Wffzzx*?wi!RnYo$ppa-n zJ@-UC-ZY7e3K;pEKG7#e1l*u1rE_Pm^|?Uka#N9N=efms@?rd7aU(YckB~#h>6}5yG8`(<-H@!E4wFw=VdD z6#Uw#+5@4X?uSngdW@jt1siMcu0rXeSWnpE#iwH$3)pb>k`xfX05sK^_N0wdXaA1$ zzAF2R?Fk?a&<8ST?mm$i^vFq{fakSeLqV*o2#lhUncxu zS~E6U-^6OZ&eiqNiH9<1jr|*Kkk|gBN%muWs1S(&{%$zHuyna7k_ z1H&zpuO^w570X;kwjZtpJ`LJX^Y>LLHJ6LQiW*zj>^}(rgOReAOP50wFNd9hQaS}7 zG+>_+EQ{|yf7a;yz}d14Pf-g;5zQUI;42FB|;3Qo)r)m!q>~T@6X6{y`L{|*1_eZ#IQl) z5B+XHov%MW0|p~&o;Qbv2VA>$ZA$7q(qQbDSWr_4R@(^Tg@QLbW=`qNbJtjgA`~XP zg{^CAT&f@;Jv?XC;yTqONhs2Gd7u8wpQxspTZQe%S*55(EKrib#eX&qsc+}ZHSSNiY_1L zBRY+O5trSV0;U9-PZ-y8@RKL&v%;hGE|`i9KJJI|9<2c$9?)+wN{9e#PN9gD`U<%X zn@g-Bx3*5{8`lK36VT6@@7A5XmYBE{CGRWO>8lR6f&&5&2rmqhRr}nJn4)1GLXxz# z+0fsU3I+d{nSrh)82jJFG!p)Nk41d{QKtlV$9!)8-07D z@0Q%Ru^|qKT+*)0FldWYm5ohs*EMi!b6P!v&+i~}3s=YH#c<$C90 zsJxgMu=+IgXi8bS?zrgDi+Y!vot?cL`FxD15AzfxjBMqD77LSn7rhHJu2x_Unwcgs z0EkzvU%!;Wy=YO%l{b|U!8nB&3Bp&%R2IV4seLR27OToyqjDvBwU8yc9PY>`|?F)ESzO1!IF^)NV$ofpNXFYVmDgIUo(lZ9!Sm zctLY=tYxNGPfkIg%3xr!b^QZj>;m|5+i@|RMqUHhwn#sDBlVO~x zgZLsC9d|N{;2v@4H0^Q2z|}d38wzhFX&)}z1A_%+B8f^ekLpgWA!|MOu58z2wk60T zzPsw=%Ea3mhi?Hd!;mCJMo)AdVg2N!LsizZ!=ibMF)pXHNHx1|LFji;%**8&{&9{XF10)a9cyxT--lMKy#dqgJ`3VpuOlePw{3<8*P;eUTrLh1XZb+I z_D3rwd>UNR4JCCDOaPcQ#WbrP*o@AFzV+vcNP$5b`0MCVZz*<&TwgSEY1_}FT>%Nrcv8m|F z&YXFk)bP6iz?0M8j=WbB1CqN^x?Rw|V0p0`@tF&YF7N#P)j)E43QKiELkxc{Y_P&p z+9{hgcg~zo#iK@DvK25M1P&IqD~D&5dz^IFWI3V|$15bry#5X|ue3z^PU*r5z}8S5 zTx2lF(xAy95ah4<|Fr;E3mVw|3>!$s**Ysv9NC*`Tsj4ChSktwg@KYv=2i_EDQuk5 zkSMhBQ0gtGv1c;kH~Bl#v?EHOSi6s}K+QnE8*WRIPg1)BEX4kD92$E^4c{27uM+*d zPfbnHz-DaGU!pk#9EWoZ$<-9eMvW%?b8gSV*X;ZFAFy`twz*vI5QzXa69LI}YJcaW zSna`mjw2)xks(ER=FwXgWZFMQY#i`yAZ}kfOcGhh&Iys-`W}CyXyw;FOLKPT)$E;$ zf5>yQWmLLk`*XIT;Vd+sfMpTGEB6U$>@35Dzc6eY#)?5 zOTL~(iq{&8r@pwhdZP;!&zj>5!n$T=RGo-jzG->}ALo9GGnNw#V?}X`;cE&nWVffQ z0M^XN$oLMmXm&EI&-8@LFNmcdVn`MUVJ691gR$Sg>tDQV_$!roj5Otx*o``^4IMT; zmFFjVOQe4TY9K$ec0S3F)Q{cVH2bYAuX-Yd8bt)bT2mw`awal0UhQ7qFgyxen?|De z`7xu&q0|nr>u)z%+waS67)bN!e0VnFb>@hGaob7o{^Cw94SgCX!=M6OfL*A`Axg4@ z&ANY2O=$u?B-;u7?I5B3^^|&5^hKv@pk!&oI>6%tP`c(ASDe#gfvhERV@-`r!X2LR zsxr<(zsGbY&y~&m2CbHkC&usvHeYGA)LHwm`8G&-FQB<5r#eD3p)`YUc@B!5AathZ zRog1P?fd+N%ynn$tWQZrSJo-@j(T73JLoDq_{Yq%8b==bAs)DFY>Hpfioo@nH|MNY z@Q6Tz%?_Y4C?Wrk&nfT?_*x+EvG&^a>;4Q_ZVVz_eRxZD9ohv&I1^+4V^`bZ`{<<& z+!b(-5C)Hj`479p9SjXK9w`eZC^7M^YBkK%_1s+MH-^8QIzEuzTaf^VSAlFK@)Eo;~lZP!hc1ice~vAto+?gY}Z|o3LOH35G)L$(!EZ6 z2$)^cq3mfQPa$Mb-_n(}1vUzFc_5q|#4cKJV$YOg)1dT3F%MVA8|6V%haG_55IeFB zh3TbO*_wu-v9U5hSCwzVGS{pz{JJ5+d&@#V!;qAqbe!2W52~!=Z&xLI`Qa_yZ}CEQ zK3uS*bwjLaQ>#M$ZW}kZU63mi4ucLyVbx2xd$A~R(46kO`(%Fl$qu1;%axUtvA<*x za4Q?lkiX$t^!4jCLMZSM?fDDJD&%-WMYl^-MyHm2l>Skz-)Y;Sv%a?*z&xA@htkuz zHZD|g7MXc);<(86i@z~hRu(T9jlCSoS~A+)tpOQI;pDlPx`0+D*zpgJpWr-!2c#{- zTk>rS)i`vvH+QHp3Ss0;4|t%YOELTLrlG-j?OW052pq>~DQS#%UV{kVh!sTq8-5{4 zok0$3Qv9Mb2}T4AO)?GCypcGn*SN&fnrI1EIZIwR-q4o(kG3YAlq6p%m*W>iap44@ zvY{b&#q^c}HDmYv8=VeTKFIy6n~c@A3VKPK#rxHdC7c1iHPW%GqwKV9d_cqB^1?OZ z12kwABoCxI?TGAo_oaHML1;>eN`2^q&~cj;?Z0RDW59LX4e|Hb;|UoC>%Y234Qur- zQ^fCvHiMF*@#e{f%p$M3=D5|7@S(G7?f*Oyi&MM>aAdxYP4B=Wir68~{8`pGzMhoDR5=9{6ciMn?U{~Q z^kGWL3+rS=%&hsujDC3%kaj4XSK{ymm%(Z|%Qu%b6HWPL+}6nb&TbS8{AG#f4Ui2A zXAus+q&w%oy6KmfkjN5sAqenCs^vrxbSz^bZw5ZHxeil=bBn z_s5qfJ?I(1!wbptI%LrLA6huGNm|pXkaJf4bZ|c&`c^c*sNU>NvhL*{{urT^N#XMh z8~zVvCj@s$T1xWGHO7S9k6zt423=W98p>V=gf4vE`UmcQY(t><*+x}RZ2~H}k3ow~ z6MFwW1smEI-@0_kk`>@3pxmA#8i40&v9>Q}cJuT!Zjv_q1j84r&7KZ(gowgkh;AtY zs&yJZfaYvFpnW_5I1e|T=o!?$66W5q8*n=-D2snwKdW22Zr4$|>|ozR;L3HWtO6mrPk-a00?>Eq{MR7P zGSS51-XA{%;HKFLQ=|Zm0zt9*n?P8kG0~VWg3(dwlvGuPIeO_|jS*OB*)#P8M<)s_ zKKE@xv$K0uW{p>bFLKVm8^tz<8Y)z#)0>fm^iPuV=WTx(uqwnc+oGKFT)cH8HzPwF z3cWJ)HB5YaCuwRGU<$Uv!vKXOk2N9C#qdlH>fs)xyIncF1&@I1 z;Rp4mJz9|QI_=tw>g;nq<;4v*W(N?q4@8BO_32vU1Bt&@;RJr+b1P{#Clb4@a!)- zg}CQnL|%o>H=jKVB~)S_px!cYcK}A5_jzdK8k8_P2rL@Jfbrs%mSpii6|AiXxm$O5 z*{H2^=s)RPWO#+AZtInrL;cAkFCggcMS+GR0MRMZGNRjr6HDjoO3CdIZba@lW`NtM z@wZeM@4QoG#vMG?Cg+iyems29G;S@mPrx+iz-fZ!-i#~ zO2j$|LVh~UnCZgxdw%CFd++(e2ji@Gd(RN_%b;Ov=0i|lHasj`yESSuIAF?gRuH4p z(Y8ZrDPSU>$Sx?dG`!7vWk3{a!0+Ifr~(F*)+BWE@!rr-$#}<-S(b6G7>FeZW`^c4 z6|7;DK;zJ|2t}v&P-cgroTIRN=ct4y$7L2q9f^Skg(OiYcxMos+;cJ@L3{6x+n=IV z`V&MT4E_|WL!y)jaA;k@HZoz%i;oKX#i%ydjd!N60J_Ati)B3t=ib|g&bv{Fg0&KG zm~1E<8ygb?k_Ukk^a+G9|C`{4)|A4u;@TFzW-Qf^JVkZwnVGFXI&@YaL><2H8 zr$ZtHczj;tprBbYBXBC=lD(gsTVG{y^WQ$Cd`z4=P+@WRmRbJV&>13w?w@K!3L^)8 zK=mOdN|EpaU61CW0d&BCS6>0{Kx;*Vr*J>FQR+f&wV5Wz8B`Z|?IR!EX3la-J+6X$ z%XSY9$e_Hc%TBV7R3Lo=BNYVR3`3cA-trgWpnxwFH*>*56zqU}fAIwP7WK0QIuGV{ zUewAk=7C@xPfrBt8w7w-mxAaB%+u72^i4FCb#!tfg2P>`f|R$RD}J>UMq*|5PB)Xl zSy(vdl$Wtxz(qOlZ>5v3 zM-;7)Ru4D0zvJ0O#zWqLFN15{oY#fTa%G}+7Rq_C#nZCEv^Llz)5KswKjM$7AII|< z+=e<$y7w6fx@B0~;E3Wrc;B4ukah$(6NH?Tq_ewYu#kPmibj<)?jQZGseJ@{g_-eAzN`4l3iLy5 zfG-Ey+VZNtefy6ZBT6&EyQCKPBi}>xwro37G>t5dX^1l&^jLMcOb_4&zbG%(HyjcP zlJrkVmxP;r zX3t>&BtOB=z)&Jcu?9#<6DR?o7ye`VKXt+C(zrlq;iiz9M{{@XqaA(gR7*y|Yy+l+ zFSlLyi3GHWU7xKh!S%IX`akKjLs)dg9*|8*30@1B&X`D7F^BZHla^j=V#(3>Or$GO z?8C%`?6VTr^Cn|~-em|&CD+_Pc7#wT*Ys{;fWHG>q%Dk+AQHbeS0a!x2*`nI9cot4 z9`--~_4&~}{DY@hhETLt;{_ZUFtLC-?QOzP$;J&7B0*mrm?!MW{(w=b*Q5W1^L(Yc zT|3S8oYweXq}(hgc{B3%&cDxzj``To9{=Krd!gI>hH7uXykskf2^`9ARS!!5roVyn zuaJ-kiZH@~2;nc0%g{8811+QAfAhA$EyDIq1!NULaY_3cT-;!LD7$d>a&S2A?(W#H zl);BHr)@`wq!fTOE{1`J&aX}B4*nJ=14d@yw@I>k>!)U#^#Mzh&=(Z7q|HXzLt(%M z2@Xgv&TE=+JCvMp-W?$a`n-~^3({8EpE z9S?J*b{F%$clU5WCSbqS@VT2D6IF@nO#EQV(u!mE@^rZakSVj*jdJRLH<0);SXdBe zSKz+8Wk8xS%lOu;m-xy8<&o!5`^z~;pmo>ao`Y>6VDfmVkIC&6FglgfgDK0qhYEeY z|KNcBJvDJ0X(>#S32JST<(Ja`+wu!TsxeB36+PVceFZE2obld_+kXkw_VecXL-`^H zXsk8-&F880#rHk^@0A0Hg)W4K(BfrCcN9>NIynrNFJC|h7hufpSNWN*uW(8%QwQ<_ zsvh+cNl|<^$Un}Map9kvGf(f}ImplQxO6+@BMip>wl3|$SWgB6;WDF&&J*Rc8QGk? zGt3`8?0_Ty#+BF|Mv{F0R7$TGB=fHlU)Lco$Id12dV~W@Ko0S#@3YNgoee)`?Im6T zBls>iYhONclM*UWNL~c1-HLzkdF$}tc%+?)<;QnsLod*{GOFX@(KE9ZXs6%f6T+hV3LVGMp0}bj>={2F&bZZa zQ#%bR=zk$VFe;!@ePxqWIbk_93(=cFZIWu$lprV~`r8kpy4m;SNCZAX+Wq^6kyDKa z6;WPfw*}7b%xq~y)jfxetpu?ffKnI3tV4$vDhz#B?(OHK?(pviK8KBh-f`!y=8!r8 zvbTV!+6oSsWKu?aI2MT*Ls5<%oEc1De+C+jYY|4!;{YoB7`1=R153{%Q=_Rd@)+TGKQB+dV!X33?Bz!Y z!eINpe<%itb_fj~insu+kroVj3vkq;>sZ#qIow_#mbynT{N3!S{!cuh9bSVmCRhQz z7kw1U1Q;%0?{R}UJ(~6D?XP|o>OW7`)qMJ`f9}w}%oG63=-R+XzHvwcz?6#)5svhN z-;+Bd&(AD4e6bH>f}Oiu%uBNT_B2TIOg_aegDis}*w#?M3>TYH_f#!!9?3 z08a)3ojOnr4fOTC_;GuydkT8|Q261?leJB}<1}!JM%qhhbeHP_q{-kTtLoewpG7Ge~zrswJF>letI_jZFc^|MTuc`u)U>9k_9)fUHjhrgf>W{0Zjv-RtK65#07mk zl2C$Br;zX9hW+r)xI40j2reurN+O5!JP4pe8elt5K|61jU5Ar#GG7P}e~osOe?lJ6 z2=?o>Ek0Im$5(9sF_>-xK_f6I;D`sJru8BG{pPaeAUcvaVwRbcQ@ix;}4Ifvkl!okz+aa!)cA5FNMF#5-_b|wGNN=aCb<7 zm@P@@Giq=$&V79Q2+FKi6}Bj9!`rL>076~HuqgK#dnG_ZzM^#E-HaIBC{sOL=F`)VfMkt4Qp zX5Y)ukvLQTWoL6xK0+d>oHatT_*Q1L#J!Cho=4#?zxw*ZP~X|%h%spB!&$qv= zO#KQ@)QNuhnfM}d>hx{vLN!FQn(eNHBgh7AYd`D&`*Y6>JGp*&%!MAjoIJjSyx{$! zCGZEo3y&X%o0}U_tzva@JifR&USO?EwF7&5Ln7;NTy^IvV(=yt=6`kOu~ z5*D&PYwYnT}<-23;}TFL0V_X|>fWMfIK zd-dY})?D$eRu@j7R+Pk{@d_xR`7xuHpB`A{ltW8)w4;KD+FEPxiHvmXK4khD@zK5d z;8j$^=ET?wLkGTF`wZ8tmn|^GqiPcPmtSEZffT#TJSl5qe)%rR&c6uO;{a+~Aa*?1^0L70itOidK(eI5Kxwmj$(+u@NVoORnB&@JX9i|* z;;7Ax+YkP^ThF(RKxLUFVGsEn?W8Ot^3ToPy>zHCw}a931)-yYK-2B)cl55a#Oz|! zX6y{H*Ym}r2>PSQGZB%ZYI6hYL`fsfQ%IFWP9F^4SyM0aXD^l47+L7`b8SD zF2*uW*0>3uVpV-HFU}NYT!@?%JNz12Ze3jBW)shs$60i0w5*z~v|fczhs9@Nu%&dia2-IS zHbBGx5omTZh*fxUn4oTai^y^^vs0)x`Z0dzYXGUR&5N6wCZ1_VKKbuNe}nQ9U|tW| z-b1TO8fB~}yzT2MexshM*x>%;#m1%40|=Ai)=4XbN{LK*M84vHZBA>kmwG;=f7rsRSTxA+$v$c=)-{%aBwa=@CkR1tiSV3h{ez%j2rXWQ6$;%-SlaedRppyk@v> z#dI%@RM^^)8W{NDVOvX1TTAJ1>d%h8>lkUHg1Z(?RjAgX%x*0|>4I`tA|V14)K23` zJ~zhyV2)~t{fp;6tetLTKsy_V)_pCNQYvYidWMv?PLF$j2F2Aa1fe! z+hB{u(j3t-?f=t%FWPex3L9ji9mUjJoG;SS&n8r?X%!)d6F(k(TYtn;l~Ov9+uC#6 z{R5O%Chp<~c>uGC)`OO$ER2F(iot=BqKT6&;)n6dA zfh>r{&COOyabX-C!@gTWzMw#XV2ETMbt{KWB)4XLh(VWTdDeYp5tg`TuJH_O6ALlT5759`2gEla7!f{}bjA zFxa8SVgiT;eI!3)a&=m$EJN?O&wRYzJcp9FXsN4aUl%D!xH$ z`s-6Un%YRZ0pM%d_m~*;i#8Wm|JjNHz;@lTqsRI{g|f9x7U$!Ml{VEt$h$o#RcB5#-^sC^KnATa*@WFeLXvA$rn$|2mV^`)LQfsq~sr z`mYbmwiryXcr& zht@yInK!ggob;&-%5(<2r+B=F!CISwUWWo5(5^W^u~%L8J!e#!pd6-kIrZ8hXMX4{ zzjJ+ZV5@G3=E^)|@6t?ptuGPYk2k1So-qd^WDT)oM0U0;5S(09Yv*02H~hF$v)`oq zci>r&PmgY3wM6S<^GA*5zG_=vgGS_EvQKz;Tt~0T?@0>ZBL#X|eaJ4giEY25(llAQ zp53~$abx5{p$!ZMomO!RT1J#aS@m+!vgj;`!~TZE65;yM9Z4gS4g9$RF0rX?opwsoh_jmA9WwjsjB!9~y~3MFDW z?pP*1zVlwu)A3zL<74OM4{N8@b7K$#qlnaAlv)46V~6}rBtDsk{KgbdLY6o$-&O3w zDCIii*pdDw-uDv~yf*?Qg1T{K>ZsjleRgGr&IZE4C@Zt!sp`GB{7L z@0C#m9x&piNgJu^{NY#G9T2k8%G4yf!w*5+2Ru8hmnM2!3KjgDIT_l$-#7;F1Ee9g-G@c9y>y z{@lE%=K+FV`SEw3o*a>a!1B^@lin%qa*2hLFX0bLm?XO3x;3Ph$}TNXdAA}fxeT{) zeYUwmspRfwse9AvCAEDk2F`b#`Gtx*SJZWub9~x#=ir-U>&)r#Gv#2nqX7~ufk8sL}c=G46)FfiAWOaAV` z-o;?c>V?wie7?RsCkVzU;K`FGE132PNkoXV@}9f<;m`$tf0ibsETBfN2QwDmf6o8y z!MQ`*CI07iTWerLaBR$ES*?f1vaw zQ~=>vm#Jvy3<^4Nw-Nb_|0>V!HrjGZ=z$n?g%M-+-ROCNk)h4OQw51qySp06f{HzW zGAv)r>o2g~nZ^%4A95YEZAbrlNs4XH)QkIkAAfdt2Savo)BSP!vOTu^fSoZ{S^{%~ zY2RaW_|Yw<7q)=P$L$}Uy)Vn_IQsWRRBR1VG{7S%<^$;mcEDT_rfJ9SigNkXudjs# z9u<2!1MtZfF~g7L-l20%^(^HbM8)z5LS8%3c?~;vAgEh zyN5{dXi41(U^3|l7)NKO&Um3`Aro%t!z4W;VR**ylL#(! zy|xgl*r7lFkEZW{$9ix7|4K@ct*z|sC`}Ctx45ay&_I$=X=qVaBCEu099s5j5ve#v zl%kZ;kQ6eKN>+-j|NH9s{h!x)UeELD$UVNFab53q^*6LXmF_W53l>c4YPX}>jJ%Ed zDW)8a?$T*m+apg#nDq(FHxhm~mj8dQU!xbgWbnnKV;XY z*{^LjWSe6IDyi&{$>5)nkuyW)kb^u;Tw!#Ecs z&@UiZCMVcKTv=7e@M<1qOFaa-l9{0}4S)yGp60yPU%9Rw>|ah#n}XTX2(`X88Ym#VyT+OtO& zUQ~&_jR6U`oI*x(A41+MAA--D@>#A<*P1G)|r;D=6u^icGn9s$7aZrZ+ zf41L>B>@d2PWaS7+{tDzykh(9T8J6etUS~VqxD?V%Ehf7;#bz~LOz}pR)@bNFXfhE zXz;<{)(qV)#8e`dlI^-ad^bi6n$L={C z4u`VH$n}da`)rv##gWi>G%oIGi8!>dyhTQv5WGNf^i_Xs@bt-%f)&H~0aT|=$`SjA z-E9r;kbe;WymD!746LOi+I!zh_U1wQg{;=!=?h9A}1N?KLWn z`)7Df!bY}4E)QR5iI=8K$?&Bm>=qjUq=57M@>j_9z#aMGif>?K@mPMAn@lfviHT~l zKC0)WhHs$v1y`-QZ*Mze@*eKgz@h&{MvSy;Ce8gJ-UdfmhD9BQq=yTdib^hOvr2@z zz*t?&y7i%*d%+x4q&aVN|C+HnC|0+;?p8 zUAMG0?P=_sS7mij8=#q^Im9L!=^dZ)SQ6}W0^agCX;J%;9}_5g zsvvgrmP;yI^wHG&wW)SUbOJROBQ`5#e^h-m1XU8l_+XMA*;KG@9|1Y0gjvWb#qf-E z!G#D*NX*)?qp9m$)D4fp)~bzbCKP8hBEh7BJMq15O%QIi5G0zTDZ?1alVhM`DO8Zm z@D%Fe{alH(fs6UyrsW$8JpVS`{w3YtWk@M8DCBUPm1`T*o+RH@Ja|36W;==SWv(bQ9A5^Xgu-)pwMJxc-2kJ zF&C2l@86I6l>x>A6$1q=V65o+TzqxbnDa@}60|+A_;KTgKF|!XrWJTJ;|JAq`HL57 z@7}zz98iUESbsN867ouF`)Uz?j3P02VFPsZQ9_eH3&$pJ-MaO{fER%st*_>9_+h!W@e{L6&qDgJ^@rAf!70b*@4(ss3apRzLksOtodX0ANIRxuSx3!bLy=5`=A2Q>G?thkiwc9%pMKh)bYT^#0q8V#H zBrbRLP=eX}l`}DZ1fo z_M&`Cn3;vJrK*#oBc}B5aD>c{3(S#F9HM~k$g5Xhyk-YjZ)?ahIfr#{7)rr8oKbQ0Nfk}CP8zKXXfUo^ zTVDRj`RXon*}hNh&=EQ5{eY&HKJjdcN7?^j47?7wPdFJ!@T)KK%rbba_#f*WhJj{^ zoAfd`Pm_|8L{6VAIdSZm>#vE7 zuzy!aja3M{FGx5UtkmP?)l|2AI&whlm9_#E2tof#O*s~lswID^J}i~z#beWz(whUW z%FW^HuQBwUBP8U9d|h(QVzYCZnDF1nvk?g_FgrbNUU`*;PexP9@S$X~I+Kv?Dop?DE7G4b!D-7&DzJN zGLcpxILT)eaVcBveJ-f|^Q5}yv>$+$;rBX^G-@0tf_Ux@Y;~>I|F)q}gSaM2?|t9& zE6I05_Aj4b5BnN$eT3ufl4}Z^8~u5D1-v^1N^Zan_WNg8 z&V37gFq3oo_d#oE+ld(+(--`ac=_^WDs|83!HlH-DzwVV&fGep&W`KrxMX%ra+ej1 zhY)pR9=(F$k+Myj2NK$w9s(bK-MvM8Alg2?)vX3M{WAAqpdd7I>G_;3!Jf)%t~()) zc^S%nhQV+&UVi?7ix&lH03Rg6_MgfF9~}NPMdlqwmz^ZXE|tk>tZe>i#=k~B%QTRG z)F(p4kB%`2JLlREe3)_&ZmV|n0LA(A?b~y)j{re91D2V_B>U0k~7Ng{IDaBVpU9^kuSRh}RUI=UkG z8x9A2xkw<%UAZ0%kC7{vnBr$#dEtawf@%c%QUEiOf=T=(N?-ulK$R**OM|Zl z1SBE;^<6h6-XZ{ioQtF^0GG3k-&$xY$kd#I-^f!=#&>`_-MJ<`dYaLM@5r*vSJ%Yc zT93T&BT13|hg71af15s{QK7=S8a`ri2UsLtwCA;8GXM)dy!9mX!&?b zOiT<*e7*KJ9KP~J5t+TY*ZTL z;n^(w6IDQ&qsDHOQc$$7Y^n)WR%QL^OB^hNIuRjOPXTS1f<2UuYO`bsbCpI4%w57T zeSkW)tHx~;dlOT0wmv{#tSw)lT{Yt?3`m8 zqSjVMKe?EuJ4MbfT43qVqXR8-X0x}=g*@62rDz>WSJ2t#vu1np$f(_1i9{(wkJ9>O zV)-jPa9IMHaSpxOcJtJ!%VoX$!Ge&J61l8gD}p`EEL_KKnCL^*6!*-g*$Imac<4}n z;f5*^bcq`9+8PEJDyDzP;2N78hfWaKW&gH*V3r zmQN=3%V~7EPZkOQ%+f)LPnkpz;?Zy`+*jDuWEi9TQ9`g_esxU!_u|Ei@hNG*X;Eba z)ww)IV4lnoy*VegPX6|tRA}4}qynj4_gk?MxEZ|l8}ELtyJLaPyG_ip+gLVR)C3^` zq`^Sjg9Aua+yt_t1$)ploa(+bW1yu6@;0PZVKyl&quX8Z~kOHG1ZZ#-H+-8qUDG0_$=K+r3@_QrTDVihJ z{k-KeunQ!1bN?L(sc4!Ua2vOnDh)ZmEcR`8F}v@mZc1Iwd2SHX0bafL#fp>KT3EY( zL8iYa-yIUORO=5VnFwtR@V8U=(n6to09t8Xk}bS0N#AY~E35iT%!0YBz{jU{JX|0>Hl+!a7xNJK&&IHDB?%{sOvSIQ zPu1kqV+7j4eVoCgMT6Vl=$!BgLGMoiXp@S+eCwX~sa zg}1QThVM_GKILX;WCPAaQ?q0BBAQN0j3|ntNX_I377T*l9ThRbgaCLv8+YB>0fkp-*Q%4Fx zTELG@wyz2c8-Mhh&oV#@&>%EnF&#Ekfdl;2hr}J#&z|&Sv7-O1BQOZ?uXXf8L|j_G z^wvqu-#$Eg33Im?d)%v7>gZZgasrnI4^9$~TD&3amm#Oiu#L_jA?aln(Ly1Aegu&}f30ym!l%e@Im*{tt+vH6 zEHs%HJ9{>G=$god+6sC+g1CKvN+I+&wU|hlr(3Y^{0&8c#QkX; zWmBccSM}X7rDXoV@8>N$EbI+0q+#2GwLtD{03tq1PYi{HmQI&rML+_<*)ho*^#8R0 zQBikbi7@mzkS~}P5mhHv)E<&9H-e*GHs}i~7BsXwF=`J5V)mOzDtpl@(TF$F?FIz} z>8;N=<`)^p?@_)vL@cE@qsA7K=F)WIpt_H2Chfqms`Yp1| zcxX{5^q!Q<7jqkITQXUtaA%FDPw>oNTzS0??j3$6)B27ytN<5qaU)7k6pLFQ9Nfc_B8AaE{G_SF+j=WiMBnm>BJ3NR5{q{IHs2;{cY!3+Ajq+|tSb)-!%?-c)7;W{YDnTP1S#jfsQ3+$UH zff3si=9D8qqM3&{swSR9_oySG#Rz<5AXY62M>!cE8yn*(4*H?pJV9R1AqTFJOZFIP zq=l4q)_-4*e>fZ3Fnvudj+kI57|E7`Zo)`9omc5vLy7*dvFHaJaMspUh;!= zDWP+wYV!`i?7UD7up6DX190!qbCyS@_bz<}T>^Rp$jF?MlGwIilJCx&?QA`BSH|xB z$7DL+um-O9D_e-ov7R}ZR%(a%ba1HC1Qupm>!m4verh4z^W_w-COfR^V?FPO(gtd; z1A{dD4I#&d?1szY5*AW{AL8Y;TaoAxg=edi-H?K^=B8Q?LdtP4!nvh)=J{e}yEk4m zHVPaAFm;=y1_csbxKz;R%x&x|>fL4**)J?Iw%ZH58skUQQUIRQW;PpBWq%%h8hhn+ z>aRJh5D~d!n7`}X@fFR@C+NZPI8fBhLe-1&d^gsgapMG3BM?J#Gw`B^qZy1wn`D7k z#Z6AeeCP!(VyH3zf>?Hdg8nT)ynxrxc{2>MK;iVe2}3nFpAp239Sfsua~7peW9oO2$|!@bX< zM^>Qp!k#8oF4;XP7PC$DQNLBtHPcy@FYsfevE=dxP15e}f?*W5(LtbfZmS=7!u=hhMEQ5mu zWuRj_=e9alK2?N&m-sF4%1`LUVq$n0bgh}GPtIXJVCuCHj&d`+U?e}-{|O(_I@Q9| zroG#1CvaXSN4o0n)n_2{E~clYjK05dr8M>}OqH8_(8Z!x1iNyne<~|&+`@WV%ci>t zQ2TJ7EqkcPUu1r@%PaKxqNKZv|0^uy&=5|fyoGa#U81jC!to-fh!CSau@weVlktW>W@U`=^veU5+Ys8 zS;dTM;8~P>0{SVDnism)rDs{-sZG2e6km*KI5>xap*eXQ_FARXMc_LmV&Shf{CBV5 z^&(3%g&i{^t;`C+AuR+%hD2nd!hmY2iZ5O~spIN6+^ce+I`G^%X-vn!-~hn*l-dpU z31ul?Lq0AE!XX_Tk^}gQ(6%7GV=01nfKx-eP9udGSv@s3^;6nJhd|^DS#v)IvYaS} z10fMN!Ogc^-rAcjDZzwt&=l#WV6!)s*mbxWg*3Fy-Nk+?A{Af(m<2CM=oHqM07Fx$ z7YI__poCe2gIh_ioa}xS>KGslbC=lrIbPTYsvZ(5NEM_*DkCr2o$~mNkYHesl;`fb z^jKl9Z6ZQ7Zkw|2^q@LAY1uj5-ZLrXe)q_v zTOZ3GHxa=XYafU>{HRl9JEqJOkA1lKt>>fUW8L*KIG^8bEWkTOyM|OkU4s3Q2C_Ka zZe_su=Cf9_u41Mklt{|kcn(cGPizkr3AyWWb>Tic<+I^OQTI{Q1M?6H4>{Ru#6X3H z>YzMU@i8cAZgWSxZbEhl31b9+Sc?-NIabN=^O}aAQ~l29 zrM!kRm``V>`#U(u+fnAL$K>t$aUi3IRcZH27yUPS=rcfG@UD5FXd%3&@I=qEk{eLP ze!&0>%GCh636qg1u34bLhA4@!1o01qX_7}GDtps#LNk7Fbd-Tt!raBWcbIU7YGk_4 z!oJ=0K+){Kp1ic`+8W!D{VJNvLT?nObbR=fKnM#nbHEo8qXpDTlZT|Yx5gp?3um#< zuoSP->eK-qAE@`|^*2Exggeq}e5k=`Png1Jcd?LJqKP<57)a!lg!ioQ(kQDTR`Vm3 z*RD;(s{i;Aj-I;Qeu@42yRN9hti-M;vi?&N{xQHSro)SY7k-c;(I8L`hvM@g+aShg z*)nq-#cP;8K%fw=nk7=cten3r1$;{L{z`MxpJddeM2E7gcK=*Cr>ou2-vn|yN5h(-;_YlYt=rYc7tD^ZOnUr`8k`Gl>p)GtAcS@|~D zcV=OD-3q;`Giy+E;qoP}oBb^%GqI+Ko5zMxT@8HV=<+!6ZnM!5z$)G43!?HXJWH7EXL4D@$ z+sVwp7d0K;cnk=GTG<5FGHdC$*IKH~R{Oz0^c0sxl7C)D1BY;XEH{~>F`tKT zNPaktxGqyMIbnx^4`1hR-gtcjJ_yNEp`T#I9=J z^Ta%7&^<$DY%QbEgCq0xQl=4$1RwE|={i_`jk|Ydy-NeZI)k6z@p?0E{@!br&+Q?@JrVc+zPoA4U%Ih z?~oxeBpCK&G{JO=q4xn}B1jMy);Xd!A3;M11TlVDV7eYfGw>=&7I*~R-;!Zool*#k z74s10SimG|AxzTXcGv24g0W&v1Nr{GFD{+|u#QQGNH(R&U|e7(78eF&ix;jd_y`sr z_yG^}(G_SqP*Rh#0zqh)Co;#3svW=pkAbO+yF=!-7^qwr_G{j$X=CfpojW&0G4t|< z{c~!&a+WJ}{3_vS*nPCK7&BL7$!V@$O=)a0csaBm!0QN0B?tmAcV59EVBXkK@3bg3 z1%3l69kEdTwg0R4+>L)nGfp9r!qrm_Rrz^!I@}S~$}k^_Pg~tFC>U#=R5Qs~dG$Gn zs(`nKu9$ZqJAM`Vh4}x84`-`d_p$daw{ce-XgC4+1EOIl=oO8+)1Az_p~=&mPsr7JdTq@@{k4TG3sC#ntIgv6KQ4D7W3N&L!86E&e&_{pTkaUF&$WWl_ z(UG_Q)MKq|v5kh}wh{jcJ9v~~@mBaIs*?G}_3#0224kzSUjbwPUm2&F2QSVyr{pDT5!3EAc+uHGBVFb7H3qL zzHb@{Uip3tnh8>JH8i0X%()Jl`wv_wYAdUj(b~@njTMtB$@cFlG=}zvhG>r{(f`jpEX{W7hw=0 zDgjm59f6^3yjbwS+hk0&)~s10vYio7G*AG2cRh3dwlxu=W_K4}Wh<}1&-nc5Rsbyp z4ae3y!&la1hY1W%5Y~bkhZB*+axD^PRmNm3U-uKTKgYpqKMoP%hQ=qXqw@tkh<{T#eK5A-WeLoUV2JI$oEzAFnMtm>}9 zXiZ}vw@L*iX~BVjm(t(Ht`)VF-W~rdv_<~ zzy5dNNFA&k>%4}AHf+2#oow8zS6f?DKF{v%?G?R(teGBJcafz`a)s-$bh-Ach4UZOL#OUuBQD(ViV!axzbx3ALK_7v>tI$qwWx-V@|Y1fROvpS4$*oK+F^<+%qs_W9XL&untu(x`WovPC1*fVeeej zc))wm=V1(u1dgj2c8)_W=hB7(lm~sshpd*BIHDuNPauEUSFT3NPNAb`tAqqPE`$N7 zKFeDD{o>^D%9`-Ihelkeu6+a)1@ILoGq5dZt77BgF`umoQj(GkRxSAoP-ljnkK0nA z60kE6@)Sc@SaHJPg5BXbdmJO9mfSz~Fu7n=R=IBXdSzQdRq;F9i%+dEmXDInxK*?| zrpR|D-wN}jw&RI8%XbEdoiC#dQ;G&=o7To|CJTADcshzrszOdl)T{Xu z5$VmlKqLf-*lbVMo3RU5h)Q!-xrVC56wq1b%my?LVutVW8D*gCMAloGm&^C(FT=Xh zB;c)mjGGOO+&@YhBl2AdXnoP#o&D?Seh4Ywixk|G|ng|@2uqU1CvPO z0H0@vQ1z9)B>qT z#shR0dtu2*!UuzUNe7t?1YsP4PJoOPOe@%AD20@xpNLIv&*!ozQnJW2Ide;9u~*-j z4F;S4DTG3cnJH~LGp`G>?I28MB6|V8?gJM& zy@w{Z?KVhLoDYdO@naA}5r`$WO96;39axahZv?vrC`-L@diM%|gtqhMZOLwj_662N znkR?SDYU!1V}D6#vLGGShzr4;ii1eE^KE(-gI#-{>B2eip?ZxInL!5s2>LtgALVTVMp6a1J5mWn$82zDb;{0Lu6R*)dAhG(efO~r5J=ORfWW{z z>$2Ak^H?pC9#iJ4&NAtjHZfadcI-8+d#)%n93YJ5*BwSb=&V> zzw~h)P&cB)*;m1e@4Kh#S`Cs_5ca$XN+WuT}z0Re&zS(pE{yg#uLgazUscgIH(LS_wcx9Gp$~js8pdUkH zr`6KzB1=DS4689kM6vLt>0@9+aU`JWsX*S9*sEhU?jK%_YGS7$QV{B9v8IdX#>wUZ zM2#wf%1ZltVfEg<@9vz9)CRzeL)FRK$*{9JBPa6WT20T?yZ4kek9QBMh`;o<>N)z;d*Js#_Q>lQ zpvX~b2!j{~H1GIz66DgIp~oEVmX|NX2}?m~kNOyw0Vg@aoqz5UKk*b40!hhG4%F0K zOdF->F?8Acw&j?79JDMltS#vL3Mbq8Q&Sl@yXL?cB%8H6`G1@edjE1%6{h5611F1^ zLVZAI)enRUS1PAne&aGTH2WWjRW@woa+^35V35+D4{8Do1}qmEtx5LiI(SN12C^&7}2<*0mRvW$vUc`4qwem~XwrlJ z$M?bX*SB;ZxDqo~Jm*AQm#;#Q**>*DgK+RUTA>ko?N`v<;1ffR#HSge(lWg>K+U%Y2wv=(BW+WeoV(iYa)olv*s*%8Y*NM#tU1m|3UprAeaKZ%5Jp!w zlG<5I^IDAVthmfvoen<>d9?q!WvmgnW^QXvK{(73h(lQZSA?>-9=rK33S@mK zVaTQl0evGZ2*N+CJ^HtU2xDoe!!F50b{(*xcC>T0YfOIdmmj3Kg$Xrx)|9d|1H) z1-TEdBzgW<46bbj*}Blfg*?z*Bz!ykqXTcrD_GbgFun*d!XvXYwQbXkuBq=^u8o@! znxz2jp?DqkifJwUUy>^qbmCA!g|-oRkBll5*}~%Dflxgo@0sR7!OrL?AtTIt<$l{ZRAWhVTx@wt6*dFnb$(bDiYO{Qy1o)4A zPkZ{^o=i@RP&Zf1xpeqLVqToRsP%`JIgFso*g4yM5`9gFF3ELWA*1@SFG5T!5_WR( z@E+)I~(tQ$*(h^+_nJTGtbe!qwMa+;cw6d4nPfs2o4w} z7%(kMWU7Z7*n=^pKY;bZct$ZIIx~6mXNby!_{uBs{`GwxMa2L%vPb!k4qO2bxHX5eUX%R0ita0TCKlMiT0GU_n&S z0GvGx;W=?a)6Sl`>c%n+z>x#fR1mV)FhCNMd*F~*=B(x&zH0Z@9j9!82cmWY=B-m= zMVe9cMQGSKr8e82Pbzh|9>mazDew!!FS6Op?^?+wIn^-1jx0Y=zSVmC?3Eg&4dOwA z%O?4cZG~H32^>9@yoq$yMw{`^4ste6VxSOm04qu(B*2^k%J03Uv?M8YEUDBEeK8JR z$R1t@=zi51`z)#bvu-kB&GFEAQUsLX%c6V>?6-EzEMWIv$Bb*moep^qO3@Scm?9Exan`A-m$L&2v|LFJWScpYijA_{B{r7~N`-R{ILB7HDS2YPa= z>Y*ETE|tJ+83s+mBWRFqHm*OVjs9_kxgetNgvox26LG`Fc5>OWN9+TO^DZ#V_A&UOv9KaU^oYgngJ|=AB+`x|C;O8>y_#n`Y{G65N*jo z1@142@?HvPBMf0Ay1{NzoKXssWd{l?N+=^RyZ9WmuFx_tK;=*$`QOVz`wb z>h`L9+&<_TU_)BGbbhD1lzaD`C(P0|*5mFB?NyZ_8clrxNOsSEEJH5>!%@ zU{6`i=pkTXTS37Drbjii6i)8@itqx=3esb-NDxH1btFnhcxjauR3)puelqp@JY739z#B^>G|Mf<#rqj24D3M{2Bqun<#MfqO-rPEK;s?TU@xc z_6z*mg-bUn`6eibDi8&Y9Z5qMakK4NnQwO2@Q7d99Rt4nSIIf|e8>ND@rrOz88ZREudM)}&c%@T&+8wPwViw4GS?3u zKYUmchM8$5CSQ8M5@N?akPRBTY;o_^qe8RS@p9s&+f-iU|AEDZVMuX>1Iivo)OQ6( zGi1L{pb96402*i{e1-z#+Jg>&L@XU{RoOM{)&_-Bb{JI4FsP#ZCokhp0T8YS;RmV_ z{3W0Oq-i1t73?ockS_#724OI;YcSJ1LxA$JFq<6%xGQ-U$MW(Sbvr~))Y$3z9B!Zd zC7re3b_EJd8np(KLEm6VIY(w=$;pAoKBYe6FnkIoN^YJhR}u4a#lYB)pKDNUB1Pyi zc4&NF7yNL#c!uIfCsmT*#UShrjUJ*5{A1=!Pj9#rz3&WX#?RiLKAmnX?N_jRQ)n=1 z)h7TM6C1uk^#s=)C?&y(QD_Nh1nJ*SPm1Sy3px_6VU?I^$7gnScA}?yYkW)jNAQ8pB1&b;wo|u|?ZG5V*)nZWCmh4Rc$+^T*jV@t;HzqM$K&v6SbZKOdIu^d_ z3NsvwWSC)fhre9M0pkD`it$q~9jjOMjs-42tntzL=+7G+ibEShP(mg{ zVj*H?juaO1wlJ-|y@$5tTyj_OD!f>Z%*Lq$zY-_2LGsbD3;s0k$^h27$HAx2#G;Gd znBA}kw}X>#SWR9}==|u%+|ljt^PB}ik;3P%_IAS5^t?T6bjVN`{02mxrm9AhrF{C2 zruu@SuCNIIjo#SVaBQf29y;|Z=)&Zhj`q!npt=%|OMoH(h0*2W!l>dhRccY}(T=G# z?@vcorWjv8_**F;s_=w4g{R;U*?!DvV4BVm&0yuo^*Fh7f!Wf;=MgW9hPTN)KI_x6 zSGm0pz}>NIiazU;;(^;IkQ~7*Iq+`D$~owX5DI+ggK$ zB{}Pw#C{fg&F7mqNIZuAT>+fotqTTSCmFq1`Ar zW!S&~hW7RMM=GA_KCRZMHL%>vYtn4fxho4lPiBjq7{7kNccj?NQfYEMOws|fb8p$? zrB|FyORJo!8Lv+Op8i6F>n?_ZjEtb4V0|zXe3ren2^`27FN}>)WlJk41Zh5t`4OU} zl6||QDIYGWM1jlbXxjk-1VI7PQKH6LP-*wP>|zg`ba@tuz>832%A(keTYW?t%{!Q& zC$D=b;-6UveFS(pM=*xxcOu|Nxnpz)XD_}Os9^-sygz^*0iHb!Nu7|e@EJUayt|%I z2m5Pz4PtOt5Z?E}6PW@i0EjCBflBVzvN zpEcSYzR2$+QVCq@CorZ?-Fdu9aSG6H8y{YYnXnait+PdGJ{CsRDjPPMBIyW9|5oIO zIIS@LGSY=>77W?mz`dQqE3&^%@P$noh1-&gK~H6REd2U66DSKxO-#6+KvyfLBT{HJ zd(IEDtXRJM8IWgy*a`w7l>%I8wz=?iMg8Iar;G8KA$@a8&pow3Qai487D^%4@18N! z4j+lnsgPiYE<$o0^n>kiu}B6)U~?PQAM88#U)tHqrq!{QVrqTg^kD&{85_93q#*l1 z__Y1?IP}js5k|H%R8~#lTTsIiAJDCajSaMk_3dqoDta0xR9Ypj(Y`@#XK6WVw4tgy z6?F?F^dS1Mk-~AB5%BeYLQ~+prT`nSuJc_fr5CyG?$kdcxE+|k^P>g+UDs$EH!yM> zf!RVZYLnms*aVq9DVrOCp^%p>CbuKa3enV*I{6a%5#Hs~|FN8d91|1wcNwgBz+rAj zK1MdpXHWM{GvJ2eaRBEzzWYR|<~Y6Uk#vP>4_X{zws6@lSs^jW&sKJv7{zeB>R(|X zEZZj={~H*NyZzhSN&beA>>at_Thf_widnnF@8q+>-tTjL(`=Asz2n*ad+%@L-!r@W z%SCPDn{ClT^FvKS4D^BxD%ULgtyQW$ur|Z_<%ghgCk;3Dwx|7TTMjt)A`&@1p=h8@1=rpt za`9l^ULuM#msjbtAlv726F>z+bMVLyi=yN(CU}to91XxD9J3W2Q`FH0LfI|=NUvbp z7R9}b75APcDJB43DssQAg{+K*oP&A;avp%1fd+eid3Q$!gl7g(99HQu}$xqye2r95o zG%gK)g5Ed0yBcX@$tYW$+zoN4q+(J30$qg)m|n@&d&m+Z3vRG@Pr$tzr(RLq=m88? zzEhIZ^L2vFGJV83yGd!s6KqA?=xsPMh)%@+d}?cR7?99$|CJ5|$sw{AVi^S}nbQs8 zb`&n^8HJ^C3WFra1PS$P8TgzS{>;^P|EAs%;flKk?jXuB-^UPAjkxCZw^+$~bAg@>7~@oo*&)f=n6GQM~5CYT7X~6UcGwt zd0JNf#^psLvtE$2;kgEm`|K|gAnI6W6YN~Je*M1gC4A^bapG|?7=Kv)#}OnP|FpA) zi+7>6U$<`El!^%{PW7?%D(30@^Wb%%HwmK_Bx@sN&?N~b-p8eghj%BrHHDMuu|sO} zP4I5G$kxoTy0#In@oRZ`%erfmxS@kS136jFZ&voxmN^W)O(^0w;5JIMh{%!jHaeC$ z`sa@W1h$MHM3Z|^z{sFgTbOdxZ9yOLLLthT_k@(&~V z83+R8_3vb5wv08_|EbarlY5%&vFIzK1=AjYO&Z3+Xr2iz(2V@};e(ebV%Q}lUM&AH zGB~GUb=tfEd?yxO&dO}9=(kfKvc51U+H=Rdj9SFd~dmPa`Da%JCc& zymU&Erv3baRRR!{Y7tdMl0AS;dKje*Vh0BQgF4mMkWz>r-+Spzz*uo(n(&Q)4xw2{ zUm3xq>Zus@|9K>5{%@`GX;PkDJ}N4TX6fAB_hu%lbPZ@yJsOZkKuSlPvU6Q`yAE!xY`LrkRE7pqiTm?o#uJ1| zjy6s#3BComJ{%z)L14C%>~j86BrCjw)1RTnt6`pkbn*Ax4v<09aI`DbDlsX&GhZP(u+w@X4@2!HF@w z@8oWqYknBAr#Ov&t{-%bl>A33dueIu!tgE#s`Uq1)@Awp23c3sN zG*f&hC;A2krbwl8)_HBL9ea5}+S9n#230yb8jXxPJeQ?V@*hKZf>*kcPp}e>EilK1 z5S##zid&uzDKyHU3M69FEJw-@W)*Odag3ze*~2cf8-4_39Sg zh7f^2f!&q9LCT9okw7v2)G8p>r45y2^}NTw)ofyg=g+^M`RS$1s-|YM1+}B*h9|{& z&Y0;H%4MyBWby&L8cRFQkfOOK ztB-!>hajbTWhX8z7EpbPkj6z-dEUhv@>0Uy;eXqSh$g(-^J4$X&HxA!nL)AF$`+YI zX0YFkBKS-QS~Z>gJNx160922{?RDzb{>v~F{6p4HtV)MJIc=xSIE^H8K{Fi!ghazE zo5o6f{?L(D!drDG&$!2&wjLr{fiB2UIT5FkD_;PuC$$=mL?& z&1CKrkYl?hu z!Sm!OfTmFAgUg&>h4DKU1eflppY~EPS|dzCLc$PmP!6Iy$oPHI1GNg+$4EnKYv~2U zgY^z!FcE3U+GyfO-6Fsh|AXE&TC5iSZUmv^F z0fpr1)4x=4g8^KmTnf=3q2lSyZ@Zd4m^odL8<7xK9^g*v*VCTU(2jPn9dJEGCZi=V zg@1#k);adl5@tb^rj!yJXFOHvbH3ykO>e}&LFa@+7UL-1w2iCm_3F^q7~HZUl$00{ zJz)J}CKzUcavN7_Ee$q5rn(ZbR!kor)8(5X6)#jqz*Pfr#LmvKn4{0CIyd*@%cj?e zPS^;)Nt9ZCblvV3iTMerHk@Wp-5|9l-mQD&)|A-uiWu_I0$_%sV>8XtLgP(Ud|KXP z{!=BzDH(Dqj`53E2o`>+i$1P1aZMFnwxA)0hJZ!2v6Jtc2ZnN(I}v;_M;LpvR$2Aj zic6CJ{J;@8B|_BAm9OTv9n-P}rb^5nTc~M#1Y#m4t9u( zQQdx*mxjl}-cI?&tC~HyQSZt(dktVL9;j(SRQk8}k1HW$01`h6}&4N(n6Yb?s; z75GMS{le_6hx(2q1|?j5i(yMJ&OB7v4PUM$b;jb8bgvN@5TfM#8FK#);M z&azmtRjDwdWXHs4(fEs~S{MTeERKwheq8y2WFK8=-e2J*J|}C4YwroF(K`^U(w^qX zE;S3X4Z1jWKQ4Qw70o&z?+V0%_Cc~iQwZMW?&X8_HUJ?94-qbi)kET{Emu~bK^`C< z9=J#Qg;muW%fQ|>!Z?RhNeWf|tsTy}Qrqg^_q3F1`(1JAg-_Wg+PL!2G1;w^%0n~i z=9RBa%gjs52|i);dndLTjcUhLxwqXN1*rTM;YgvBUb-S_hzM?Oq*)yJ<)fx~!(;7s zrM&1x3eWan_h|$@N2k(9Msl3>ePE98EPG>>%+K}dNiJToA08*-%`g%E>Bo=jH+W^l zE}U{V`!ma7;Pgy!W%skIR}Oo!WP>S8PckF?(Wb@T(TA#XK9KYh0gDv1n&-Fhl7!sA zQnaaovW7J8_3{F>3p0-Nv}NyBG6)vZ!AjvVVuvDGA9}t3*T`5vRFyQdGEoUojeWB8on-y z^$t6CVWzmch$rg_rCa5yw!1(k`&cPvtkANaF*5UT@2J1_{nZ+kjw%~H-!JMK+ zXxEJ$C1_FL_$I~yC1$}vpP0(cX)EFto_%pr#85Ae>~1tWq2BviAD+R1g70+i(Q|R( zy-0GHyZiZ}Kb`{*a+13q2Ro(>o;r2P0Ua#t81G7bOKlw2u8-B;da#3+r?jjidjT*R z0LL`7@8&<}hHbH8p6}y<(T$ovP;73u?;S#c77-EgFn21VGY-mk>n4Q!QkOG}n!Jfh z8n2B!hfNTv4LrM&)=SO?V`&%s7R-&lVOaNi>|S=7vp8acd1ca$j=kC-*uUDS@LNEK z;~WUJiL?Q{xO-2+qMF|ls7i3}b1^7J&b(U`d#$S8I-_@^YmGrR5vt7GbskHio2PHR z7xLSvX%aI->}}q7-BntD%|~5b>-*kct~IPFkDiqyn}j1z zexnY1-kP?t%Cq~@F@3zs;mKxGUC={#tV9cC0VX1$&R|;54Al=^ybVSNORW+ zco(iQ-TN~@@aA#+2=4SCfGN58R$l5nGxSVMgH1G!0jr6Oiwnl%D}y6@g_w5Zd$uF9 z^^_qe09TGm`o=7er6%uRLm(Le5_SrMhVRMyu!9fWDuh$Z|CD9qg~*2f_uuJ4tGCL{~DkhSC zdh75uV!nW^p~JvGZKD)K3d7?P6cl;VM@P6WFsTh)=I{Bq+3#D~z26_6;WmKp5-x|2 z!=)jut8@#5->@5?_mYK)j^G#wA77L_5^Y7Y7#B`Pj4D`qh6JT#&SsrgyN@C|qgfuU?6IImL30-c2084V5_KheZ}&bu+|KW<4@} zSK;i5;Bd-wb))?~+;?Pmds*SV)g$ATXr4nw{nK!i`QC=f z%48TNpy@-Eo0jTlRny87uAWec>nOi3yJ#Tv#ne+F0>4l! zORA_mKX5yzquR=}x^B883*~F?k^B@uvCeNxq zziEHTY_VmN3CoaMZXPs_M|$3`!1+c2RN%0d4L)dk<`ki|3IG3ZY-@ATh_>m0rq~Tl zhe2m!NjM0{C{L_cWo7jXC_$v7!)H82b4|wG+vg3A4LG_V?D$SoUSumEBoBcj%eva3 zdjCHnD_)PdSgd&&vvL%IG=dUI&<$PIiyx5@SIXniXl?@XLW77Mw>O9polBKD3c({Q+eWQGOJQ@NCxwuh zM@}i?gEZ&(8j0?=w-(UUPN0AhtgnM>h6*++iG(Xu=;gN5zcbmF;Qv#7qR`>emSbE3 z|6dER)=oRrN1v}0k_dup8J*X2C!ZuN%XTEku@ z;RIPG;q3E|%;OJBWlOkA5+Kz4SCi`%+;R~SWE!H}`@IK>CjNXQbV#ZVigc<+sIq4n z^6hA;xvsb~_$Z^jZHAO1bXKI*N6}gRSy$L|eAf)9$kB0<*9!%iQ<-1vLK+Z+1wp(q z2Ivk%TeUsi2X2>Am{k$}hKpiVWHjbQJv!Z69EAOezkaDIS~&T;-!}J!qB@n}S#f|s zJFvJz>9o06;%}g~I#_=JsE(dHYNsbVo>lKKD$8y;_L59aG;tQS3Z`r{%1bxJ_;00y z9ThG3wBY!wVm$dRi#6--mE>#-h>Pl?dI41+Lt13N^x&E+ny-wx zcepN7_vs>NA7wii#oO}o@>ZI^e_UUn)_A6^i=8X@vkNN7M?kW2cNHDN1%_<^u^R>B zfr0EaYKggTRT2EL{F-mH^rc-pcUI!4Ju)_Ygu*0OLn1}ye?rZ%ew^mN#Xi(UwNeXT zA%!&KH*X@K*!{mv6Q`12?FLT-G3pYPC(lIZ{*1#yyCjyAnmSXiNl7ts%h2Hc1C8af zhN`SvT_H8&iLeiVZb}5NO|JwVf2nkUBvWBEhMHfhmnaX+G*eq0KepjR`@O+lV-%t~_Eg0YI zdQ)X@Ykyr2=F{>0AL!^P|9D(p>hqw4??)8X_LR$yOm^_ZFaEgVo4@=B4F2B(CIvD>jFQ0|cz?m9#LOl7msgPMM`&PAkD@dwK21_oOaOf6LKtC zrMzRBw>Ax!lQq;qNaCbujT@iPj12tyHsCA()=au;bo@DI6DF&n_$ALDQViho=05@( z2t+nCc(fnwP6~D=LfiyRq@%$3?}eyr3P7=^5XmAm)*nlkp&1*d0Az(sMGF)k9=gY> zcY|V05X#7w$sRfw~b(fb{zSy_#Zf(~ObdAH+0r`m5;>H-m;^Z!FKl$q&fD!seuFf;vx}0#7TAl)p)rv}2aGIFoa% z+e|U?b=#j!y_@g0xQtq2I0r*Euh~*r$3A}8Q2HM-&X^@35qIVK_8?kUlalbDIi*pm z$b8Tb@5V46$lHL+vL+Tm8Hiyh2;n7l0D=z<*`*S2KGXsns3@w5Pu1wg2z zA+f{3E7ZVUH?Q`Fl{#W9rBzh;AP+3@)O-pc&9y#t24oP}EGQjm6f&Iw2#r`8|7d({ ztgoX=6mT339SXI%9-I83xVV_0*L!bGXXIcfqyRiVuvzKl9p#rskb&v}K8{LG9-I@n z=AyNr-r76xSav|B@(9<6&BrA^2NZd(Mjul=vIkv%gzOA-4r+R%=XE}o7BjW_Ght_f z*c0-cG#8g0GGFqPJdfZFjv$_l(j^*fPtS@ixzSc}W-E_K9nK{@^f3VBq34z_-mH-R zK1=jMXt|%+<1jNjWR7%TN#lg_14zG_!xg)sS>&vV5)gxBcm%1$l%~z{cXD#lNM41?AJV;m zEg8$A)J#Lyjo%jvS)tW;Mo33TSC@;y4BXJCwIeGO6?r#oNqVYNaA%Gm)>J$2|MRMt zI13pMRwZcpr_$qi0RNAt?+(QJefNJ8*`bWIqj-qOOvnfs$;?PmRtfcq1`*204k5~3 zsmO|kt;kHGL_&KKHu~Gl*jYF-`9OzuXSOPtGi#wvLk-DcuO<6 zC-Cb5>wn!AHKgDKIRZ|TCk9R-ms8M(K^NJx&o}sYB)G55!1_Q4$cE3C@?^C2S3I1Y zL&y?ov4PphsC=HsrnHsk&;Ri!5ZAM3rFrLEdc}v9uZa}i-3o%1ffA2>4S6Qk+%pr? zG?*NtWMkL)>QF8X^xC1Qc!c(hHhmMr<+wMKCXq9k_lY1nID&f;9Fe}ohJXX%1?{*( zPq@$N62Behzv5xs;I|B!*hT_bM5Td};N&5)^!}Z%1^sy2I(QkWu6&`%HRy_%3YNvk z#2o!9TQ<0EdSZgP5`jtn$-B4$xmNXxPOb*e#y6N?ExXIF3fCEln8a`m|8X1lath~7 z*19X)`|AFFWB5jhO&|A5q5}hMa<5qavPzX%bZkO6>_obCs^3yb^U+qDmj8g?1B~+= zEqKt@z0s_adhzoh6BCm=ZbD_>`8QvixK2;&`4!^L1g?Vac|~bM>p{U=i{a^O39C5V zcot0`#?c+RygB`44Aw8cn@rlN53!^&Y~FHA{jO1l1_W+onUN<0iHSYfHhIU5l{yplYN1Dj1B(P>LGnp^jT6uU zcT5qMajt`=6HFf*jYJ5F2A+h>;2o!@h%N!C8f1p$5-OvGwJ=-6&VLjtzKV$zEda3M z#P@6}<)nI`4aC_nLUb5CZ>>)!mx3Y=>L6LDhAW1+7>L&iy%O3uJC~)Ov}5~7KRW8y${FW(UX!gGENOn z<0s%47E|DQMX1lRPSFY5;u<)L8v$opC7|Q}AL&saF>V|!<1W-4qCL$Xsjnf7Fk32-S=+*Oq$_Q;D|^I zB-SHzPIfM)Adg6DE0VD7V8HhTYD3y7(nu4%`Q6)e6lqiNiLT-y9{~)N-xzFi`cGzH zj@&nhk%=f9(cR86mu@X-O++2|uwo@!X}H|JxNmInW|o4Tgh>TYM`CL4Kiqk7@pFD? z`sXF}wI|@@L2<;dw%M@+&}xP$97L)k9CA>nfa;f5*o=f!{d;`sNr(4 ze)^=oUJCgwe3N@16*<;v<6yFKQF(5@rz00O1TlSXZD37??7ZIN*}Fw?T2g z_?A%&T;!WqQBgrQ3I_z8%ZQ=@jXBHagfSlDsGxduA4IQ0dH{@Z>1QHhldqca2uVKD zxyr`ELPr7gKZ*kh9BLa4889{t^zlF6VP+xGp(qm*m|a&kr|}P;NAf!>B`?x0{QE3E zu8)rox;`?Yl_yMHtgWlduq9d z#4UN~VXeb3&C4!SHcifj?_c&Htc3~!GIz!@GK;X-RtLid*v>lF1^n)$S>$88_Eq7H zW}Emohyq%D!mv5UWQVNjH|TpBVSsd#XA(PX2r3`ruK?taOw>9>ce0;JxIe3bnCiko zOp;we#6c$DQQM#7M5J(>VypI)jw8~X8+;|oX(EH`gh%4mk(AHWcpgEgv3u{$y1P8@ z3+pdzx|fCTmLy~%sDWdZ;Ee}{`HY`^Z){Y6Y@4DGp1SY!ek0>HY`_u6$is?LE{Xh} z=KQ`gxjoLN@M`JeB`{#E3k#2sP_8IY(_IXTWx$J~0lLC>esV7fj1MAt_#}ZB#ei4k z>uVl?TbYBGpKpWLCGdI5*>Bx-xH8bGl5}1`cS*dJkP^=K`#;aRKk)+0$Woez{2O}g z0@uM%5CgUmnA@8ZvZeaF;+h0g)yDCZ@!mrabIOGMfO6E;cNyvS(Hn0v=~FfI59|E)b2sL<05~i`k;^AZ_^s z*~~~yput(R7ofu}#HVHOE5a?vh*!pqfB@)1sgI|g`+Egi zhiXm|ng%N5DA4JDrqUzO-3yn|#79JimitC1!Osrcwa?tx1JZdwlo$pz$r|+d!B|%l zDjxiD=9X~h--(4&fi)UOvg-tmO8gQUDe1AAcW+qyYhv zxr1+uks_*bU5CiNYBAPTmHS69Yz??&keHC#ro}t9sz` zQA==aDrtLJ<|W;Zss;mT3`k-es}i;7&tdhF?1S1-rXgcVJ%1!(X!GY0F&IX`HM}}c zO$HyW{%@1^_@sO>(q3+35e}|JvR4smDHiI#kF|6%(ywCM+iy8pwOdG%Rd&*VqFSJNA@I*L-Ed(53|@4;e4@(gutA9jDsUFP_6!4mJjV z(S6a|!Liii(_edFP}Zhvy1iF0ieG=N=7u=`EJpL&aSW)@$OcN6?*l zw1Fr`IVNUQO!UDrq5I3^X))`8&AfQe%*X^PeWDtHHk6ns?3Khv{+LLo z_9*bWA=hZ+)m?scPe_DQFWpM0-tf*>dSejVY7#LL?mJ4+T^1_#z?{XzjmOIr%e=Av zB?<{a2O|>i^Ac|xSS{>93rk`PGw5MmXiBtcic>5Qhg0zLuF*QKcmv0+trC#2rU{+}vBA|_84RZFO_n&*ZJNhsGR^K|rk)zCH zR3Qc`ylxnlKSKkSE;k2F)DD<6iH8S?i4CZMFi_wkTCqX7NwANUcz|^XzWf|-VRN*D z40o?yG4czK&?ptR^H;;ncOt9$z# zn~xq7J^K!JcY-%TuONO|Lc#rdrqowj=O-PqwTi;*t@_j;+VeZ6RVJ&1NeJFP5<(nd z-v``Qdxq9mHsRSkFz{65&S7V|u!s{efaK^r6v}&n`74W`kJHVRgWwJb&922XZy?Ao zp|UnIV*F(e+x9~bDT=V>m0r_{GHRlK#Z}WUeuWv}SWMge*@E?%?J(s3+n!{VKm>H! zROrI3hhLASCPUG*ft)9hlEvqE=VVnIo%>-1PhJ2*0UX9fLuaaJ@CEl=K8eM`S7zn3 z*>8Q$UlwGPF4trdGn`;#V%ihC1x14i5xb0G$p>OBxtDSs*3x=;l`~YpJDZP2@O{Zu zCA-jZsSuup2()*Wlr+f2xcN{Xx?F+Zk6>y5v!I^BWP89gHK z!jLa>TZK1>J*WAGd>|<^xcj*_YgeD(~$XAI24fQxQFo|_HK`N()$C2QOR z4)OdV_cLc?L9kKP)aDxJvhL-d`!P&3@!R^)QAEe@se3&$cx%Z5Vi)Nt;cYo)mDlP4 zSU`})trn&2iBrCN?z>FsSzG6UjQh7~Z5ISSKaRf1F0?ge*89s+E{{<(e;hqC;s^z> z@&xDDN-Z>J{Oizs_u^wnkD<#V<}DB;S!6u+2xrYcjtoiS^CO`L-e?wUj9%;Z^-A~$^1%=fI;{_(6;drtC`>#lVPNHvl z!q-ke*2N2$M_x5UOhy!alk>A4@(RmHKmbW_pvVFa^b#RguIxR6MxT4#x(M*ZI}H9p ze^p9cyl-!p@rO}yHbB3=SJA;WbLCY_f4Ub2WvG z-sZ+eCm$cfyE?IN0IJ1;xQ@)M1A`n}n^*i* zlzb0^OhX0UrDYU6D)#sXJh%+q({jbkTQ`4Q;WT;SehL75MvAZ-YXcfm4nO%LptqX% zM1jEy2Uh5;Fak3|xQvHh)4~)%mr&yDMjx+^FXZ{9XUwxgb?Yr>^3+pM3RJ4NvIj%oHDrUo@Gt~k+H%)T&ng6wrvoP zIoxdl(a9>maYZEM&S3&HCVFQVM?QvzS7Tqb)k5j}K#24or z>Rlo~ajpY@IT6L+)P1_jLjxL*WF489b92(^P-=n zS@Z)ojLTECP$V2FxcU2YJxX@_os7FhD<%d$A$pv=$$If(IlJ^&sm>&kzV)bxSVpV- z&)-TyS(A1jw~3k5o7$IoZ3d$a3O1m;ZLeibfDdWAPW_q17K1u0nSw?dUjbJNi?VTh z4I2QURU`Kj6IX-d<0WoQT8_P_enHWu)DF}v5JL)w{A~KabpR+v$wY=U-2a_(<*!Y&g`x0eU^Z#38IPNbZT)|yB5E3vvHSNW3Ye6DX}LFqq^bpDXhF&9FLD>GyKL~ zee~3+LQ939p`)z!^I=JLki0Qb|0EHy0^Offv9fR;x)Bsc=*k;`p|wg6;HRH3XRE{U zAR>0uort3Q<%;GkKW8yjTtTb=-}`&@^0iDF>)_BMOAFpTEN+nB{yqQNXZnuA*a%Oy zDdD$ii61X%9P;^ObirB1sf~)xI&To-6?aWS>p|BLk^E5~Q_g=d?)q53pck_XNJ421 z@`C`qGwt2`Oj&^+CCg5G+2-8kAur!>t>3XL<#~j|tq*Fv<#fC6ynHF#f%PoS8>w1O z)p@G8DB}j>QC8r-WCb>QQ!JPg)wa4r>rKx-24IYc!t|3iinSfp-4N7gfmIt%A-qPV z$a1a3O`>X!>6=L5>(6{U}l4RH|UFP4qMvbm)UD2kkJJUz^AnkKbhq0Yntw z!csYRKw%_e80vGQEbT}Q5wYQSPxlrA_Pcr555W08rgyP{hQL1Zd>;G3h>~bE$M@hp zcJvsaRff)Fetx+0sA`;oZsX9 z#KlnJr3&5)EtA=qFw>ow|9Fm;F}@i7O|WT@oZn{)oYxy?uvc^3gg*or3^|eMmzHIY z>0B+&E$3gh90RgFRSRp1`0LF^J&k_U)($Nk81<~a*~{w#Y2l;RN+k!~L z2ojqz87r2VYMr+zoY)$M@X;Hq#S{9vvHl!o6p?${xQR%qCOJoN+BXmxK4P4)3WE)^ zep`>zi-g8TzxCbDnOmyV6iyN;;q>IBI>2*0aTG$bW4QEDm&uLlUC#zJFW5jbibmfq zip3>z`+T7Wcm(La-655Pe%bnLA`g}ML26^eKDPBOC2g;zQxBPzJegV}^N9Na;qXDI z0kb&Z8guM(GA%cMrA8At4IEJHHKo&7O!#B;%@*AS*b9jQ@k2gAEEPC&U zw~k-)^X03Hw-CT0L=^8J?#av=ZpA+PC>S0GVtzveKt%0x*ziD#6+1Mq6y`TVgNX@f z8I?Fsg2xeWuY;YuVC$VaQr%V>SXqt=sqUxEb|EE@VBi$i5C?&&eI;Joc%y`8_WT!U z(IGEqhtUKyUD4uC!LN?ES^xttr^v$hr!`-OJX!m$iVBdO8^B^@o~>A4;Z=4Mp9qu| zk^T`7lTy>s2@yMb>vIsk`WS$P|8}ke5c8m)dfq?(O^csr{>DoXcI32Uyd%=}yE0l! zAMPC3)>Og{%n;xzd5MAPYCxEl&J$@k{?;V;p*oG8lWxWvhq?ylRt zKg&a{H_-r8PZx{7cG&44#v-XPka|~`0>}y*}W0-xg`51X7Wry zA2<{|O8j6;SH{m;B5OH;s^jmz0w)88;ViJgVS(Z`8{j)zU)%rphVra=)rGT!n*}^v zhrJdo(Ai~s0Xrs@87FgLl-cFOkMx*ssDQR%0%>b8rIyCg^^^d{ggB?RL`-pp4tb8$?EBplt_3Ucz)-` zpVtfJUL=GWE=btpq3KGLb&hS`S-RY^M2?7R0AEdlGe?x=&6u-6zfz}JE5!&Rj@9Xy zG|=q?aa|aj*M2bdTeg3@EZg1$N5ViL27qQ;UAflqTj#ja&Yg+xj)5T|D*-9RHC=#r zQF`q!nw;^!V`OY*6q}LVuovktdmAp;2l7M;Mfdj*)jM(7z$%Ful3aPW`e(6B7q z9|`SFCM;LP!HkX;LUW|}*V=V%I1pBu`%&oA@t=S2W9#^L^K>FlgGfcFf2*ALWgXVd zy|OFxgXaJT%>U86=IxNtBOzoF>c_#8sNZ-LxZYVH(y>kb&NCJOw)Hd64%~#eE}lTI zd5N_1p+VMcCoC-8%agI}6R4+?fEHu%_~FBco1wb1D)$#}CAp^H<(tux;vQf^aYM67 zGLxN>N`CG#>!?71{rGDxoW9#it78evz&Bwz8Q?F3(JYfxHF;UL>Y!ah6UrFSs}C^d zII#`AUSeIAnNim2RSdbMYv(#MX(MqW44M2$D%CtN)hii@?%E0k%?2HHmj5%id+cO8J)R&v^rSZp-F~YrMR&PB``2 z;`0YrKdzB3* zg?B+_ThzTc#=Tc_YW|FLF~K+DKA)z{W++ca*b7x3G%rV;QkY!AXTQ8#go1iuHC%dF z?pM*fz~kV{GZ%_M8W?Z`vGPk7KRq9_(G?9QMMT4YjWGy%zXmQH3!jK+bt^<_M<85y zgP?>|WiRAChJX(wdxEj{?_|bK@25KiPezTD!hjwzGGe-L(&gFED$7xO_E?kRy4g+^_B~bn#U98#j zxrVIt%frg{+%F|5`cUhj!O9CVabxxgJ_HuH851j_=NQRLS6yh$>Pov^ zZQ33&%Kv&{scV}5*Drk{Vr@b_A_{%LTf929Dc?d(Mz(CdD!ep{*b9}JzDLTlnGf;R-4 z$wj5CT54`Vt4L2(FaCAW(pAqXQx7EYDg+UhV)tI2_q5u?dZ7b%+!GjGCqQXSzh5Mh z7(8<#(;>8_j)uP0gZP&LvqR)JBauqvneM((33C6tO`Oi&zXBYt_%8B03M!($Lw=Zqnmh7}e)BN|( z_^FqE?p+;{Bk3hZST{l9Sf>s=(P2p#oOrtX4Wx90@Ix4G9VQ%-9j6w%1^5JE)ELX& z&bUnX{G6PGi4@ZHdQ9(iS8ZAKRk77c8Vk*@?byDdqef_P_fm6(X-56ZxW-F(RdFwS zBGnH=0Li*7k*9^}m((MR2E`izRpJ9p$kk%J^{AqPX`&G>tr%&(iqL2dkdUh759Mx1}=3V$RJ-r8#8%(NCLDA1Zc>jzj z^&$QkyqW*e^dS*{mUm{a?Zox-5jX62kAtTE*X2I_I^o9Qqx53O-R@JzRg1Q)(hLz5 zRrllL*yEd#%{3H_k4-$QNSA86EyOog`sz20He9i)nIFw;CN^xsgH3z}A>)!YT9p3Z zdCQmM*b?}A8n8D}K>`Vo?wr{+(&(gXxQP?$Q%89G5W$5J@9`dm#mW ziGxowduD$c-PvOm`o~_eWL~(`VG62uSb?#Rr~}rokHoJvyODqR3k!a>;WCP_ zf9NU5b0lccjIVq6&SY8VmADrXBfzWhUhi3GKZ#W)L>@FIVT73Y9)K(dax*=_o>*kZ zy*(6QFRmqYajIDaotrukU-G1mpuq{j-WzOqK(0$5fG%n4kP~yJgc$}^hl!l5goo$A z{!ebtXea!-*N)SO83{va@%XnTtTvY;c}49|Jhm9OJ;y5!PV!>ZWA1 zge^9C20snw#_$_c6qaf3C@qeAd3Qv{Y8i04|GvN3k#La+VF~2L$kNCadT!C9+SUTg zo6ts6{`ieP;gJ%$271FloROP4y*yjEf-XyTzb2q$Euj9obyk=qoT9erd#FKz_}{l~ zQHt!`tow6HzjJcl>p&iD-;3SPI^2+);k42LU|pD;S=bX_v{0M?%+VU$-ROa(cbJIo z9j>_Cf5l|bLikSJcun4z!s){#9**omMW7X)kFyh-X`W`zuS??!WIqd@19;jPtLW{U zU)ygSl-o^Q?@;Wb9qxN7>L$@)A!tk-*BAlF?SP{_n;Nn>=3v#mq!}{UM_LE49Ws#; zzE8|!MdtVye8c}P@C?oaK%u9w2~e`1QSZ1z7kr*{6ll@yPj|nB(zskb+xZ*Lm&5H& z%oC)qK&^FTm_-RBE@OBtzy5`%^+KL)&2Eq*{-`qaO`QSQN-qaJFv-dUQ}e0E){nR( zt1ngv((UQjGIl;)tuv^VK3#)GqSQcPsWgXq=jmzPxge9UH|Tcg{IzN)xjLpZpV{ip z>su9DM-jIojn*0n;wBxhBGtl+Vlr zeT}|;n#k6zM0kp$$Ubd88_yQ{Gxm08uD|mQgSd z?7$|k%BhxZ#L@|lgLu$L9mHTxPZ3=Tnmz~wxM7cKz@-8G5F_kNTvO+zE@5e|v>J)# zf+)K#xm(YQa55yffmGjZ_ilimLq{SL>Bhvw)IihAp$)xLdz%C44dsuPBvFU!V66N$ zQTr{DWYadn3;r6D@fGbCn&dMU|%)-TqDFio77l@5jKJrEr!e# zQiF!$E&M90_3G#56lmW@r#p|M5gJ~*=)0Yr$R@y5{4e~n()03w)`P2Z^8?LmaYW3YeJptJq2-#Z zMJ(&)r#?2}k&#*`GMMn;E2ihI29e*Vi zLdKgRto$eWBbxHj!kR{uJrX`j#sDfhAt4w~CIcg^Q})&+rGnAPFV}5L#v!nw?eg=` zuUYzD+JUUKNnive!zYoOM)IhUSWjTiQbn{N#5IMdVh^V-VRs3RMmjEJIT3B_=kf8Y zq^-goif){2k0EQS9^#zg;X}qzd*%&YQOKDSw_dosf8@bLP%NTD0*0#qLm`lVLI-+_ zAfnK(YfGf)NNx+XiTQ$?Y2SA8 z4+_l8y}(jhKn;{XBO9Opc>p+!65Kz4#1Q|N6<0gTWhWNTUUSvT*>}vE{e)nS^p_6qL%W<1EwdU!XMzow~(~Vb; zznkc{2-D~odyG9AQ1*TO(k_vb68*0E+S1CW=du>!cX^J?-*xEDqs`(u43Yzko8lvK z$6D8(0EoDAoD}5PEyc_qDx-KujVGpK5xXj|>kO@=Wbta%HqP)nK5sjcj#~0)adFG> zzl(W#v0qf;eEWKjp0<7Z1~RR@9zC+H?zXKPt&+TNc;9<;c}?c!Fd8l7yA*sf3k!9B zHuNJcN=p66VDEJG$BV(Yin(s7P{}0DUftj@h1t;%csvAp#E`dGI&1J{=ME z**3fHobO~B=V}nETv&3BgTH6iS}+sp^^b_AeTr6sfW?@UWu8lKg)KoN_NjAOaUr>Cz)JHi1rMx(&2*tlDK+g>3dp&c%-#gi0FpAUrFK`7?F zViq5*DeN7ly_*8MtbM)rUY+>zSQCK4*>W$39AxGlMlj=hU^{da0@^^Ys7^GCq*?w- zu(*~}q7;1MQ4hbkC1uRB15*zzQv8HZQs0Ks($Z^wZy%p^fks)={6v(1Oru?d?3%~} zPxLplPTHnBZE_Ih3k|_U4+%8Z*^zJ@TtF(NaX_XnKv^ZmHi zM$U5}6B87w(=KGSfTwYI;evlKH2!RhPCx&t6)Wkh1i8Ke#!mN={>`(@5VnGjHx2pRq#1;F-OWJP0G)>a#jDUA5`kjJIu@fJV zFvmpQ1-Ez@2*7|Za}Lv)^80;cuO1vuOi5Y49+W8zhBWj1M4!mma)*Qx?a;~(wuPA& zUEQ_`L_rs2u(sD_D|$HFMpvvTh-WSDpa^bsYD;*1Tn{?96W` zexKbFm?c_m=0GPhaCeePg(P0!m>ESG_#)y}7E}ci=aHV&+(|8c*`42yD+mI@HxEx| z?VFB2mU=L5lca^yG4}YW#7z#_{Pg~avVxb-dZO0HyI|7(L<}#0Y4ZG#RELfQ*wh{g zgUvBp{cJ-tD%rcAzbD)hUSLdY*G#jf`iBCe1h-Z1A4z{|Q!pkM{9RlA>$y3Pxxa0h zbWH54sVW=Ep1_Z3vv(RrwhfjaBF}SdG*n#3Fs?@uO)b=AFfYh-aevXAn|i$Dlih-7 zV7yS1X@WY_tETpq-t8JW4#Y3+#tl7i?cFE~)FvvsYlOM!&&NB)|c zm^2}ELeqGQF%P7}7ZA542k}xi`VDe1;~Ao>h~A>RMR#S=zFcw6`}v9Hy0JFj{^m!& zS}4^xx$0;BkEN2)JrIT36yCxWWeQCy7T+49RV6#IGigq@rH**K#{&x=M^dliu3aq= zWhhGcXCy?6;pwwnR)hF0muQE<894()SEDyp`2x&rAU>oWD)Rj!b@WKJ;3C1gw4S=wd*So+>Rb*7WicdxA5X+LzZV=_4px7} zY|(X)S`GZ)9y*A0uxnS4$R>)-UtxR_da+w#w6ii0L&1bMI1A^)VA@h+A~^uXh~9~e2xD9X#tc}85icim>T5d=8`K^&hNG-BZb{HmeA0gNH0lR$nSgv28t80e`a^WxGlCd#XQN8k19MlX+EYt{t% zunK7C3q)AVzDtFwU|%aSTom>k8%49&+8C6=_T9Jg2(WHEC_Mv%9K397*ClNm;lCpmiiD# z3y8vslAmfQsKZc?@NjXd@k`ol5zIs-x%ocelncmRk|v6)I|}l}Ky*#4uKTv?#?I2t z6>MU5vTtadz@MuJ;YHiDX_F6zRU2%+&VUo*20}~;0Zx+n1M@v4ee+khORtHP1uwf9 zwqyK&TaZ~BfdnGWF$PEO6ji@ej7DM5{4fh&(>=@iMdWzd!Kul~I>c-bqZ^RLFr%lh zf6XhE{}HDuS#P$5nf`E%@8J(FEobk_pK|4p-k`9p$h+t)lRvtVJ(;wZU{`2cwrC#l zDW`=wdB1XyPErXNbE9yE9c%&%Ty$f>Q5e*NVJGouYY@@GD{8`kaf~m~R12lE^Wkax zg9s5MKjg?C)3*R#qm$~-FYG2)80Fcw`Q~6i^!6Z#mP~e#KzkjeGU)GNG zM3qfiPD#lo+A>eOrbSG%A{3h8y~qHK7myL2Sre%Z8ERaTvP;ugdn)hL+{}=C@zH&1 z=%7h?gU8o?Madpl&yMSGH@<+tkKy$X02CiV+7anGY7q!5%w+1ma;w?R;6lJFpO7wm z{}`jMkLS^Z8vap{j<`tpPY7Uu8!{5a(w2!1N25jCXk)~U!H;bDLP&nQ<`%Y@eWG@< zYjn};;R6j}rOJ&9KZn(!0^r)Bn+cvK9kODc_3I6Aj;sQjsS3SS)DrYfR${**lpuY7ZJ&(WcsF>K@EL-$$1I;=SiDV54XAx{pg>sggKjt#?XfMVt-qzN>R zCM#Avm8<=1~bgO&%)bq=_5Za5?c zme0=4x`l^^3dHr!7mxO;EG#T^0>abN)62xKXAHdz4WvC%n!b1-Pxl2tWM_2GJiNT| z<1909-F4nOtn}^e1IYy>9@K}g>Fj>J4?-)Y;Gm$ftC5jg0Wx9nPq^YI$L(L^YR3n@ zPu;52z#wc+7RS7=zIYRbhViR%&N<27No5vte`|log@=cqM0{2-Fyfnt8rbvTRKGAZ zWhW3)$?dqh7Uf(QXb5jeYFa=L?P<9Gty|yOUt-Pz(T@seLb$QkR{dOW-28Y8u1kCn zQ)-IxY2yj6JgLbgZPJ1qgkJTS>s703(%wE#^cvgu^lQ-2t;8QR6_xjG!{ZnibNQR1 zd^5Sc*yQ)>^~R?-ww(q{(i%)_^y6i4IPZsLj{@A zl(_atWw-LR=Y{$ogl^*@L4Cck6VTQO5Wy0Qt@iXKk5##2Dk_xFMKo0b;rbZWD4)Vm&XKZ3NL-Rck*bsD%=n43K z?{Kwwmx}(WHhEMVXk9ky_0^yMgs1w!S)avM7(+>H;=~E2BBN&@T;`cIF1cGDNkft?h@YYfc*v{ut=?s6p#rc)b#D`W+)?vvq zqDXXcG_ufNwt52{ZHn3Q{l+_2TO02vo}WYEnL1dQmW_Gf1zM`%o2ROGWC?gl>>DhT zPPvt&cj<=3d`PAaI)du?DSh&p%1*w&ffFV)VZ2#6F8FXJ`x7kp&TeJ6XkQFH@-IgZC2YMp`J67|n(;Y)Iz6F}qw)BUwE| zkBXgvjsUMn1%nIgRC43e-Y#{_t}r@+P(1RLt9GUogkvV&Me@Gl^zvbaws-^`#-t}W z{`x4U@j;y!5?;W!#+X{;)mdFUdk&gc=mcEf6kUQrxCv{z_VwPAJyNt-{0AskE$BMe zqak*MWLG=>2eD2Iq!;E95YUAtmc*FcX!r1ScgJtaL@X7%07v7CI9SBzcH2zZTBb#6 z7rti2WM!{dba;7?Y4WUGDM%YJJI8TOFCb!Ne!KXuW5_TY!K(KhT()0nZl?Eo$Vnk- zLBtfr?KlyGSuq#rNf)I2I3MxkJw}zvglyu*WXXJ0+;X+Djso>|s%72&nGS(CR%OaP zo3q1DbVR6Td)z>J^0T%jYN+cGob53^l3z0M*4T$P`-G^IT zW;btp^Y<@?!_^W8?}vck*FYl{Z5mN@(e}lcoB7c_sUudO9D54U++y$p#&FBupCY{* zQ8#uv=UQ))zRK4w8erwr#B$91HD^Gb8jja3T%MgU_w0pgzU06Ej*gdll=p7&JY^(5 zgB#y{e-WT>{|>9X`GwYb$DnuBs>@C}!_Y2(q{mKB9D`@)Y!56gZJWB_(>+x?`Qo_j zPv90L10Hzcsy9QDc2*8G2K)DGgWL&8<-a{SHAT=usP8v1cSs}vbLmD8O4XtR5)acJ zG>c*1-A*m}Ku1QFn?vdWq3N>fjV(SoU=}n78Y92FyU{|g)beNk*Y~P}QTiM5`@jFe zs0t6)i#%ZT$4UEyiHP*hY?X}lb=UPH zXLf~Q2S9Yp051hASO^j+j{qAWv?t~wBCjdcuys8&`sywdWm)c;lsqb5_~*P$zp9ea z^LX>SIS3>o8Lo&ODrzcAe(!T$v9tOzZrlYQ!A#5|ZX9fE*TPLp7ZTnpwiyimo~hDV zE3I@5lrGHkEExBp5!r*tt@hN8#yBRKT_mxdfVfy}N@0^857?%Auk3;tIr2BoCs)F| z0MG)=q5?p%Xb)YdV0@KOz1czKQCO+=3SAaOp$wLOE5(M)i?IDt{@^fMer~jTPYGjwimxh`;_#yA)DSb zez%I)V-L<%O}%p*mua1LbF1t*jUH#;A z&FrV~it*|_hYqd7?cFZ>q&MT5mrBi}s@+qM{S@iWrQrnwnt&GS;Fl_^x$@Aw6H^9SAN9C0lQ(OShLHkA88v~W3yEV_aKUKAZXwPO2>C@a zBYv47J^|Js7!um|^|nW^n%moZ%qG$5k^5FE9sF&Hf9r!!@a{FDwSkQbD~O2Om8gjT z7zb}sK6Wo&D?QGLwZo^AT`msmg6;A8MQCT`JJ{lVCo9L$D<6T-=?yklUJG zy|bXycB+p<3DF;hgBdB|r>>lyJ%faN3sbimPC!!DjH#BWJfV&k2@6EuLojR9NQ zpx>x5TFV+kXOq4Wxt~-zK-a>clC}gaiVpwp#v=LZcpj*JWXA#unnvi#ty4Cw*8_~UX_&+Bf{qAp_Oi-} zSgVZAca-VRJED)=HQpTg^AqMNel2dOWSydRJmSESJkfXmw$FYh1_u9e8G__(3KIm3 zv^DMa?mIgo>a}9&DGq-vPizJ8E4h zR*F%0wJ4JPLt+_;)gF9^KhP~6OLq z$Be{Hy>rqV$xn|nD2TiXjKWp(woyjCpIC6RK%DGBE`~m&^it`-56nB{a*uey3PqyC zGWMCioaB!$eD$18y-DhwG}Fr55DU`;>rtVK)Zy71j?cC5L$#L{7vLm0b#A|C#%tg5 z^V6+=C5NFRkAC(NO*qylqs2QAufP_wgk5WLNWg}A1e3MGz$ohlLqil$?E9^?y{!BdlgtrM7!wF>C0FxrkL(;5WjR3w{O=Ggs5;So! zWZ6=MKcL$rK?a%a%AW?ubF_`(m!>s_uWaA)>I}j0G0N@dj z$7RGuVWQXpoSwkv@_h4tF#4r@{N96#wHBH zd|;4*k?#E@BKVD%?H`a!n0NB6N*niGN`8wS*12H1C~SQZL#Y?Os*pu;X((zQ)6z%Y z0<=%T_@zP0)fFCFF1K~FU;lUiKYXBH%_==lgP2^0&rU~?wVQa4jmTE|7(p!pSz<~T zUc&dyiB;3+7M*-ra1T)w{w)o3*6r)0-q->gaZC-mE}H*q^Z+o!Xly1$C`K0om$){P z+;if}M?fLW5jIg%_K>j1dH7R+Uaz8gwj&C?U%#~Q)7ly@H6;Zk&N@ z{PH2jHy{4<@R;6@C`;6tn2niEtT7jfV;PvA2Kani^AF&vLSxL^n@pb->VcM}n-RYr#+zFnzh4$v^yE|r~w*YfL48_+!fiD&v zt->tOhzryq1LAEPu0VFqQt(t=MDnd0OY}$+o1ZAJ9(cZX*PEfr{~Ba|XlkCS`o;Gz z<^c;r_7YFYx>b%dSteGvrL6E08U9WOvV2GMICPwjG4x@6E9P4F6&Xi~aChRgQWVN3 zi2-fX2(V&~2Yq_|de64dh-S{8G9vdZKDeNiJw*4Bs~vt>GtFjbNCBzb6yd`Xb7GpG zPkYRbJI0uH3`NE$cfE)13Gsx`4Eik24wdv2#qRcnhFjW=(Vv)?a4d=a1P9pZcmCzZ zrejR=y>-OKOHB1O0#BC2<~llfIemZFEdRb6=r`06A%05>{F$^&Xif^nrA+}tpsG6- zVg<~3Yi3JC=OF9o*|PzNo!;DcUPY8WVFaCHvOUGVhcisbOvy6w064t zY@A+ZX-&FdlEer#D1=>?f$UYG6P-au9f^@A7dk8yE776-Gsyr`*CvhBp46T>X6Es!g5-?eQVjx|fv(h9FzsgRpsq8k3(u?g8+i{m%|I<#a3lIbD>teLb zI$L1NBc=T?K%qb>pf4djl8M!JW%_9AQhHZtJi>(0%|IXd~dp(IM=E<1}nXO4@i@p739*ntFrd16k( zM?XUc@1uG^$;py+EG05i;be_b&Ct#dk!H3wGmE=(%M?h4yd>B#m)Dp?OV1l9{D)enmUGd+U0eAjEZ+0H!D(n7%2EL@}#7N zZyE(3fd=+5@}V#;T0^V3WBPK33-je83BsCpU%e!X(J(+ojpXya((~6J4UFGD z_Z#(j4TaTLV~0XN7%LU(#nCdLzle7(o$^vH>}VB&_KPSLD}Q4=2uEqp!j)}49>W{A zVM#AX+lh6(?@83i;~>0BVlSc^k@N)e`jJ>A9D6yL&k_h&MtlmIn>#~~4@P~^sFoP2 zyE=#FIwm1uIVdTO_1bio5w!+Lj02=iaA~Q>=BuvuA7@W}E$RUeZFSy3!qGPunN(+&>FLkECKE>X@UL2GATpZ}NaFMA6vKJxP0fCI6^dR?!V z@5u*C2YxTrY$|+qrRlLC;zSlPmXgLHHodt27-P06Aj!2F&JquH zud)N2%7SN&i}zrgGe{FUiUe-TI1Ij8d_Fpdk36Ld#0v8o5=6p*+ur`VbTLM%wQ@sE z&%&&xCOU1ruh=wIbmNIaVf5R9>nTIqK7Ieb53)N33dnytG-yuRhUJThtH%%7Px+&9 z$2i^?v4N$2x@mn*X<$KMl64jx!8}0Nplm1aJYBCb8NJHm`g$BM^&0ob9N%~2j{Mq@ zhPK7w%i)s&DsRyPp)nQqu3rT7Ni)d~nP0$4o3^-7pdM=ArCyY~S>GLV@ZRUOp@zBt zX=+E!9f5+7?w*xO)36-9Cr5|m;9+BS-uKE0uxIlwU0@nL!vwt-61hm_ELn(pza_p~z%)DNV^&89hTD zIvG5ERFpfcqsf6_+!RZc&VJjXR7A}2h=It}$`)SMAya|shxdMN=YJn5sNE@t?1A1Y z?+5F+X9df0;s%~{Epxmf;4)x?mXIv=g?t$m`USqLSIR#xZ))<+g`o^T2f%0mO0_2n z9ntI`0ltHy`mbY|^{pJPAR4?0xkIZjK0kSSz+%Uum3q>we-$ckH{!05 z*7X@xwYcg1VqfaA7A}7<8bb2&<|{Z&xAUnN6@2!)yeXP|KI~<{cGBo^9{QlBb+1=1 zVi@2CYLbW5xotb8bC+tk)_l2;2k3)pVl)aRFar2x5;g`OuEqALM;Csb2se{f)csGo zHNdf;&l7FV~p$Q;$|L_9#Ux~idxT)yWHQKWcY@!w(E@JnFHt#QOsR4wzTOFW*23dwG=0( z`+WFm5d<*!t$5B`Fmmc~N0lMDBiYKE!vrBY2}iU`bI)X$b(xO!?^KI7ix!R&oH;Sq zRc+(Eb)2);K;ejg*HKIKD)5XOqGGc{L=TCo0ay@QC!MR)uguR(BVsdLlBks**@Aat z=%|>iaZI}_FgdemJuq3)wR^D&F`Lk!wDgk(-K0;uRo|fdoLPrXb1ke*BG70JhagINh`9Cc{ z#X)bjNq0QIZd5?3b_t1jKI#^%F>;=NO@MJAVnI+!O3ETKILPy?(euq+ensNU1Vq$| zT5Jp3KV&f>%`VwGj`($gPoW`T{k2*1D^oghKgohF6q&aSb>_LZju>tDiyo1IBFp=N zKAmLb)XRN?85VsY1MZ<9fLym(RC~8NRc`87)d7bd)zoz5`HxpPzkpdtNVcXxgDKrd zh;I38u2@xd=w@t|q4Bn({9%JUpw!1_Zd=7Mah6S@g04d)A_~%ohhp(_5sAc;f@(sR zZPl_ZdmT8s#F`omhLN2bJjiz8H(Z^ZVjW4RP0 z;`j_RGc%SsTgzGHw76pw4LV28m|-1MzHQA0HEppdj4>-)Dxb3N6K#IkMj|iVc{n!K z9f(f+l^hIEm%#ia1{JO49s^^SZ7$K{9nd>KiPs65&O+>6ksSZ()wEl8mTkI~84iam zTE9tHlUh^9K_La0TzdAm?$5TujPfhUfP_$nEN3M1I0?-Ip@mqMBe;W-a{Jd}Ji?=f zVsY}d^{LxmE6h#x?(nX9;H1`cyVwIlL8C$&6`~-8wI8I<_r$J-{`mRAP0V*HB|AiT zByEzhI*^VSI*Rj?Tf<goxv!!!2(D=7w?&0ToF>d(&RNX`a4{9x{PL0ZbktB{7U2 zGgmNRfzAy^*$qORk z#}a7FT(`r<3z+BM*=;zi`6L8Wbu`HYz^LoXg)BXf#%>M1i%fU8U zDhRcV=z_b<3Eu>TJA)gmT~^O)qC+p5`PLUjO_l3)7YtGH1~64 z8CrM1D;fu58RP6}%=C;53^4OHXHJ-JX59ej039IQPgbcN$y47XXdjcDwU-16NCr~A z1CunXBrf=aa`BYy14uzzex5^(;q%H2wc&J1P+f{2Z9#K@Ra5_ur}vHrvVHrTdEKA; zev0e5&ht2q_d0IFxn#<5vrs6rPD zav4Ydm|5$l?eyx~GryKS6WdBmqI5nc_Y>m{Qu0+Yl;Y+V7Rahn(q!!V?`IGpL7*Vt ziZc$4mSL)qSqt~u^g1!qr=k!H@j(=E&&H3;pPikZR?-=ThUuSlK`#?*m0wWsjii+o zST&O81#Q-TI7s01CLkh~(~>QD6mgEA1B}xDM6hs@gn%t`l0Bo4D};EETxa-$(1(do z_ac6IzG`lL9$ROatigcs_A8D6zaZxB4PD5;0~Lc+dQ1?pY8mYH`Z7Ata)55vTC;7~ zuE>tMh8H#6JYf*H%&5JWg!@1b>(X5;dHd!14=zhLLXS*^n4n|2E_zG0H0Mx*qr@C; zQlbx4(%RVQ`0`S_rT9wUF&0nc2)Ar%W8iMp$h#@)dT8_)*$A3)?1g?=5{LiiWlRU? zaM0Rq@9dQp=NFZ#MMwG;9mfA@fbnQgaICO;r-t2w11EpND@^&|JM05RQ`=m-N1EfO z=8$hZdVeC>hs53oB7>G=SFhtF*I4EPL8xx*S8m;KhkSN$n?So2!z3rYTIFayhBLSU z=qR)p=?A2-ukkk8^i9g{x%UzlSI{#u)-dI3aF8HLxQRHyKKkNBRXv@$Xjzo?AdLU= z&psw=j?~ke&o~4OHh&u-VXphxA;zMQ?C8BHXFXA)GdTaG(H=45?+_4Z+OK@KCYKSZuCeoYm6;vy8^sId_DXgmY zc!S}zo84Dp%B9M1*YrYSZT@z>o}Gv3Zntk_cE*@2iE#<561rP|=4@JhX|0M>B1+c_ zGKe=!$8S_sXC1is3tzC1;?O=1>DCoo+^{td{!y(9M6ewj;CaIw$HKs*FFqX`0K7Jl zeKxp8X(@uN;;@ObWSeHUrgXEtN{ow(dj{V2Ix{x`$X<-!w8IKe$QtbL4j+BM7Bl{0 z@sbLc;Dw3lcjH||q>grI4m6O#FZCSp0`;?gEfKNUv^7`I5IIvTO%LA5rRO!}1BT(F z?Mj7lk21fl1~`qN@zvmpFIz6UCswt5Z1vvO5jP^NAOjxaob>9i&IzvI)Gvbc>l4)7 zc!dzjIfVG#7d>51aw4d@@b8$a-ZiK$Gvjx{7_eKPj(Rnox$982hX(c?@=;{E|1#gOdE<`B*(dS#rA-C5SJ7cIA|6Sg6(;UdL$=YOw{NdTk|P+yYDwvBj_D9f zL4!f+7Q`7&*Y6C8gmW!pH0gt~Btw@!K-8T$<8fcEhJo^2O55m3!zP1ETrx+?z^stC zJ3O}QXxBhv5fYe$HkYR-dfkF7p%;g_5)#2XaI;upuMezS+lK05{Gj5Zp~&v>+p(NE z>%yNVE-!-+B@lCFj^)8Km%#raT!ty$MJPt{y(SbI?X(t(Sv@H!t0^vNQI6;96#+ zo+BI!TdwE?WhE-AlTMam0>K?XU~_zBawKp8WeE2Hq3@Jrm$CCZ!$2JpUdjTLdR4W7 zY*4!X(7OKsIC}vs!(>SAZ44y+`297|!h0?M{jWW`yzGg0%hQjsvIf9G@Nbw9ae#M8 zKuhAdMpfSKF=dT;f?TX5b0i})yel1ZV>CW6@<*I}?a2-9x)PrtDhkYl2&iH~*1oz? zRYBl^!oPAhH5~1=S0U@#fN(U*0*}}zb#IKikC;yQ->Yyy=Ux~-LY1LMKy08EWS2x( zj1`~~=}+g8{4jW_r07=UdjG<2Cd9x-&^R;;K-Y*3Kr`UhXWx|y&m{No%SAt~_Yh!O zBYXP*X)!^>Ip~(YGn$qDT8R)Gyq%Sh^bh@Ny}3Q-Hd2R3UoLv;<!EeGjY^ttXrDw6_}U2R|Nn4)qvAv8=)W9yqsst$|(wt|L* z4e&2cCD)z%!4}YeBF;@=dd9P>jAsHrMm8dCLxqt*2%s^nnx1l1cb?^5h!@lQyzF^i zJF+0UDxRxk+u}jn&7-K*pdxLRv!ynQ826C-iJF7`Y|7s&KaURs7}X2ofxI03WX(tP zV?uB%ppCZ`cwe(|_dHE2hks2&G;RFgQ5D2FpG?xLKWz+wB&Z`iks94?WHST+uktI8 zB<8mvqvRQ3I6(bieC0;p2)x)JIMoo}U+ z6mq09c3H5-G*o7u+zTBGSA(r~48&!pfjMz8T*T;w?@#o~BkW<(on11j*?J%~5(SWK zs8&No`Us^NUKF(datyrYbUs&GG52GI8LiJI6RbU#xWb#ky~V?i-El7(QgS437mVU! z+Yy%o^R)oN1kqyKGKRJddS2$9STHmB*p9qBNYJ~mR0f57@Xy+Iv8)q;iww3E(0tCk z=W*dj!X}Bu$x`|{_-s0lHD<4px#z}e1o~v1devIz@u_tvYSIssb|}k}qSi{u^7Kmd z34klJV|r~Jk8Pen=Sfe&f!kP@zr0JFo@v)-)6H_W%v2J(gAAn&Zu!X=n#5-2z5V=f&g~JxE`)KYiOyq8N@l#?&0jojR55DAL}^#MJq?0Z(%_x*b=0FhcX9y2wKPzT6mR8?DwJ2bhqhPLb{F$j`Vk;6BX^rnJai<{kT zHoUS0(h9)Hb`8y9$%fAtL7D)z=dm6f#fka{s1bn?wgvXWP>DI%0*~!EfD50XaB;E* z0ffoGFCe*B0TQakqzG?)pMrPdy5LS%UHc`hN|qM6QnUGe>SvS(+HN*+`!0_ePJeQ! zIS>AKb04xd%5kKEAv;V(7&?hKC)*5x*3nRRHt%6l8N8=A|3>0X&z12#)V)uvL?8gy zh&zNoh9$!Da^-K|3dOhRJ@=S?sCA3S(J|tx3fBIh_obsqweiNP6G(CZYpMZN8!V{{ zDwV9?j~n8`j&}p2v|CCnmZi}?G5V?B+1Ts7&>>d9#Gj?rj#s%E3J2R?o12?%gNF1O zxf}k}fR!GW60$vtgzH{vZGW?Ax=|NA5ZO1O`_x{yVBBms!++K*tK}^4a*J|GMLwe{MCSNrYP+o0aV=#G- zTk3RoE;5)fGvaF^q!e`E)&*=gW6DhlhDLT3P{#@#3=5i>igq0FOeAs2NSwjfp*-zm z_XAreR8W~Qsj?tdarND^wwsorwWFfj)cW?EnPzuL7(Gzl9;<=(u*-E|rO)tVC6Bcp zgBQn^6?;6_5lI;eIb;tYO>A;3uLHxeM194!2Vd^BHQ#Ov?^jA;w!3%_FK?CYb9OB@ zs0*vuE6Htknx7?DU48RZ<(`Eah*3*iz&J_PUrY6^oGF)#oDgdFqnc? zgWl{kHWu5XIRX12;6htuJ~tYpo{4udhc=#u!T>ObeHp~FH-pVepxpKm7X$f6WEjG4 zQ30kF%b_tli%vgoZIxRHy1In0jQ^#x6wD~r{Hv?;H1v&IC|{6VY6T)jEU%`Ny2Br;w$*Vuj_a$|Lm}tn3tt$u-n6x~1^FCGQ0Au2A=N@IK(g^d&KS|bV&`YMr2YV*Wq?8&}3Ku|KaUy=hRafQ#|NGJ*O`gwxNl+0; zJ`@;iB-aLTBgxEm>ZKb#YqU&m$rd3j6K?-O(Bzhtv>xv6{#%ihk59K8!}0(J1BSsB)3RQ?^Xp$X0JC#L z`KA6Z!qKT|chK?f*2nt$?qpoyoX#1NKRDxAzYTXbMH;}{@gK*QaYG71B}jA{UXn}? z2YTF%zux}rB@NjuCMQ`peTsxkTH?EAuKfo;wAWuZBTRFrtfe4r21I8DC;dOC0 zdU~Sv9{d&z!UoF2cY^8j-tUn2Npu}l8T1nq_}3>Uo-T7PKA&{^2v0p(q4IAF1m>MI z@l=z&M+(QalG3DVhV8Unj1SyH1{LL=ux$(s>l}{IcBIQg#lwWaNDwS5$+JKM(S)@n zcvM~p>0@Ohtu)%M&|twa6=mZC`6$eoYPvHDZgrSBL$D7}p2*cB_r5PT zcROplt$5BCe^cwd&;rm_`$17r{btQkeUdwh><<)=i5ac- zsCc3WKoc8;$Ro=T9$Cd?j&KMdSRgP2q-i2554vDA<^)vk=-``E{{?Io9nyHrzw#*x zJ%SUFmBkcLbBV`?kT19kCQFz9HoVze$@RfRK7igYHoX^QI1PmwwLO)`L5w}zZcU>1 zGgyf*W<@yeL;Cl%2MlD_CVo5y$EWwpjlSJ2^o)ic_Z~~PXKYw})<{SqqyR8WtfmNI zG_d;5$oKExXA#~^{(3CJaGiN1D?NkK&wnc=rZGY1sw!vNMTwQPyvpvZgs}lEM7lR1 zMJpfb410)mudE)wIH+PY)MxhEQ;F)u415C}D6Uj=(F7~${!zoR(_!ZNwdMOj1Sq1L zz{7pEeL$=;D&qH+hIQh@G0PtiQVd;!J?=ihlE|^JRemHD&#O7~^-+t%WB18^gHo@n zBvb{eU3aYQ29L6-g~%x}$no#mMV7mumY@xUv0^ z)OyYjMo~V{)tk6KoAzW`HtkK~nyoT}J`#CuThjH!a_`sCI~?l8u*e42HH0#zn+%F} zG8V0TKx+q~9^S7O#?uSU1qTo75WWiQYY3E}uC9*f1%fz&MG;Z}+;m8S4QT&k8 z?b2eGc*l8N9FJ}M`ETts?zQU-=8m`Bj8m&cpV)5SAj!P!!GQK0O(-4Z)bqN_;X50$ zK7^6o18Jw$Ee_p5zi_3h)o&J&-QWJL;9Q>ukrE=VA5VfTZbGTcy0c2!(^O8)b8(W4 z4J0x}cIvAlLZu~30?RGRaFHM)Q>}RNJx_+RNMfw--=mra4-RmWHG;_H%C}SM*<843 z;lNvJyd)}O_}P><%&!ohc`eP`QEKozIK8aOb3GJ2Rf4b*BF2Ea#Sh0p5c+leEd+BW zVMip^1T+R1fRCfUC|TFi|G=ajd{892XYKNfjs;xSB)&gXn2;qVO&e6KMoB`e=&cRA z-0YM8yP}Z1u+Q>e?NzFq3;&SS~- z_z%M+v@84wm;{=yAXj=|uQ{+5lJSL`rdC-=dQ?>Z0jJKyiys>R#^Vztd7iF!_6^A& zOWIwlE>(xP28`V7diwf#z>rs~*KslEo?vV^iD0RX==*E)`j0tmrqvv}aUT}|hVoj= zSn{UWLI6&^tElJ&4xYbaUBd)`Qzq!u{+EKYvisFm>EdVMZ`hIB-k`GA$|_|{MiFlS zsya5J55O%f1gB0H?m2r{nSESc_y;0^MoFsBB0W8^KIWd!W z9?+9T2cXc1JiEsQlj3vF`TQ)O!`PPw)O)+vqB}A0*T>ZiloFD`TDDp@#X&YL^ylM} z6!IU6lrTIGL@I1HEnmPIMAwMl{GMNE= zsEw?1lsKS#f;KhTVaIuVK}ZbK0zy;n?IJOg!`eW$ko{A(+ZpXmP4c@xA2dtMDquI| zDtGv{UzfMNUUCFGl&Fu0&6XRD$v*TnbbH<^KFs@YZ{V7;dADZagVCR*mp=75 z(B(tJnu{v8Nvf=De3krAG;slFgE}EyV7Mr*QyiNR&dp=TpwekZ7miLCRbSKa#hf_D zPlp={(rS6Hfy3}&mJ4NJlB^V$pcDXo%Sp$^Z}K)f20;M21Ye}BDM{{mmdxP=Q6E@~ z)fBU zft{eExDYzB#d%AOj>ILH(V(;I#mOc>C)t<^wICryak;4B3nu>yz#1TrbksJpV$n~0 zOJ2$)udyN=y#MKz>BH@JeRn9JcK!&H`^cA3sPUB!{0dO=o_8$Qb&(&qteZ)i|iV8&Mw#yK9ejl{t~ zvZaM**N6V(eQ_OxV4_$>h`U_&s_v7mj>W05GX7rlXCD85T7bVr5bTq!P=*mhtwE8f z(*S9nLzxG*AkwAv;t`Gdz+?lxiC$hMBJ(DRf>3q3y@rs6*iGUKN^oh~F5DDziYQj$ zTlJuo82mSYaRJm{qcHvhLXIUg$Tg1gh@Ejb!?`+njg0}%Tfn0OqSrBXH|Lm}4@re< z?~*S$qAqLNA)EDJiXd80eaBUY9WYC0W#G#SyhO%!7553U?bknhr}FgE8`*7SKmbw( zG)VU_x3PtgOL)Q|WtpO9&(Z;I^r7X0XX5$0ac9f?o6UP9(9vHbI5V-jbjw9N`siwW zuWWt2bzhC=(nS2r(kZ#u&v_d!XkWyQgysOl0|Q$lu00xS4tsrBWhf14DXbmaUt~QL zz#kHmj|_qh=(N9M4;8?N>tOES&Dd`{gm>Y{_B~R+LT|(bP`D*NsoEV9&n)YC+yMGe z{KwgdT;mfy4A;S14x-9AJ`(m!eqZQMcmo#N3$zxFf=msYSHVe`-SGWH?>wIpkl)2M zq$qLv0sl%Qw#i`wp8)C+socK|APr=o$W{zwhLBiD)CUyf>u8@z`~Y|_k|s)o2zc1U z)B@y4KJ}HS$`}zSkkzICQfWyDUgj-vvoohY4h8%UAL;}}aRJmk_3*TV((50qJeYxGq*(2e_e(C93y9uBpE0uMCwNUJi*$VR6IXRu2=R}g$abr&?JtA2>Pu#>cWLr zrHJ;U^1K7bb&lkCrUar-CS7-ihK$S8uOm#U>RDajBg&z#AaQ_H88FJ^hs}oKp`U*f z?$+`xBZL5?RmX3i`>)(_!zdgEYrR-mJRp9DoBs-Xi#osL4tzT{mKBQ}mP zQyaGo)Zs2XIP^S74s$B9mV{gcd=@(Az`(#VRBRda*2ndV+l|US@O^uGu?M{BkRH<8C0x98B|s;w>NFcVf`aOi`4x4h4#6ckrD! z`)X(WQU|aFT^51fiFB??m?2i-F>i)1|1ta*i2B$jQS*5?Yr1HAyJlo$D25jSvZ7&e z!mouxw#dpp_u|AfhEj1H*i@hoN(sv+(?8ihPahy$laoDtwhgf-=LOW7X=pePctlY1Gf+xbgPM+I!a)dJ1brv*nSmtz53VdX7e6F~ZOgl9otZzLEb8iZqBjObVrxoyoF6tY4#{U?mH7>CPr!X8W_ zNrHirM;mJ_uxMbwO_tds#&+C>;FKhbf<))R^y1rc+~SOBcyHx(j|`-7{vm2s3eh3g z5Dvr%!O~hr<_-M&UQIZje4+UHr8M#8Lb0x<&DIBklLvqPdvH~gP@5r7bDsi5AQH4` z0hl<*EJ+^zmIL7*^Nu6R0_j+9y1K4v0Ma34ofHE=vSB47iMS6SRl^epQ&ujxqL{s= zK^3v;?T?ZUoiP4b{wHI`zD_dnWvDl%gFVDEYA_o_&~+LLNc3J3Qjo7?l|#N+gp{5o zS|8JwZfBGRBV)g|!#*i+D@9V#?uJ(p5D=*4aP#H{=%gHHk?*3{egAnJvjpk^EXQTA z%S_;lk83mlgH3|U&_Cv3KNEz#FSd6mBnlR!{J=rqLv|$KRh@i%+X#($Q*s_gK-_KR zu;|+95A%FlmA5nt*w4p9-S|4~;kagQLv}O_8q~COxhnw>@y??z^AJP z(-tmnSaCuVHrWw;O0G|u;te+EF1}!PcrWo@2Vw+vsu!PiO3G3ptSGI8bJU%O}9l=={42v!KdKk!x54(BXTPIh<&n-iwOP7J!$vu`8Ri! z{QkRCrTFzr`>SheHD^EnhRL9-tHcIC>F@aI1fV`c)NTJd&=NEhV9saJ!Qtzw?BDJc z2<^mte?zhs=y2NiH}^Pyo5v@TiU8$Jr@gIW#c+9454%Pnz<@_5X(*I{yxs@VAxD!% z7A6$@y3?WPMPn+qF}dBcb8aB)D6q-V(;-obnL190QVNAcoQATki!RSs((*n82@Iwm zEUJ`!Ayas0uNO~nDX%!*HmB$0h;N?-mxF~N`fx)O6@G~;vO{ZO1^)r4(?0)nNnn_C$%1IDRFw@8TZ zu8T}aWZnW3t}bRBYNToX;>D7>rH>9}XkSR71m!_?Z?puBw`pnb-{#P8@TBUTlDcyj zrnwmB^a#O!QGHmoi&C5a{uzovmg{%Wx=8(e=)1c2ugk_UvNGutrx&-^pOLNq3m_K% z6F#6NmNe$uak&LjOI$SgQK7>M#ZUbeH#>aCXS7xdkhDpFkW|`l2RM6h?28ZQt}AM# zn>3jS9wSSOpe~54u<<_Yu_7fK9kw+jq%Z+m?N2RY#^iHP{VUyH%mHs9T7bai@$m9K z%gih61&LCm%XRjcz_7S9a#T+<1~69j;t$`~Ae7a6$Q7gzl=$M?5uekpc-? z0#)#u#$B`EV>LC?V`OP{5}GKbCv6Fh%LMCy>pKZ3kyFS(LHM_wNU~7F#WJltIhWBp zOiVUJA%-&5UOlVq3m_hr^c3{69#3BI3Va`>5g73bzkBy+CJ+;4^nvKDGh$zG5XCTt zPY8)PL|!M_-|%>2To=?gtd9nmZ06YE+`$21Ap#1d)NB|qE#eY2Ig_8Qy3XtVP@4@d zA=s|?uueIRV`cTLB`E=avM?Q;J)pA}uT9p4u>k_*1}lxf1EY?bgq-I53tUjVXcxd# zDmwm*4l%b1?j3#z(Z?s#wM7FV{$(JOkg-6E_a5N?IjoT1gq?uE?y$*rJ51crvacZ; zRv3Wy!pTrSwuFok2I`T1`knT zh3J^YXGvgT1!*fbYzPG7nLCW4Ab|dB!-_SV%Fwo8BZ%W?3HCpx&X?&H@ZZ7$+y|mO zXCM{eI(CSp0vTgX%+9Xs`QjfdX@?3y%9BoL#g0FvSSF1x9}5QG z*tC?yQ4!(cc%@y(RhAboP-;NmIs8>yT2u98Q@E@!K9bETVbv9~YFb_9fA+kJz4w|~ z@Y)f%sT<7PDfX;2i7rOhO4pN4l&p$HM=-onKw{S`hlYyK*#~AhiV~Gwa6IFpn*ip} zfdDtY88d+Hq-bBy$;r9z)F4DcbN?5IP_J;=!}8PCT{ntL{{4$cIp&P8U6E|e> zS9qShRXY80Zf1{br`hD!pYUH&KIr9!UfuMJs#cJDl+ZSPu8n8;XKW zYI|5VYvRps`j2_TPV^2NqQ$!x9bF|GjMgiA<%e}Co{=~@T3}#u|9J!;vN)EccCBx5 zVd2l8eiT{&r&xH(jCXq;i^u?yMCrd;vDkxl$dqsms2^1Z4?S%8|7775GEfkUs)slC zdk+nZ)H`hz-t$!j{0_R!7b>qW@VJ?uAX~f17sy$XTv6v<=&@I4m)IH~28)qND`d~b z$|LX@m-wJW*8+yPy^%JU9eX70-!ITP>`|1ZJE*CmdVTJ6RzRKZD>E;1{-3J$ex?+m z)jt=IcnG2#z%s=}2oNYAOwB>_jgZdZB0JqsXJ%r-lQexA`=s%A;PNQg>M*E2q0olD zlI(YY4a6U6C?s8LN(pqBF2)^aC|!bfu(j0?Q!^s^o zeD`od<#F+VG+=f#Irqc;>c`<%U@d@)hC82hJzDo!3Ca#7;in4*xR6f6B@dpbkbVNlo7y zjAD{~hCH={1Alc-_+1+BW-*D(zV3AVAnU}`)YP5Zx8I^-)?d!~Xh$S|vtKZDRc=hV z-C`w}^@7F%?@(Yr`!tQUi8csoTqr95Ac~lLL#ZOQ7(jO(Vlk zP(TLy3-6b#+E=h;l}utAT6|$l)pj4^Wk%23i(vg7LBdDCitfIY6=QG@{MkJ0h#!F* z0>^7Q)L8;CBIN_Mbv$ok-LdP6@XJ=P2B4){-3>Rw%j zN$|Dn^q|VsKNLkcLBlgbe@qrH1`fxwxpyYdCODjK@3`DF=|CGPKDhYzFDVEAHno!Q z{S6C9ALxdbTLbew-jRaS=duKEM7FdwW-#ooK6~Zkh+7(74!UIB2+_i~my(S_GQe1Y zfh2Vm2!;3`N#p#rt+#?Qer@9Atp>aUemwPCLu%wsOF35Q&j?E-tLsn%w%3ZY?Vm5E zysoLD?%=M#O6l12CzHLEH;1?-S6fZ# zZ_v9#$ynf}e%Bl_Vd4F>&gZlh-@5Dujx1)J+xrp|yd}mO552Pk&hIer1K52p>pCU) z=*L&Ze1j(eMy4jAZ;~7Yt`KOJ!4MR1bjGk2^4 zijUF=Jw1LT><$59NJRG-g1E8HDN8(+m?vWn5uj42iQJ{gKqTn{%8f@<6PJ&y6yQ;C z`ln!ge$H~j+Q}%Z)PYkLp5!#_m9sQp^84*aUNB@$G(HT%9}-pXA}Qi4#FlSxe}h}1 zD9~(qw(K3%p>0Jd_iR&{ zZ~z7nz^%wG@CP<-g%b?>0tZPxdpCdc>&!*9%1HaY?z`&}pUKk?NLDK&&G8a1CPrH4 z{MQKO6RFqCx&8VQE_2EUYCZARN!%NB6jACJ?(y24#j`_38ix=iCx5HZpnBkRDU=R~ z!aZGLva;I4Od^w?k$tWQ4*=3nW7eCSPqVb7#9Vi8!V(+8Vh4>SuRT`hkXxwN7o@hn z7wsg8kiV$^?CmTF4IQB5A=p9@xhF(FvEgl7TU5m49uQOr;Sh+k#f3TiP1pMV3^}EI|de0RFI2Frf*G~y*#xR?f zyTX+na6dnM@Zd@TloTlEM4@Rgw%znSD8rGM`zY|pw^EeuyPw2l2%Q`~1v*h3<3oK~ zc}1HogHot|@BV0TP*%uBoP*jJQy|J-``=_?=8{X${1`Tcn1{U^Yy6VM{Gh=B*9S@u zc=x9-mG+GXV|W9-!ALZKx`y38rIqL&XerVWGca*%cyj5koP%{@XYX%IQ|Fg!#t~PB zNPz0tyBnDqA+}+}?e-qG4M{|txXEdiTdH}J9UG(kq54Ka<&0~=fPvkxaIDhe2iCVw z|8)8r_L-%VxF)QCN)UV{nW?6FP}mN|BoJr?+D=Pc>?Dl_BN_wN65%o>v09&BUd$8} zMg9=kSCIr9dbY-z2JfFk+(1+ke;C+^Lqex!v_}5xn3%yZD@tw+dFjw#)WJqxi6_rd-mR=&gvGorrGMYM-F?@TIVQQK zj%8x){B7yi*!f4s`g^>GH~pycfUvA7$6OTvE%66utvY(dQ-wh_3Xm!j3;__r;lkHI zkO}%>L2M?6TuSnjezs;QN=ee?2jS6MHD8$Y$?@f{vl6jI1CN#;eCz4+LWvP^@+xvR z(0F5(Cdq<#Qd_C1siw zGoC*tw3f}QJuLrn<`7&i9@t90WSl%@P{8A#=JE)3e1ANefS|feH7Whz*S~QA2<#8_ zjb>kE^XD?YQSBz$GC1?gN)uzV^CLVmB13Y(DgtJ*l#PL{HkmoDP0x6WTgotYu=y|B z%{2T#SYHhxUM7F1lua2Uo-l-Fla+j2JZ)2&eRxBt17wAf^?+voP47#svMO3e)re$DaPLH7DKwD6bh`D3vQPE%J zO)7(HrPT23IF~LMeNmOKUh~@97^y1fafY?uAnbvL3M<5@2TIxkJgj}!OZjeW;M(+i zGMiga{S&%~f3fcd@=ap)-jA_1F%{>9EMNj|m{sNV6Nn{B40yM5(0wlc8O?DzTop8) z(KcGWd%V~W^D%LYM$6f)K>@>CF3XWxJo$l1Y+K<0;mxi5j|Ca2xulg5VW=avt9<+C zz*<>UD+6#kK4(feo@#5oA2~KRe))X}z&9Zl%Jie*<81PK-eV`a;RXj#0jW)Hn*E8B zza_KVW?rs<4F@6*aP#VmiS^fdc0l2DkGKIq6=pQb#I@Gy(s&2sx2VKl5%bn|UpMIo z*?4*DXF4uO+UYcv zhSoYmZrt47W&{nybNIw;@aZ#qoJ^_Mw}O+%DI&};e65>=tXtlPJO~)K@!uU#B9p-m zSQe|6?iyYwer#LF+v)$wp5wk_V|8?euM0VD~dII z?&#}YS3ewi?X^mJt;M^#7L4e$lnVAq<74z#rJ&I?9)iY^9C838&)p|eOtb{+jX9TJ zLu5gf;Sc99iES-@Xj=1h@NWiwadM4-K_YH+^szT!Gh{Ts`im2NdjJahSHxjxKwVBW zo{JN2*HS)^I!5o`JHa!Dw`d9Gnji(Es}RBtBhYrJLk1cW6UkNxv?F-!hAs0Sm?mV^ z9U3z5Cw}WFl)$kk{z8;A-wwH@!hYk+odS&7aEDjvxV_}-Uq0%6wlRgF^9F0^w%?2t za5cM<&1C4%($wrl*HaZG%_lHWdP}5GCYv!Yqa1ucj74EM-^A6Ev1)6KM^XR9$#HD1 z&>#UL;3&ifLb5K71(v&`f+p*nKLg*3z1G(q-5&G`rm6cu>0JIA`0kR}M|1TqN3id=dU z6vbV>1zh^}ktSb~NNkT?uzmM+nwB(@fN}{wkHY_%=g@!vAjnWc;BDW2?@WwI&7s;g z*bNk|B@0sQpY$_EpdGtOsU-Nvne6nx_>deXJW(WL*zRGJjV)+rP~owH&lglIJT4ODA}#OJ-0eRwE_7RQP}*1-&IYc$Z> zDW0vssKaSm>pzffcTf?hD2gTP2!PH?@oIf$hkFQfI<7`K zsu#Hey7e1$9fcnQM!}_quZFDj1MdvI+q*+Y8zVjUhWHG4^T}AzqsfKni`-ZQc0#dz ziwqa&t$71fGE7-Xu`ospZj@zQK49BDJ!}1?>Ob z+-$JJv9->rWFXVnz4R@xFocCxkbo$#l~frBzwNFAI%LEKHEy!)t!Rg`jIL(zVllR- zQe;`mj)S-KGW9&-%I|ty&JjER+NoW(U?^Yt{4>~mfSdsKw#0jJIw@t6?;nznIi48f zaS`(dNO|yb%c$7r1KEIDfwU#C)cNdOBOAB<-Q9L_oWK=4hXCN!Lk#Dq@pR$f*|Bne z&OkmRpZ`w@%{Veb!<&Vjp3(8)4{FpdkHho0nnJKrn3eep`$j=vV%Aq&cWrR09NqhL zgp8Zz78cbsmj-wrF?=Km@M&qng#Xs9w~)AST)1;2Vegj$2wrG$&Yn5H&~2O~cO6j! zKsY0}^{`}R-Rh*ap2zxIaX}+BSEKP{#hOlSh;+&FYV0qHcII$>!@WK@(O!p)0AN9Z z7Cku3P?$;CIG;QXdT9sX%v$8XKDlPk?4*3Si=7(wB%B@*b|- zYgRjURsUmNAszzJ(&Mt3Wv^ED2b$Q650Z&JhPCrNmpP-uFhr5n7)y*rUd(xGgij}S zED*~MhENMxCKVNx?25J$W``yoe^x!1*zrOEAjj;N4&?rsG7VO+3b-Wq3q_>hJ$$Tl zw~&+XK=bi*`fns_1V}wKV)_w4Eo=r%r}~g&MB>tJS@?m@orh*ht)O3DA0!O^HE9Sx zz5cK&|M-`nPvQb5kuk+OXZ`rhQ?fSJzX5$ET@fsDw0X+f60sJmTE66*c=p6(OL*bW zQ<8=zJ9qA6CF`&d$V}J?+$fshh8xrNuS4(2gKqO3$j>}1o*?$X(bnv}5Ssv}fKVh% z%;1VkVcBK@e~xV@H;}JGvUbc75m6bT0WM4j%hd`c6*Zb3Fu`gBwPOIL&4K~Z0~%{K zY%dEs<{2ko`yZ^o1l|W1lnj3OR+0;R@*nd~??ZCI6gc-jRKB`aEiEm~K#T}B@WX3- z2J17ZhBW||dVwPy5I!UK>iV>`9+q*d#RYoYa(}+Vv#S#3VjV~`cf?Hss&Ou%y5~CI9!MQ_VdCv+$J3y=lzCF+J6IKDD zP>>C>NCY&b=l@Z)Y0kq4j|;GjC7dWmikE)4?+M&b%dzqB%QwW24GQ9P82d4+dr`}~ z8G81);SN>m{!iuv`GLiE?%aU6y1{kdJ9ZD@$V0=Cnq>sa;Nhti?DViHST0iAp))fN zoAOPzO|7xNn!p0!u;A%hpaKx36!4P+k&^RG407H}i^BNhh&&guCC%~K@jlod1jG~F zQNE~p#C_;&7|DQ^pW_}VtPt>k<KgeTUU}B8!crx+r;&1zx0m=-PfUKZoTE8s_2xd68 z4WW^Ji!;rwK8+ytWCA20@>3ngM9KO*@BZN~^h_8tAQQ~|zPCL#xO!!O zv54^BCB(mTQ_)AQ&n&+yz3Wbr#7PtbX&}HqJDEp!<}2f8qoe$9+<;1SBc5X#ns%q3 z^@*xIl1bGIXSv%S6>B$rveO#J!&w(5=h*eK&}t9KEQg|x>}1nuS`Y#6vw}oJf{Y;h zXQ?t+yH>#<$j--i7rkIPbOQ2ozaEmHZ^{DjWk$rTm7oP89kH9TT?VTDKSb3;no-9$ zm!U}`i6+3H?G_$DVQ&Gg%yYL9GHKJK9p{-=DP37Oozy$L;;?f6pxir;eL8V(d5c&;VId*N2Y6d~Wo5ddS%fjHCqe)Z-omyUmZ|sAgci!* zSOsUYZU{c%U9|-(%rBj`pI%oe88zmQuZaAq=rWM*B|Q+;5gwH z3vIvtHdbU5QU;We{~@dh)mf?u8NoC_LqVj(U~b-XaupZE!&9)>P!w{qVovK`#I2ap z;;QLy}{S5i^UPQ>U%;1ea9ZC1d z-rC;(7o-buhaSr^mXWJ8a*Ou3aXj0zuIbmHJk+oljx>%Wss#7;T0AsyK766=PLSnI zCrmp07^wH*o^@AZIPo6`7VtrmiaGhZP4dmdEG@ogf%IBKj2BdP7AYAqsPGaB z#f)kH9v{eKxxOSSx|kK^d=fW)eGMnxNE>R0w(P2&_2Z0|+|qefo-g@;QGDj|SP z{y<)nF{EOc0vLg0H98cXcVpJtiN8cdP4bBl082zOjOpeRSV%7rhtL+{L4gvDR3i7q zIT=AAAvQj~h9_IWDu5U%Y+q7K|CD$BLEGql09vRhtG|?7i8|AGd&)k=`^b3Wg9B{x z6Dr;y?#>}M<)2QRu!B%f=Apk+ctV6NC`lG5Bg76y))S(YkG+4xIO4sx5h2v}X=+kB z<1aL}yd##ALdX2KFO#`8!~l82*E0c;A>v-QFPt4NnA*G}c7|~%R9K9Cwd>xD?Yqmu zzLWfC$S-hpS}&+{6?;tI3lFadtj_Xz5Uh|WCcg1n{0Dc!Ea5kY)FkViveVU1%L@Fd zc}NDRh8&NOF?kH&pr?{K5nNzlNlyUBXldhtJUTcp~34U zV(@fHAOnp~j`sJ))En))1bvr+6;he8aT2o=d4$+-T;9-dRN%b2&Yz1|BzUe1hk0Ug z{sQ`TqG%*iHkM7d)!Fj$F5CKL^^@@otvw-z7I6K9j=+*Os^oFEnpS7z65ajA2Dl_5BHp&S-W^hp%!^3 z)~{~dY@_Jzty6picU#jl7juzM;mAv*NEpUvrHuJ*NAkJZV(E9b7g-95Sh=RFlMKU|qmBNI)`II3`VLm4z|6fJ1TV>FG;IeLAMRd$U7@ z>ebDtC^qerZXZ5g5lTqpZC%@V1S`W>(N2S$FpT(8wfa*19XDLUdLSXt=#eZ*)?rUG zinkPavL-2PrOAh{?UErXJyKo}PnNIesPFkLlJS}`ZA6&vb6ogut!0w|;MVPIX9@jr z?QXxm&w>LjxtBKs?5Ck1fggBaxJ#PO?Hg&sHzeYYMVsTO24bH>!iX88$7%n4*pxw7*N+>#BkHQ^a5}jF z^J8Vh=)tx#^0X>)=KU9^Pv8GQ^M@$OskGw^$bmhq%LxJ8ZOjIO5RpO+W!wJL>iPn@ zHqMw={h!wl$7k=AV(o#h5OSH&z~+P-v3uBmn~^ZyTIOxXpTs-J4MOMS(KI}ZK#YF8 zTrlaV{ckbDlU(&c%?z~7Lc4e|m*VBE1tx(HGE=0`+AyiQqb%$Z`Ve73L0U*jEpF-< zm=0myA#FLy20N-UPY-v;JG5uuc?qRUhGcAoJv4@&SEKlbiM#^V4O4(ph*SdJAKUVS z8cSjJuv?Q29LT>|(|r^P7kX6}9^c>U@)fxIB;NVHf|{dfZqcmE)Vm*~FLCeU4i+Lu zyVTno(A0LZXV3)8HQoQY1>30B#%`+c-d3`?@h)r5z8}wVy_0YXXmuDV!kb#DwXNyA zwv!RB3-UJ&lKrczmnu<9ou+o&S;)TerupB1SjU8mUO0#Wfq6DlhR#WWx5ssPF`Z`kf5wbJua z2$p99S<(Ntjdp~69bzP;YR&o@%%$Rnon6C8Tp{{RNK|(2Q~Yzunpd+4o3&Y8T*h6o zAmMY7GmqKDFo)n?Md`&~jYD6sXhYGFiero2afZNQzymbC=uGyQGf3CE5g`nGvC+!< zw&mv1Tf_k=RinO{6!qasc4@1)Oy1{8ozDiW~Ioj)&gWo^Qv zyr&PRymRm|p#q&O{HoQ{3c(tJ6)f3z;K%vaU9ww~)*w8@!*HS^3@hGE|3dt@69#xp zx;3xSGjW=OaK7?22MnPg$VvReTgWYmH)Rv@SAd)Cg;=#0Nv@U5Hf@&R$ijrc#Dsl_0CLM*3%vkYJth6uAB@O}uT zzw-M%h6EZ4t~&yI|L6k>A58S}h&n(nxq`I`hQ@#7#^s~dElVZ%`U#9W9R+Z#u2pZB z$fG)4=)Cb`;Bu}1bNvy+g64ij-8VAkuRV$I5XZW&ua98IP+WiLj_Wx(QSRWSax)Ql zj1Iaulj$+E-(;l+kt9ID2Rv*YqAu?OW3~c4dMuMZWqj)$mxB=}7CnHT+(VIL1qefC zFSzLg0a^*ktuni0ytYE=04$ZV6%`etmswxmS+>9NYcKj@)HvA_+oskRF;8xE_fU%~ zc@5AV%;H127~VBEzD>Xt&Cbf|12UrEy=CO4v@s2U8_+rsGN6MewPR<6ogx@NozS${ z5of72g~lI_gmBxnV0R&KlU7y|GLR9sI7WFN#R8#K6yaoC_1-^3`v!>9!a6Hf?(llP z*g@<4sbQ)v(5VC3HA#JgQ26bxP@K|25U~$QgrDscT)H>mdLRKj6hBBf*!H#YNJ;L{ zyX()OiY0;P#P)=u0}}LR2n)f0;^IzRr(fvo->$1yhlod{ASi9D$P*gnKdXZ2aB_P3 zEjqkBtSOPZVsm?RJKv5LheB(x@RDy$;&U7Z*6bPbUS3G_RZHo;`MoV*ZqtzjWs;UG zvNpsdMttd{W?TihBI)Sb=K8NZ{}rcAXM z__J#8WZ?z!VQjNC`7$Fva~1Y1G4t2TwfRxPs|cowBT)bT!>F>i3csd}I`|r3mYn9e z$!eCXcJF^8=wpG7^48 zt=iw<6eYZ0U_^WTg;WRE!DA=Zp7AlOa~e**w6@u4f{gGK>8MF3PZrt)SH}AqWf}%u z#l%a}=YXz4D(Yjy_i0%-uwoQ6Nc*M*+NPdp;ea78<< zX&OgqC$V=9lp{W9{CsH0*WzjkBoh$1S1{H2M=zwA6t2LI5`KgySQ@4w_em2s1I!V( z@oq^lMx7bWNQ8`mVBn38=bV|zBg2#Qi}={fs=UCs;7;xZY_byyYJAYw(ea;#iyIA! zpN8vo)-BZ|7;b@g1ON#EeueurIQwS*i<`5Df<*^2?dGpYJ&{;E**5wsjnM2EEPH zIl#T22axt;&%t6Y0?FcfL%>2ESI3x-4i{Eogdx`mJ|W;-ftm4RZaqkVZ@qedYv)+n zogco8w~L(GeerDa9wzt8w9kOLAj)sM!XG>lkKdlLW~(Y^q@sl+s7%q?~C2=Y*IwUwcF zVcoc~Mu(ds8SU68R-MN9P3faFjwD$%1c2*~yl#XIcfRCTeCXdk=LsP$tr-2sO)zp7 z#;#tyN}wQe6sU8HbLT+^n@Em7Et-rw1xM@vP%b)3>wPG15kn!fBZ3NulNr*4s$<*U z8S%3#@KRfG~BlA|jo#jNiM6iK-_W z0z5|d<2aVis8XA-91kij4{=$WAl$_xQ^;I$=Ui4}9>QAOCKP^P!bClRj3dPbaUe+x z;-*i(@my{EO{RB9cu*@CLBK(dK=z12$>1khYh3EJ2TtXbjVJr7r7!OR2#;;vkBqW6 z3T4h+kp-DZ9oxb3b^L^x;AYLz5MPHPJ7J-{>chej)gTT;Fo&bbK=P{!LcM|^0zlmS zp3cxbS>Fdq*y13>jd2pa7?L%EVB?5tm&pqaxf23_9F!-GPSi@>ux<6#BDw9W$mtCi zBLPKu)Lyi*Bz+$RT?mzzEc!-h3)x2ld?*Pd4QsWTMw3LqwV8+SsO?a97BIVNV>4Nx^Q9r771%8F0iw$uy!~u%=%JA2-v4uy)G~KU3dNC8u z=T=%|mann1=1Kwlrf)Gpb8ZSctOgbuMOr{yoC)9GG3#iXfC9_vaySTuQIdsGIkZ|U zG5OhQz zkByu3+<*Ta|1yoZP7C0dNQct~c1*Nqs8**Tb!L_8X&Kvc(E&B=@yQpmXq0^M!Q@_m zKM`39YsqpE{GSx!5BQ`EO!N3PeS?8jt+ zzIoG<+ei=r%el=?^C%~zmLP$MIOdYULXf;GU^I8om#5-;A)oVtIc`Bf+zf=0`QK(1 zhNxyIb?u}}I+D}rr8#8vyjom#N%cnIgmp)qu%jZ{W;1Tg99sp&SD+MG5Fy7*5e*oW zI$SA2((Et&7k^8(tc(q+nfrBl&kMxgV(^4#mA~U`;X|9n1!BPgI`fqX2YLyYjME!2 zQfOHG8n-Tuk6_h1^RS(^-SiHBa+THFP@$i?^mJS4UzptVO@8KEBOM5Ag8#8=LF-=K zg#Tr){58>>KeLNFQ)7NOu;|TS$<~h7KXV6?IV{}L0`Z|==~S#-CNXpGu51bWPmFns$9t>Mcm9i^F@{~9S7cquYgfl#(cqGz>IP!7%Y7?aKcDqr`mbY44!=y*S zI&6ftnm=-Dhb2$1;)~)7y4YNS8_NO#9d#1}gHMQ&k}sBOtUD$U?v-5q1zQ*sfTGpAXuox|#D` zU0pX(P7@;aOuw6lm@MdHdoHOpNX*=4VZl$#F18DEQ~)ONPs&Hol>xEnMYne(*lGR! zGxGc9Z7OcgsX7W4;Fy{`JIf1|oH1AGRXK~!q_5c!bHfr@fp|pDxJxH$lnvv;FrGpT z6@s85?eM9%v){jd?0GyZwfrANc;SMsnE36Mw3O(;fqd5=tbWm`;qbQ+msICB{lJQ% z2NaFK0p5#+vJRKCu&Fl&ScZ@}wBPmY68Tm^H!npeM4?&u7w=1A$Ze;N$E=FPK7rVg z@Y?(!h-H%TiZd=RMJDhbeGeX;kD36oaH7q23tgc44+wcf;I=zi>>d()VkrT$+(wjK zU>n3QKDXE5tCqj1n((tSRl5*_ugf1&(E%g&L6)*W5gyxYa)7IMya9w6P1@*w+?-_R zPX%Cq0vSa-KEcCAmfMh2B7Bkf*3j|A$bW=i3hq;W{2gd3=~+bG#=E<+@@qPtj2rUX zro#P6G%i#b#9RDJM}@nIW(}h|See-R^^+<)5T*iA33Te*MypG|FY0!_nv7tKq)o&~ z2T^A9b{f*W0V?GMG*bCt0qO_PYg<2?ZUkY5T9blWIx#)%w=;e2Lf`Ulcv1w##cwg( zqDew!xV7-MGZ6q&KIU{;(#oRaN=AQKm8e>p5pR5HZ@o%WURG!m8n*vw0ge{l)rzx{ zDr&fM+|M+n42nM*(K!FkTjtJ{nb)56k&hR%^G>&K_Z)oB7)@+{!LYRJ@8Tjko;VTt z@_|#&99C^qVv;eHj;z2s7Lqk2Da5~3XaQ19gB&wnsOL#GPfQwfSKxct*P-q-xEOE7 zuxPc-tO4)wE^K=wiqk4}3t;JfZS1nT2fY*qJrL^q3|~oS%06@DSscnF01B=$z})r1 zG_JIOY*tWkNlCsv>}gxI0*ofX9Qq3;*IIi)CGY=xp`J(bt1K>@`VpOvdZY~rL9*q}G%56&C?cQW*q0Y!@~vO* zf>~!uH__CNF%i^F1tWlmc^<6$KPmS|sBn!<*P5+QDXl6^C%!q%bG+lQ* zmi^nlq$DGfO=V`JY)S|rqdZ8WA!KA#C@Cd75uuO~GK!F$R6=%0BoR?4q*4)OzQ?!s zeV@Nt6ol1;;Rre#ZCVPS99eI4_>Z(}im?D9W{`aQJMEKzNGFg{Y zHM*j_6xV|-iCK01m_`$ooSY|6H0BPSY6H$2j0FS zLceiHnvrh{sr5C$c13q5hAJ~nw=DQv#E=Bk8z`ZkBLw@Nz&L`O+`%+Jq&6t|J%X3S zCS%LuHp0hD$3>er?oPxlz@VUuswONRbS7}1U0z=L@cX`mtB!%0+qG9!7!Sp^`G&sW zVXe^f)Rm|+KEDynMNs4pGmE_^AwsT0_ep}1jZnVW(SgC61kqJ{uxlm!1ckSv&D!M2)bD+~ywISIarTbY~6H#qYZ z1Hr7dmbo?V_f9VQ#yWzlk(tUZa)2aajz@@`4>X6z8@(r>>BG_fQUjjOs>d8_8*X&N z10EF9fuCm5YNPng##dMda0*oo5qS0LP4%sJl{ZLteGkoCy|_1VO$l@ip|g6ZdqGRc zLCY1ljv>=Uom1S{`q3e!Ks^<^GX;Mm*e)AQU+$*xM?8ibm;t`)bSNlEzx-IvjHo3o z+?iLxCP!a^w~+s$5uH=x7()CwH_&Pxe@V24BT zgIf8|;PR3q=c~JR^2^FDd43~-BhH$9b9tC4uph}s7+qUT(F%?aM{n{Gf)0@fQN-wp z&}JM6+)fr3R-Fe zL5F8tf)5kkZ%LWQ>4^8w;eS}&^T^ZhB;V%IY0a`V+b*@^0wAX!$mzZBuCsVJguF88 zW3wcR5C0T}t-g>~_Gi)SsfyGA=N-_`|%1sZPY{MpJ zOWhrc>(19|+<86T1U7@BVEZkwbo{9-I8OiflrV|+fIOrY0tk+(v3C(sZkiFUgBUSP zf==<7E3+ajto(7T6G0iG1u)aIU~E^^dbIgcH%4%Wl9)4)&m1dXC-Lqo^LUNRbOi7`5XgcX zDGY^bQfz)-jC|AOQBCDi6r2u;V?2Uljb5O5@VZEDS}=3l7r)h;dOUID;^_usJSvdk zf9M_S+4Kg}4>y-u`y}QT;T|CKJ7830*R)8{ZZpt2&{m!{+Fxk1o2rISls(hT0X8-b zNCr9Hrl9ivg)ubpl|w-L5qPr(;KlyrK<*P>_%#?SK~Z^DmG8bZdud4>+<^p-_~@*l zTL+b2y*-eBDE7CzCA?9@vvhS*jLoLI_*v+b|E~YsrBD+@HkwEoNgL38R4BlZ&+DFp47l(jw_mHo)w_dCU_IJ89+%w?qx@o z9jv$>x56n%yzv|wUE;HjJMFPJclJ%##-&SAt4^arTmu#Xtw~{?9&h2$dZrwvM!bLx|1mp-Cz65&NwdzTd_l%S>OpV@IT)SRpDcSsjey9wm{SXX~BxMtKLsD#TZ}G{& zK6-a~^8XSH1^xCd`xDvrw0&~ffxu~eDO4@1rM_%+csn{pnbkQt5{YwipcAq~&P5W!Ky?x23y%I1Su~TDKBj(3gX6!xecW7v3B@5Pj0H8Uf zVmRMbS>_GHj6iILU`5lmu^r{$fB~|T)F%{{KUHe>E|rWgjs`jqO&1);cIrPN+bTRY zs1Dr$`kPfxPZhjzZW{>nokbIjLyHgVmP9L(3W(2k6Rq!MC`GOyRJyRes7pGud>#Ss zq!5JeB?z6UEkJEDk_RvuhyT@N{^QPT;&B`ipFiV+-WRvZhj&>RWM=(uL^FV&_F{EK zFN;loMltEr05nb-C@6+}-;Q&IS$`iiw7FumA10i@D=*#Eoy1?IUe>l{*_sUKfT9a& zYL400lW!aM*&RTnA>_NdW@hXJkb{hZee0o{TO$*Aq@M0KxcuNiE+^IsNYmC4&#!h* z+%)<7NCk?s*mO9Hbzpz9wAS~)hN5A!>H~ldwt)e1SbO7^$(c^6ZA4aZz zsEOL9o5n)=K+h{+ybu6Xib8m^Wwe`a!M6jqx0LPEHfjmYxdTET_9};ja<3lFt`!b= z0T^wqjiuLmVRO%mcPRESG5xxM3Ac~zKgubvgAJkf!vd?Q?3iV}3^`8LBGl&)l zss&((gbb6f5Mn5sNUwVBdm!2O( zX)>ivtC_D<>HdR6JwtCoT5ceT*;bV*xn=^|YBdq*z*h-z^R6uS{;{_Cn;_STVge={ z@R#UK_UJA>5Y1i{O&}mhc|oR96hiX;Ai+HiJSGzN1@vQtXm+>7P1QVv#sg+wzW~PXoS;FSCW)|(3PPpKGI4&5y?>$%a`YOMYia1@{(q<)AkjOd zm;WYRRa$_W z*Q4@CGxsXc%_~DlCoVs-WdOq8nXwegiHSYFH~${1U=R?{b^k!7+!wu1nVel#Ez766 z<)HUQ9+}1(d&@X?R1rk?f6+M57FuKT{~m_Vnu&bIu|dzdB!9+0$;swV)WD5 zI`>oil=LE4fpdxjEyv7ODDkew&xn4=+E!Pk2t)`^53&P?x)MF>hgiUr44mS*Kjhb# zOcqVa8T%ZC{7Uum501!tcaFV=jtl0&RlCSiDbH^{MO5|tU8wwbtsDDL@f+5#Ssk{ z#r=MY+F8$9gmBtY+Bp5heVicRJ#yFbYH-C^VK|My<5a~Kb`;n1oWBdkGBQ6zA;&>znMVx_3EY( zQsmRa9kFj7=!T58#9xQBhs62fNKkO@qQ?aaOk(}!z1~~*{CDPAIjBsKJQys$zrZ}* z`q&9qTIZzuMD2qT6qwb4t-+-ucD+Jlsum><%vg;e9@nU!xVb)6n#~BQL}=q1a1eg{ z{(Y&TZtJd1K$-Mmr$PE0qHJZ~G?>T9of~t3gq_*SqakK%v*fR%t-J^MN+2>O03pFlBbagUNjK+Q4m77(fwi9KOc&60cqaWnfvUtWeqs-x29oa?*N ztLwI+LU`Cy7nzQmGtX%N3QA{P{?Ox+TW-5|nEx8C?(I#0?TC(gg}y`Qz-|Y(ys_Hs zRfcQ+P9B1+gbYlUeR4{Hm#7?3Be4LS(SoSZGNEXzhRA_&#bqo>DoyhC^G(t(%TZke z89Vd6pU0N-k)nR4MR6)h6>G~Xh%D$*PYbZMJNGtRsIRy>M7Fcl6Eij0y1#TzW`WO(2VO1nB-r%2mRRVT*+5ohXN2$z$H6RDur zkFN|14V8KQJnNz1bkVpELgz?s5)nj0ONl`QzYB^(!bcPZmlgkFsHf76qjyqwf@a30 z@VW0r@inmIvK34n>csI1Qx`SPVRrFN{v-8k+_EdM)zSZ(+m(fT1sse2myBJFEh7tb z1q`H2N;*&=+-Xuf#KrUrt~E^&YjPFNdvGh6QMsj@gM*Q+{;8WTmi@)oMUQx6w@}^V z?(I7Vn4EovAgdY1Dkh0z^^+1-_kM|&-+Aa@lH`jU3yLxUAS0YUXGX;9#uQ2OPYh)w z@#u@gK{w4OKC|1F7H5x<=^eX)#sFW!Jm_P7%9{%qe)ix`%U8}wZNIeBgQu$~eRvE= zZXIeG%86chU#nr|t-(8QK%sy7 z*N8suGC^eJYTl%+Mr(2!&c=m>sdh4mQv6*|1fGT&~ullIis60^(9D`Wyx( zSyFaZ?A#e{?f?Gl;ONIY-&*?~V0|jEQ35kagI?K@XInsilR;Aj$O1 zz-e=eoO0-qUO{D;QGBS=%#~r>=lEz3G}NXUCt)$u{S+Y8%H|(;8SxDB z9$tV8tVx->ynkrKdHcjh#oRW%$2g%UqvKsvMWTyrIh#-}YCHPpoRf_2wah=HNMtl9 zy&Ev)UPFEfnhgi`R_)6VK*ItOm%7`1*+j$TeHD6O2TKP{gE)h;XOIN%fz;Nb=CUI{ zEKD@6-&y+{L!d`EEYMvpnM$_BJA6oo5v>ZD z0gJ=+E3NysWb`)rY$`ON`!l}PY*;Pb1l8zX2TL`{sRjys(=E|D zBF)nWP&1sx5dDy^B*0bE$T0F;=lhhDY8vqGbB}$pX=VX$jIMoZtSje+Wxh|&N6pk&n5iTV zKg>HwxN=b57#m9bS3{MC8!{MlG^+w|&8(pC+;HZ7dva!cMan1?M9!qRfwts11eTCa z)nc<2qW62%O3&->;(YOp@bTA^S*a2aw6?u>u?X-&iDaB(Mhd{pS(iZjYIc7GK*T>( zPnmV)&e92?S%t&P*K#oav5G1b^LV8v}=`ya2H8V>d! zsw|aO*RkpNbUwXyPjU`5YCni82XBz}o{xjW-4n?rMyLMqn}EC;0nsxhE*_GD6_<&! zZBP&tJ@`CVJc_M`y)s1f$bwX1C1y=DF^`v@$N4t26+D+0D8Vxa*$Q zP@7M?u@7`ERTTQ%YkZYG16g%l(`Pfu*b$)2O)p=Dl$W22KPY<8ih~?q5OCfBt(ktq zkNIRw95@%TI+Ym~mn&Fd@Wai(JMoWjIQjSafSMQD)-&9=#F$S* zys*B$>cq}zR-bM*x_v&4cEt*E3exV@m=iLvEzHx>FTjCD@<5;pJdPI!k#dU3+aKwX z|IP7W=mi_5{6y#m;wVkz7iuRk*NekD@Q@2!kN*-V0bmQdkfSK8$pVS z8!pAuF$iC=sO`rl8dJ}*Q})>gj@<)AE8sPt{OKs%KAa~B$wD?;-MH^!%^+WdhLVhB zAV3HVLrmSQ6KT<&Ax(>w+3lL!46N=LN8wf%0u_WtRY7lD@ye`YZMgq~_YkP9Rk>}X zbpO>M4WG4S@~~?!ou{l9JHx8KARcI_$){71Cc8h!P?t8Ixoj%Vj%^@pdzCV{zkjtY zo-5!?!WXut?%D+Gh~w&J1GBi0jH7c;FHDcoP`4nyW1J``SV7q1_9@%0;xZek3n>O( zRxZv;(Z%<*wShR>HV}z^->E7$yVaHV|4O^n=`{+)(z-hBoH<3#k;iAo1F(bwN6%wS z8DGyf50mGe1M9vnipLtDeEF+gb|?ZThb;stB;bS`1)l=)EY6?CaZc_5=scunjayg3 zu*DajnkwzEah>hvXm_^4b>H%yU4pd-X`<;MLv>xAJbKK%xPYr9Dk*78+{cR>Emm`E z=)+|CT^(zz4MyWj_KqjpXvP9E1r|tJ5RmWVV@|T zei=7cSrmPbICKT*f#Icbi&|QIvDL4^)J7m#>%l_!R1~kO=U~r^(VBDwvWxywF*LHQ z#-oOJ4>9PO=W@8rTJFqzu}N@Mw5x{Z29kJ3K+UV2$LVF2mX2j!V_Hn^veA9(j&_3j zfZB+_9rUuUbwd6OV+ZUCNJ1itMnGAN<7;Cii%{wKn;Xk0N%aa`PF1G@8+QTr#w z@tm0w52v5>Pi4xysq01Vn_2#$19_E?kSpz-lOL>5R9-Gd z&M8YvOZ2XsE}|`J#~+Io`L7~GAVrRTy0-9RZ%U#M4}19RTug!hB?r$FD*X_k&ci5b zZJW9O!6^fmW{>uIXn5E?>wl)7v4Mv8gLOqCGe_-<5NJlcANW1y(MryzPW=b9wS?}* zl4be$#n9LddHV>vh~pUonZk$e?n2kIif;^Wmvilr!Q=p#Zra2pf1Pg+5@MyI#!9PGKU%j(3YW&IPdt*j zH#UR1S!ma;O~l!J(?FC5gLjB!+WayL;(2j9x54Yf+)mDXV)VgnkGbW`4R#Av5>ag9 zX5ITRT8cM<4rm!$F8|FC-{r@I7ghC{VS+kGF{((^_p(ROTEU>aLuCv+JUrPLDbcJD z7{e`<`{#w?sit#m8U4PvFViRBXh9s6FES&S<)yu6|fTv)cFExpQJbEul}4dcUtLo-<}mLG;cpOGASfW3_x>d?{AFswokWF|*bg0tqMoe$_Vu|x)Crz2W{Qj?#*35s#@1;@{Vn7rG^5LPj z8@x^;B_Wr?PX91Ex_ZAysd$uwF4T`3v917oZIkQ$4mQB2g*Ci$fgA zC@Y09{Y zBZcwt2d!c2ARs0#_0R-6VC<^|gNUpN)~Y&if~|MkW0~s%@u|RkHlPpXZSbPl{8?3SLQOtKFYIQCDQkm977PoUyJ@}+No`3 zGxF@n`yJ?(G9G-;3C53x`cw%IPR8@|-dsi10foXTYurG3|NWsY_S9ZX$1mOIYjXOut80BO(+H%C|O;u~qy;4GMUionO`4IIot0!Xfj7 zyHfZ1ab{O%`o_k_a0Xw%jWBSLQHve=M9Kptu2|?kWRpSFQ zr6>2a7d>Qt&sh8XIdUS1&kBjT+=^D0IilWK5ZVZ2Ad`Uz{QV@d90IsMNTnm30g3wL zZ;U4Sx@u}0P&`6N5}3SkPeEeGtU_q3F?7aW+S;MEFfx<+5DQJn!x^rv0yO4*c zCvp7c7Nx438KWq;z8}_il`}Ik(oz63s>NCFJo^1EYeXB(S9EMF(Sh?RZ1COK^Gx6D zBt})Q!>AMF3j61Mjr+v{WQ8&|UQS-jOCH{Hf4!K9$OnMpWbiN1k(nrirIjUS5U4b8 z;6O|ffw|`Y5@~T&Sb~up`;dsS?DT?m04;3)3xr8!-?fYPqP~1ap5w-)rTJ6z0RkJ( z;dRkIPgVcVv&mUF)A}nOLsL_epH)F4JfiMXpZ5_FEn*950D1!GfD+eM!>-qwCvCFj z_3PJ55D#xZ{lPC^|2~K2{fxrNQt)5@GQ&MaLj9jvyUln)pRgV>y)AS%Zmrs*ajfp} zsmfruu|jnS)82eIc}$BK@E0G$%g=Ih5HQ0nq_Ey_(=KFh7;pTmovWxgm(UPw#*`TIAq{1Wdx zLXL#Q#8yLKLtsGCKEZ@~2asmAMNztIqJoe5emYq+^8fyR6Xt&?xQg_juz6+|D^cwi+svN6@^@e19L_-+yFeWTeSqqbkeY^h>C%$W?}xqPmN}yr#JNK_zTZf;)He z0mm;tRD64Y_H4F^SYbs!U!!{_+HuMcG$ilM58~KbZwqgTuDP9@tatL{HpC8HGuUX* z*!1FEOgBxYk*Gwk6|YD5@b;mPA2rP+IRFv0I)ATZ(7T!oz!{YjtZPggRM_NMMUXXMJha9^`Tk>u>5gIGb~#}?YRaFSdbnByTUSLBtxbd2Q27)J?mb90Li z@NJD1yZ5&6;TvdY(c+V7aJWg*ap=;Oz0I01=GC}kEZzj`aGeKF*2!1TR-ESFL%`zJ z*1c%B`@gzsJv84gxN3Nj=p-mCua&p=OW&_*xikQ0EYbeKGHNShWg{2pHfGqk_wp$a zV8E;ZJ_+3Zk3H|{UFKtOQ*iaRx5RpykFA+>N}KubxQ}cRHgW=){Q*kG+!%y z>={3)&@f8iz)nl#c!Xi*1i~RSOKl7m)ng4r$3fargsuy|plTr5R+jztxFI(?insNr zu>qT2ypa2$)Mvc`ao%}vac`5U=CO(HhdX7m>`Lr{04DWfE&45|H@!Ey3+|nF| z)5ZZuSyvKBjSy2%`gQ<9fnIp;!ezV(oq$5@+My}bu!QZsj`HW@kB_xO$?3n#vyN|N z@>shmSW6T6h{!z+ls(?So#(h;H;MT(%S4F=|2+M4S)ZNGg?d29;TbFrR5JjT+8i)MY&Sl)H+`w{FE&>9&b{n9%(hY`Cx z+IrF*qa(D>sQq-|&Z|khln?=WSXfzGe(F!|R;eOIGhkkNpvm9>FhSvD@-+AUx;tl% z5WN&d^0Lg=^%{L@#3h15n}+nTo>x`ko)aA|PDGX4Yr&n-eTvESaWENuFztz|hQNP# zSHrlpqGA?3qF$)YJOzU&0)8IUJF$rcHMQ)D8~e}Da5wPBM+Xglt98>^7d*jLZ^8mL zP>x>Wgr5(Y;iYlm!eh;b6L^SG_~q0eqzY)zubUl5p%%LD24EaLZSA0%jr#3^-_|sC z-;7sZUvbv*T_F|+VI#BOp8NKWBZz(b)=|*V$jV!R09SwA3{5~0%*j8MEa=9bDFePk z9~OAlKJaYy&1*{#9ni4bF*L8bYoU@Y#S6b0zLLU(%*ivTt%GKehS*NSU39tm!UkZZsjanq2ox=3phkhH+E z^EtU;nkzU*FD{)y+uW-tIuZlSG|62CT_BRqN_qj)Vkzn%$B1-uD4Yldht`Ucx%lUF z#!U;AwL8R)>o>$T{45Vt{vW@_iWu`T(SWgVtU#I^vUU0*3T92(sE?J#gv zmT}c>Qtu|u2D)t|KTPv6XB!Qmdyy?$7=fb%-#jDXcFRLy3vf-aiqx{s!}z%M)p*Q6 z^R2G+h}4z3J4DL-eraiYU7b|-B+<+`_YEbWBpNp$eOBewV#u>b(2ttAZ;12+fgtDv ze)T%0e#C+#E>VI|p+ua}ncZvtu-V?MQ2Pj8FhC+e0pZEdaD0D=mGd z=9;fZh!hPMBz%ifY~Q4{8qjHXVss}^4bWy55s~{0*A%@pj#IewK5f!)TWF9L06$RK zS55i~I-RyCUqNw57BJ|=-cFVJ=nc3Xb$|=^_?d4j3{hJKE`>-VXw@hJ^Lq4_cnp(? z&=6~)XI)KOSqAMXi3$UwbL~r{vQww_#k(+Gq(#!*A0(DEfy`=1KYC7G*ZvC#jomafB{2Pdnw5aN3gj2%uWH1|ac|KPlxH z=DG>d^^j0;yY1)R;S~7y`OScGGMnSVbW?Sj^5~ffE}B@9rMBD4>!D@>efJ4P(HjOU z04s=~^1*tI72Rw|hWw16l7*H>Q-4qJE|yMl9djiVXpG46Pjm2bYMaCdg7NceM`E1N z?ktnM>F}-Bwe&Qnm#}H;_1>Uk;zQxp)Y=+UP;e@mY2P7U8>I3A{6XDIkw}c&Mq(;) z$g0F4><@qDae}sjGJy*9+KhcpU>c-OU{TV24MH#eC%Jz(=EbUa|Jg&`XAG_f7YMp| z#3!);O3rgrg6Uu;P;C+dpiNeEPflFgSpRi75xCHxdje-#i>bc)n>x z@ApwJ4mp{NExvDQ*&>6EnX21cb@1oHzVQl@5ol$#5#=Eh{TD7l7vjW+f3`>@{mh^z zGLH!b1lrCR%^B(`qMo9NV@NYc*h|Va!dr_W)Kke1pPhI5-0`zVy%tEXj1D}_JeRmE zF_$C?(GEheNmFFfJcu{b$_r;fW~`=ufGg%zP>X%HJI=JB3$P?GOb%R=ltB0N-CtOi z|No6^yT5U$1-i2zu*nERUwxd0UBN;P$O7?30*n$yJ_L~w znCAwgF+xay-@oZ+FqpY$l!8|3TS$}0hNw{kldN79+Z*DZW!qt!taZpJoNaw)JIx=Ag(tq{#p|KyuIE;-M&P`MPU) zU`Q!47gBJvC#G-zxT4qB9-6eYg=7>hII}eQR2T!SHL7<(aso3{Jsjg=hf|8N(lJt+ zhC;^oWUx;n>#x+c!x(P_)O8I-X4Yb^j6hrzj8V4TgdrhAeL?B**>|yXd>w`1p9~Ga z1$QVoa$oadKx^&9?FJaT3C4F&bl5@DWe)f1f{Im`5WnIOl}Ev_xi-lgdA^IM48bM zt@*nL<@g0aUrfNAFx8wPIDoWhKxy_9-;r&Pl9)5VNzyC*p=5B?V&PAgkv znVYLv56HG&R)>`7P!ZCytJ0abj31OS>X^T~cU>V;jOWCA$LF9!vIZ2?Z)WG>h zykBJ40Fqs=T6hh?tAi?+bI=nL%+gFTWf?z5*ZsD$tHQ2egdmuJY{l{hg@{Y|(fO&( z#FC3Pd>F-0J*lpM!Pep*KMSe6ib9D(pM3nKzmT_+;TINNs*+4*<#&F&f!#Ug91+yZ z!~^039Z(aNCq*FwO-A&T6{qQK60OBGzfidBSa6tq1DtSPxK1oE$)-uUDc|Xx<5jk2 zB&7{8HBF>lO8-0VKF#0hyKDN%nTQTkUJrj^P!O5ec$6O+Af6^?L`A4x{KLu{q1(zt zHZyK+{ErARNAwgG={#3%5N3Y#0x}nX)yEZ@3 zv^IHP502dWNzknX5$(-NjNd$xo+^PZM%(@kXf|SzMn3|+_YrKoB#8<5x@Gah?e9}Q zHGN1Ah%>t5^cpymtj9`nkr4JWWt2!2S+$6PT97_K^M-JD(@kAE{fLN|05idRK}w4i zh2opLtJ6|tIezbpV6K>VXs_j!&GQ$n-0XAI>#Ou}YrWBNz~IowuY`ayK5@6wP4$t1 zw?NFuK(r#J=(1<8p?K9~U3n3@bFyI34F$RNi=Vv~8zL+L;07=^9`Wz=%ko^s)qrh) zhG}1OmCkpxWBBT6`w2qqDWGD=`g1dW=jZ!CM6+4y%SEMoqX*gbW_h&e0mE(e3rLYK zUAghFN95vOQ0UA|`*w65y}#+ziGv2!MHg20?HvHe2j4U=$cCo&cCN-wRF)&7qhNKJ z__x2xc0XvsbZiHYrkN+@fl6hVNf$uI}0A$3)$QkX({&LfhNhOAaA%X{)*ihn`)m(A15{ z@i2tcRM*^`6G1O)C`cvg1cM-LBOoYV`H9}*Ebg=0)L2|vZao%@9A;`duS7NCv!h8P zs}G)I|4t8UDhl8Y6brTFrNiif;}m}%*vaZAx+BvLo{bQl)V1EiR_4od!~TGDiV@-z zgmyfQ{Oeddaiy|SZM?ri9i!?Z74f+RE_L38P6 z5;F~?=de3JLhfptA>y+7(mE1rh`o-I+i!1;uu2m56LaY>73OU(Zf{3`^QxD z25YT@F@87^i-LUs1_1gK9t1IxKiSzdI;6X;>RQxt)$;r<+%nsaKIWEK17sL9e zfl~}~EFF(Od1z1NK2m3l91bP=7bxS%01oNj`~VdR=!vr-13Q#k{o*5&nJ#Gj3Dm!F z<65jN@IOqqjjjN7+nr+;gXY?Q^t?Os{#!B2uGG54rKf)%08=DU`()s=>{wBaJO5w6 z7!*Z%T?pGu#Mu!Fv64Cs6uhIx;p&C^_^3Px5?ZNAwNH6Z8wxQ%m&_;vUs^ z)8fx&fZ|6N&F5H7dCi41V{tv8E3iBKn_sE=knPo7?M^d8CuSW^{J93)lw|e{wd$!L+(N}D-a>mr50`eyNG)?Ty!&muRkE47U>i#!wCtI zcfe4uS)CS71bPp83Vws~9^+1a@`X`9;|n8h#%bh-;%dE#A3NdrA+rLivE=Pj?e!yW zzj%q;GyolRk`i;=!oD{ylqabh|i6A3sDZ(FgcpG+Bq` zo=|to(D>kFDPwVVA`~Q3EU+U&ppNWt*AaC&A+fpAN_u7;u_h6#O{rTSCsh1^PKmbQ zSd}`xM4=iUU*x7;7(S8%BNb&0=|$R0`njjst+B5Kd!w98|aDHe2 z)G<1|(Bub>DO86ju8w1~4Lwcc*^FFSIeP0-k=qA=fk2>7Q8*!(9|`hK%R|;jRilbu zttEAj=4;a0oZCR~_Tu+OLc#9GTwGG4qB&a*8U6W#U1pF^?Dz!NDSLvxKv`L*r!BPzcs+63^#97V6 zffHYrgShvMuESXIGe9BI3`&gdp%2`SsH&rOJe^?F2uTfvKM8fk{qYK6&q5p8d7qay zABJ@n6K|dr4g(G0Y$h8_SXh`Ar``Iihfo)hA|9byxD6@D4uX;ALu7N=m=W=37XKkA zBg2aGUvzVxW$jVk3Nwqj*(s)tP7!-6JoT4fE**20DHLT33rJVrH9m^kh`3XTvIvY;+%iav{*F;EF*OTgKbgkAKOc2PdtSZ%(2VbYJfG^g9#zdhQ`~ za7sPH27zg5#Z@?RDTDHjzr(9<_Mba*Mg+ALFbejJva#h%OAGvMIx2APb*E>s=L=lK z5VJ4?5C*nJn`FQF{qEl5PzXq{g-Lb1OgVh$_~Y!fi4FjopqSCXt!q}>aY^GrT-i-; zi5J`>awBouj)t|iwz7$4923i&gz4uF<%gc<+01-K`&$%6tGfX_WuQvWhY>F(Ju%nmPG+jL=Ya z!Qe_bW(pSL_tjqUjC&>a4tl2yK*R{7zOv^y3DTmo{6TJcK#Y_h*siO?WZExoCfHU; zlh36X)pP_y5p@AbF;GNYT{oW|c@4i*SYCcT%*yLpwphMcve`}3^S8>Ojh8e{tqp}W zk8KV<5oAcbKym+ADHmv9sH4k+%&7 z6c`MtnEnwLPH|D@`xxKayB^-p^|2#Z5UA!@ab)OTKGYWy-IyR&Eoy%#~r-`Wh@ybvzw~2GU$+^$o0aWTfF)Mrw7EiTHL$wxv9e0pe$ttBfBmt! z610$jDVT-!_5MsS9di>`-y3T~g#ASC3E6KG&w=T4e64)0B0P(?qXK3xLU~VIDhj4= zc&piHz!Gu^7k`=&j^pM79}PtZNem@kUP_=C>%@;%obc+Fs3eLvnG+&n`EAl_M1IR&a`4%o zAx6u4mNp^+i&bY9J10`1D~F!y(cC z{(Xe{vHr8ddtyo^XW&dEpny)LH&~@VaP=GLcoj}KAn+L|o36F>2g5$In|`=yl#qB7 zfn{k868*%$(+ZQ##o#%G^G90~0wL~NK`lWOWVikF)WtMp zs1|mH7VAn<;PW4h`lz4Ovsp|m1432Zd9|apcV8Th-K>uI2^7)9xG(<0pH&Ma1eG$X zBQ(G^!_3DZpxk((ce7K;;c;Hs_`goI;epRy6ImH%DI(&b=>7{9(+$l)5OLoj3N9_t z&Qv1?0lgze6Sx{*PtZE#0Q=7TChrt3+8dc!V`GncrU)-Z{Xq6Y;BBtpX8IkCYV$k* zMS=Z@y^7gE71}kkMYC^Nqt~eVf0D&7UCl$yUi%|&0wII0JaDh2)0x?pch>=C{R`6r z#;}c0jXd%Wp_*#AwK$V< ziqle+#lC;UE?!Ap#Ly7BVk1-`P(adCKhV4Jym2m%jVa}#>0l$Z*b+K1`LP%LiJpM_tx3}lpmqUKmv%>weN)@!SnVc>lr!n zp{QNB6;+CBR9Mkz{rTfvv{azT5Yrh{+8l*SJ_n;A2#1W8;;PAxuEKlKL&&k*gCoFR zZ4ADCGRhf*HCQ?Zuh$Ug$O0LKMj5(69Qb&WQ8#W7aFzfY{IkbimqNDAhOEX$qC14l z7acsUcs7eZ-}$1pdp+)YjXHY%?rW*#*~`nT2}=1@_EcEElthzSJI*j7@&ri~f0n6l zz~4R~;xiPNx_kFt({W5p2)lL`#RJ*7=uNQ=7)7sG?smjhLs^Ahk<)&&niu3RWZWri zOBBgS&_bv_p(eEGox1aQo-Hr=fDE(7_;jf18vt-IF7hJU1_tkSyu4)c3NTD^KTxB@L4nfd^sLJ95+wW%yH>Y@i~ZsvLg$Bi1)MVE+q64p`b1Kz4s zku|4N;9ix32nr^H(`4s-05hDxuLoSmr0#f?MDc>;rD>mn8T(3LU?7cPd-t|`o>|}$ zh`T|69?^5j7BNli1m6J4i+zLq>81ysT1>NdaK3~qMtn*sIAXSQz61ezQ1|Gd7A9W+ z*B2m$qawXv54PyOHn@KIc9c~CN`w#Rmnzy{#R#+sKqSYD>lW3QeJ9V61o(_;2pQ} zNq)ZoSDW!aB(Z=Zm~fBvyX1zGyF031E)wzG03cf&!ZI;~ooTE2q&7_-eR)fk7O~k! zTDqwnmZdn7q5Y{I9ISfP8u3<+8-fQ?00WRkDr$dvL?4$XmIFaYh)vl1Xu;S%%=X$+ zfmwNo36*Bc4eFt1xNmA87k|Ww`GV@=OEx|Nr=r~}!dOoN0Fj}xBy$BBc1YqS?B6hM&_>9Cn4H5Cg)Kjh zjfr>v!kM=P`^_*}JfpoAEVo&X=_&+b&{055_!P7gFEJx`HnA@ue`z=xH^%MZlZ<(chrhBk#@VM}u^x$ly)Oupkh#b$x@Gl%>AQt_xm zJAsqKgSIVRJ3^~OzN2bif}BqOYt)2zdk!(G z%B!+Wow~03SMHfBIdch%my5bK*Vl1;;KIaKpk08b0xRi`z6R8@!q|BB*xbgptAQXb z?T~Uw`C*iB=w!l+_s|;vJ|#{zg`~2Zi^Kb_D1-<4UJna<4xTvz&|Jwm*Fx9AV&Z8; zw~d3kEb-FeZHCU6x=StP`U;4hI@Y&vH#!{)4E~(rvC?ltgrf(jz6nGK@Phnxe;otu z34{YZI0@cao*!Huxb;g(rEuL`QQykk`x@mHYu^S^0PEFn@r*LlWQiyrqU3zY^H zVsW&llEvn+dBUop&U_9kd25Pmys#exK6Kz2|O$fQ+S@VREhHiO_(;YH_Uru zfvO`0FElxspNt+v;QeZfU*!xvLKo!y0`k&vSPxAUR5g%^T6RsWOuP#8U?a9oyI@?< zkRwL=igaghdpQ*!_cv`88YN7`_)czqREfM|M#-%Dml!IoAhRVm5!5Hp7ol6)0D-bL z)T;KQA*Bj?oQ5Iu;t+27r1$pinu^c~u(c~F47QX!eBqvq>APgj$pZAmlz_wK!r5UY zOaYI&g5{Tv1w^Nh9TR}$TQW8Q2e`j={?;H9 z7KABlSbYTQ3K!mn`!WA0gfRq?I32VC7-VV9&TjhdaTadtpR==jfJ~zmmF%;4lpVcj zD)j#5+IC;DpePuuLw#1|*BCZkp1hNtsW=g8x2)S(YZagJ1RvzX`2hjzJ-i83XD7@l z4ZK698E?s*xY%*f2*&b!4uO81k`XV`Y1B_rS9a}rbs|50hQI9NUzyl_WvH;oio*vl zb>Do79;h>6`3nmRZ72T3Q7Ug?PQPQCBZJ{Imc@%@gYzVjl!87h`T$3hGy|DwMV&y$ zDsIu*{KOTNJMdhzPBy*&XB15P<`ESC?6~#R>g~l}AF=md@pat? z9I*;0i94ijFG;ycPG@CefmUdJ|)y&b4myo>9FUDSw5KD*6vlHSf+cwLvRE zlqZCAQ26D6!^E~B1j;UKu0Ryx#6Li=BA``d7{eKe&Hh)TvfB97#ptcOcGIsj)Kr6O zTB>Sy)8&T5WGk)Eg`GC-Z7(OVngpO90m>7Ax+iTqxl^xpDCTd$3R9tI6M*Ky(6gK za4kfv*cj$BIi2jWeB;5X?#)n{Mq4;Hwzn7b_y=@0ogXjx{p%OQO}+^fRjwaiP5u1Y ztLpfYpN!Oj4PG)Ow?QA_Vb@~_Z@wmMMQ!FO*r~>w%A1FPXmK1Nu&yVzTgO>gm<}myR&y! z^xhmg5yl!D?rg&xkE=ohabr0dMB1)*@^!Y?LrZ~hJR(~06V3$Uvut;>aT&nUBn}M- z`N)V?6Ql=Vju;(op+<1sl!gf1_$>pu$E}Hi%Eu5~9s2aC>dIBC5?r|*ZhVZR%?TJr z!AcU%A*4(^lWoanv07Mg)$fxuAK*BW7uOUV9aH9TdJ39Jc7-bJE+Y2uS>ZdxFeiF< zjLxL*lOL}nJq!FYHXt8}DXsZ84kA*I{GUm+uP*j%f3b?C&2ZYhU&Ywz((k{pTm{~< z7}#Dk6)HvlijgYYeHR|BlNA-cv6X{ZAPuqtz{LT6z=j|qL7dOT)d7h;&H#E8pW+Ti zG)p#lX^~%#ZK&xK zjNJuH8=4(ra3YM2P@m*`Znt~$FV?w06Y&`j_Y7<`ks~+(3l2VNtR~I zigVp*-ngem^0w)7U({8UAKd%o2#edeMfonMOpGb~fKmYA<)!gUgI(M`si@$^yY;ii zKbd}-0~2X2{Kd^%F)CaN9ot5z=u8kb-N7iW*yHNbzaTh!a*af&I)N_6s$;^(O4*xu zQc#>g9kqhOuqN@t&y09`U&h_KGpOwLZsmun&3C5zJ>DMA_}n|ePPf0^@*Qg*JH(z> z5$h!^D5#l7o0^bt6}JSbV91m%A{V7>gEDWL-OSxDH7XOY%brxrKaP zJO8*^cZRGytRhGYf?skO3L5sD+jn^eH}8cDDv=z<8=Jh9 z6IVnXov-p(3WNbyA?X0 zMYI@X83B!p7>GTh{XoL<5S#VeL@3ks_4KO2WVrpL^RZcX3gHF?CD@k)iy?=<^t)tB zBgZ9NE9lMP#n>FTGe*VHfZ=Z9()ZpHGF6t#hciK?fjvx!W#772SV52((omp5w>s!u z`qeB}FL|lv*%snQLv4C>r7v1%;;_IaV!OPwNGuxUKSA^Pjh+XjPrg4gApnzWlJ*ns zczjPGDJdoZLtxC~n5|XMjIlz57Pw$>D}2Fzung@1q18dopz^Mx9)d;2k0OpKG)h{j z>X!<=f7*ij=^lE!oJ4s|W=_3qY;^P_2DX#^^xc+wo04i6pE;$e{(wpdV$`||pSxG& zRLVeyiA$Qy&C`AS#^v@XNfkt!Oc=$?4z9Sh5|(0@{QhMDoEqeuJo;t4sOA^B4DIaf zLL<6H62JFY^N>+Fz<4M>99}$mmYhUa0-%c<;%b<}C=Ijcy=?Rz4eW+S#(}EJ%3L^P zhz`!GU=M0sf{bF(2P%mjK`1Vxj&iT@_7Uep4}jUBO(Jv8D@W3wxwa3;)!dyU&SShb z#e`s6r(a38&|gxMu6n4lM;zF z2qsBoh?-}+%F|sGgSbPK5_dp4=|*2Q-5sSBdD`*z<6xb@7tuDIQd`H*Ti)tq$qJhz zC2QrC{3pj^(q6g|%mo&TM?SL;{5RS)N4se~e<2cus3xwY$BLWI5-@fVd zh;kL%r1V~Wbsk$R0%>m%Jh#08TK*gQs`P+nf7f%;qY8_9;cFY)@JOYD=cfEAOlt7F zVI}2TnG@J?MxpEi)Da*IBgGs@Hrf%Rd*Pao_eno9w3_NQ0o_0APq@0cUIcXrM6?Cs zqN(e7Emf)8hnS9Bq=+vI(t?JD_vV(kyU6zZ_ibl={MBcDFdttF z55pHF8QwzXt+;!puLuH%tp}z>+SS!o-_LP_T0%AKc|$+9=_9+Q-LEJ|hX*X`<+}xW zX_bGaY@n?HO(1f)OR0wF&L^C(BZqMYE+H}DQIGfleW11o-pO*YYclg%Y~c&1D_1wJ0IoOnSkXrbTxrvxhAruTMg)Y=s#!yOBaJMbtMBESgbh8RwBAw7$F2a0eKkQ=Kc=Cymv z^w9=T+6@V_Lm<@~MKCmhS}Mt8pZUJM_f4kNcgh5&MK;ZQ$W1m~Yp=bsYnGA8w#nLY zV>Z(Q`q|XXht9J?ChWf`{eLb%(MK{daV6 zrI+XZy?sBZ_#RlwScVt^pr07pi80*)AwPQJpLGZ+2dF+?U%xwWf98fIqVgKo)sZ-XSV^3S$*r8|T%B5#oT znX4RBJyBE~u!l^VcQK+IO)E49*bnFz>eKMak>`vRZV%Cb5wT!L^kL6eaJG`v*C9%S z#v^rz*$xwB1nB3ODU*{u-b zWlTn{Z@DxxX@+By#Y?Za;|99tMf(5;_&EFn4~Nd zu0k26jkNmw{=IvGGN%A?Y+tm}nmJT`^#UX#m5yUjL?%~i-gvmkVcZ2 z#ej~>Ht#cduq#%~A2!4!_L`MbdyCm(1sDPh zgIG^eeY|z-DCN-L8OH1QKLM+ecF-$s&=33QnxU8k00f>N!8^e2$uJKdip?S)E%kGD zL_gdrwOrvi*u3xwiM?{7z&2*v=HC5~6f$1uvP$2KU)9*ZjrGwl?9j%TJ#J5E2!J1l z(V~uhmzBzt79hHKie!96h69n2QreBd80(;YMQ`u~N{zrpQ%o(l$0$u>YB5t2-($R+ z^Yxp(hjQltX4?XgavIcqT%Ed|T-EiN)KZ33XFg!gZYLB8%8-@z^%AIjFB|vZw}SYL zqff*tA}9kL3e*-~Ti2j#a%RKdhpcxZOXa86t1WG;$efbz4|{{HkJN@tsp9ai%6d!o{0n=9tDhafGr(`Ndz}`z zbFFaG2|`CfLVy+Wa`V)8pTf-+8g99jnDfF4-PB#{QqH)wCwT#$fCZ!?vIjsksd#am z7bpQdQB1;vKMZq7YPm($ABHe6BtHIV;-W)v62jJnB+=516 zu&*qO7e+g@jjWV4d~nB`<=n@$(~%Z%zj9r3KC?K8)CCvCXSzRePe_o-75El#Dp)lL z{a(OW7NU$()>XT4JR9$m9v`ak&2P;0x}4$p2={`ik_&>dwwdVyfEOyJ{&hmXe{62Hk~s9iA$#tN zMdh$@UO+3PtK!t=5q};zN)cL0vWdGAR$X)kT&4eMn)b>*pK*306513aqtivtxJ|ri zqQY3?nOf`RJbA$x^JFsj^ygzcBYQ{Hd_;NH7##~0Td`?t+0LI%H^1su?o3PV;JxYG z@$Qp&wZNefWu1ffO^jy_YQJW2^Y|r+82nuu*Z-*Z&!|sL|3KVe(WN}TFRpiL%4uF*!?pkTd{ zc4-30ReEuJXj$y}_=kC1m!e~v$*~7KEmg`qYs|8kdx96X^PqnSs|on${Lp`? zoPDCg$@xJC-p>p!pYv+Eu8s9CLM@iqjVeHmh=u@7ZD?7;W%rWLn1G;g#E2zOY*47o zpEme{B(NF((%@%`q+gP5G=Nufp+do4WmItHmpnI_@j!A#z`uCT=LhEDvwp?$#@j|M zkCZX}L)cw9)BZU^t!?=q{= zKi|3W;Lx1L<`uJh3_w7r#LNyBn5gI=r#NI=j_H;O03S0=XJEm&geicaNnfRD>?^`U zLre0&$|=R4*j9vy-a{F_N7YU{0)pACTg&jd{QxQ0;9&wXfm>|JSl^5zZ_+trw;YH6 zBKepT6cbnER!7S>U%#H0Bz*vHcD}&P=LSzdeEi6U9xbM8+79XJRW+O=pHW9%2;9Md z=H`9l#xfY+ljk{bheVVdv<4T$=NSwM!b(a?^adV`4XNL?Vja`ls4&a+wi?0n@F&g~ zN={|}C{jL*_L|{cbuVEj@6)((4bNV{?3lZT0%8D{C${V5Bgg-h#Vr6y55G67e|7B4 z3TJ0$=;p<6kXjm%jMN&(r^0!aftz|ximF?#qV$MMi9+z<|@B2Ne9`ZAa zevn>($J<&_%cT042Qbi!XcBQl1>AfO^fn*KzZ!=iNh`+8Zm*KzKRA75p~Y)lQUK=Q zf^9-I!_r$X_hL2ChxFJhVdA0XBqjHP3;F@W`Db}2deNYM0c41g0tbHWCCJuV@o!NU zDf(yQm9r%X@GK}Ou*H;{@d7m=hGi`I%)HS!xh}f~2n{!*JsvR8bmR(aPJ2xquttC@ ztsoxc^VqcPyu{Bzx_)HaogI~@+-|8{(;0$c>sE-M#?k{I$Nl-0^+5#6ny9M=k3GxPXkAFv3hMfn}%P|QuD_UMKz!Q#z(KPCP!hVD;lXP@))0yQNFyYulv@O>0cYJG=l z3G&+WP&HoVJ1%(=J3R|BJg0GUQ}02=KEt37AW0d=*uKN(=Sx)sS3#0)*sFUjiPRvY zarT-I)ulrpZQq@yTt!lamBQ-!811PK!?$vnq~dfnTvKNkAW z={t1?Z6GKce1d}JX_w9AcuT{!syBF`Y=yCDj%nJijdJ0R@I=^xU-V{(@GH0*Ky*!D zJ`^5=6H;LTCVI@0A86R?fEOfJt+V8|#9sJ?3l~yfi^CP3a%dD~6wScEoh^O&at3-y zWf@z2QRj8{BiHw?VZ1OZ6MSpiQnJMgHTtJcYF)N(*TG*Kt=KW~wSUkdtN0>n*D$9M z)Lv)jA48+2i?TwoUPVqWVEh~q7jPvmbl-WcW|?UghQesDyCHWOnXJs&FvI{3R+V`^ z^7(W#>Jaig6!>+vt|j3;j;yG^7&@SXidTQQC)@%*3=xgmT3BaGH*k+lCL9b-R%wO| zb=AeSMIT)+yPwr(Nncv(fglXygN*68F`I$A|lM{)vHrr8-qK9Y`Xep zjp>_W>HN!|juin;3Uic-(ANU-g6=Sj#Y#j!CUwEoYPIVgu!X~5W=2j?5E7yCp$-8a zDLLYR(J>2K$-Z|MWPgj$(?~BEIfQ3EgsQ~4U)f3_Rb`-i*W8ZyN%#vL>up*uRXC!Q znv~(H7*d!F;wOIGT|ev9E`KlLDc}v1re-m|&b{L|Cm zaF2Y2dB-21fijl|rvF#!(X1Q+O5*{3xm!H3C?N5}DKtH2b%baWWZ}m}ct0*rgOB_-$CB+y1 z8ID>mJvTSpaU*g^j8x-v#aI4cdX)j9fDffq_W%`P3h)=0`0+G(PM*W4YZ^%0t7z1K zjh*GX{V@|4`824ILZhQy5cNzLfov5{=G|(+Iq}wQje)@}ro2CCOo7fEjU>+36*T%^ zZf8C629#9y8wkfRxS$*iI15gp#|7Mdi%1;^WZL}NlKhcg?{M6WXSiI);9Csys2O;@ z&oRyu>Ym&lWHqs5xF1TbMn0{%gjZlbL}%?(&(IT9NTDE(MY4cjRZCY_#hpeIoYG3% zVhe3w_i7Q}XwAHhFURHPHX*{J4Lhi_cW<1r?&EK1jGi(G8Tqh z@jlw_u*-N*w;T=wqIerx#2)(x2)^2QrWlhuEVbQxpTDo_V*Rr%wGME-QNIrrCJbqw zwAZD7E?uy&J)w@&dr;)y)Q~9>7HEKQ2dLekEv^YjeXp>oyYwcgQL1pbp=++KW z41%AG^{e>Yq#hru61d!`!GIr|9cz@f7rcDW?Gdyb8Sp-&iBRb{nSTGaanQ+wvu*nt znECi6`JtJU)fNdqS)qDz>>Na8(~(z!M)`c=Q`mYjLqO|kT#DcBOexT|H=z$f!)GHV zZX-pWdcE<4^9+H8dB{&8#*V7zet>qaHN zX16EH4}Vt~+$U*bhigs~2%@iTY2L-27iOTwS{Ba3<>GP-QW?@ep>K|Ivw6&GQ|r-! z2`D)%U@=y7-Z6`~!mn-G$QGm|fZrh6XxNdh#*B>=W;r}7Ef>b|pGlGyQ&5AY*0KH1`xILfau&t}lm`$+p^h@|bYlgg=qF5GH>(ntVN)$LYa5Ci8RvNqz z5gUoX7hTb1t-Y^z&qSAIbmBwh;*Z))GjQV1F&X^4EXcOB#6f_&R^J#~!R6Rzf zq1XYt^JT%CQVCU!{N2liHn^`r9Ynh}5Aa1wW#tHHC8z6I11YCC z1Cx`IvLX059J$~oHct2T_tRd1`4*dvO<6Ix^I(Xeae9WaLqPa;*SZj$U3FVy>QC18 zzPq`Dn{4S(6>~7avC@&sW-dMXbm4@4xonUcd*jl;l{=4{{_6Y&X(-|hrt+D#4=_M~FTK|6-WK2;rw*1n3VFg3FkMGZ&@_VO50!V{M^^ z=87uDGrgErJ%?H2MM_iXsLRkQ({3Z0-2hxB5#n-`>dlqR?r#o}br6^P8vIB+v-x-zr}%nS%Lm;_3!KZ7GZYl2Omg)q*3>(eBFbV&^8lm zK!jwD10pqeFUCejUVw0Nx{kSN_M#BK3=AN5rW?4$Ah)x2(-^f7v(5s$X~(hcs> z0`i?%Q3(d5XJf1%looACQ~6JE>`(phVaTWV^&1rQmNC25PsltFOd0x`T6K7;|Q zd8`Dao$4_ixP)plm-AlPVzonMFQ+wkfM}wDf$r-*kNHaGDAcHyO_C-wOIx^9-*<+B z>VJ^BGn76BlDqW}%nsFg5Tjh%eXZhkkuQ2}^5}gm#&(8$3AmsgjFs^^DNj zXRjg*yqOjDd#mz(9H=mU$;@^qG(fPoXAv=e{^Ite@S9lNiq%Ps7iJn`$y-x34yYzc}ORE=nBa{*(>?j$8DEk^nj5S0uRiP0#%ik>mPeEir!aD$t0;%nBLW zWh5*D$r(6izQ!X#_j@CEMn8;#fAbQ+`|`ZL!6w^Id0o`PCJ?g9)lK-X2Z zc)a|kXUU6}a(m=qy!Mr+e||drZX&t)y6r3cxpR(MyPndmh6y0#=-iWcFYpa_&p(wq zQFXYtwg#Eg{#X=Bw``?N^8rd7G@M6CgREz*GdnUSsH&<+Q*tJ1!$j@AI z`QbB5$1dLRoSeY({U_!6!&T;g_>z0ozvqsg=E^;jk!=>|*{`>4lRgC|e#yj$9Mb~) zO@3ZL({~)eXq#X@8_q;jF7mk-Lo{MgTkW<6rw~k4PWXR%LzlN7Z<~sLOV9C>7`_w1 z5b{GQn@s7X7OQ)C@BPgC{W}pG@OI$GJP*-P^_ziY@u}aPkzv&!cYs{v z3p`R-#m);aK9+SY^@4X8n~k}HBg?4~`1HjA_=89o*tP!n6pk;S6xx`zq1e<)# zy4F-!@3AF*))v2;tZMSC2FMNUkdTJQUNngm$}yMQmXL z_t-?t-Eb>y z(T&@IBceNhggH34rz?*WLUg6gi0zyN#|@iT>nyn{-o1O5QM?Yd_WbGzXvdVwv+6%R zU$E#J90wVq8)=^e&SPM6su;?T=}_~cHv)0cDKF~|+P_rDO~lKa0X0~h3ZTU^7=Qsd z26_W~-0>~6JMhG$mhw*?nRDY~J2e46TU?ac_dVms0gI5sQT!%?hS8y+rE&85ITn$R zue+T+D-Na$7_JxGdw(a0G-rH73`UyD7PF1~D%e0Q@8`ZG^Gg~4i|34QX{>H*kG%*@1EfPc{M{p+Qy~CEmyCx`B z!+evP+pJ!4wx9d3S3=^mbxn*iH-s6OC};qf)dd-bTGj$LvOnL-+aMRprQh;nk-p_= zSmgWt7ZDa;=f$A8au}sAb*I(y?d^%E1pP03^>?@vQCNrm zVYVKdZ_%QR^v@GYj#l4{{($L^H%;65d`g*X+qfBg$c>K&mybA-jtC?k{Fq;5F9T)_ zr~`Z?$2CZO1p#4ckGuhvwG1ueA>*&;cvR(V${V@#$HPxc&<;Hel1hR0E1v2xcOR(x z2eJaJeY$8XXh;W~?m^AU<4vZ3Q8_F62KSVBoI{S@k5)6w_s~Q4MfUl-b3iypP=&5S z?&rqv4yOqh5(Hpz7fQ8->z~%M_|i;n65r4ot2DvBQ2)N$G_IuvR$Jz{wr0^PV6C9d zTmF-v*;JZJ3en{BCCC~mnJ;sS+j-=J8pW_H{ay)r-YHGuK` z1%od?tW%%SwVw3U0Imh$0Ex5&?CF^!HxdwvKpn8W$VCEcP=&&ExxFCI_6d2?18c_r zs1ZjpBsv*GMvkN}!&9Qoc~we@W<};SI3d9fg3*5vU4Lg4B=u-iE}_j1NKs|Ly{ZqX zm}`vg0wPgPbSQ90#ZP)4XjALj{Q1)-T3U;r7hAEy{E3gmGuO7q&45wnO`kF2DT04> z+MeAnlwAc9ZP(!2kd?7Pr2P5%!HgdcLWx}8pqT)&if=OTN#>2I&o~&Cj29vrF%yc6 ztJlepo3CFI4~G0`WrQJ>4YhAje>=6$I1f zMP9P0hn1GS>JA%H)*>Haxy>cZEN+I@tysDLo`$j1eht|dY|4e*d1d+CN~h!CUn?Rm?xQ9ILqt{VLiD?~3+Vx)0hBmZv8ugGQ-v+1 z9335bA_lLIjA6-)7Va`$sI{~6y@GXI3?7rzvn3lR+ULGP zAOpWCo-{4E3Qw>#ds6^!y`KdmEiG;B>jBuBa%6`Wt##W9;wXfi4KC8%oDpLVn9zk{ zOpbIa!~PM;UuF76{X?O%74sOBrmCMC+sDH_w4RirNKMe3#$kb ze=&%@ChL>fH#1$4*;ZRZUANi6)<{oqIJd)R$54%3=~qN)YZ$0f`0R?a!NLy!{pV2t z*x*}&U2ve&ePloeX_yPUi^_(}-@S{(1U{_c4Mqk*m97zo(<0}~TNVi-s%jwMh5}x} z!;8K01H4ttJg);^HoD3CJ(G}oc!~Es%zGzL1^@IN;j4W9gbgN=3dv%^ch? z(DQ9mm{QL}S`8QLN3@*6(R+OXenp&IW%C!UOERS4!Ezk5X;k24nxDTm|8-tIuE=n* zf`o8%NrQ>^>4#LpmY6$nb|7Egr$k}Ga_`=~Mp#vG94*74U$wo);eXD0MkKy5qm3H) zPME{P@72M^AaW!EyJq%vwe~RfI!uK`wQj zcrbb9jGVrVsqD^G8ZRb<C%O9^d&Mu+wU%fzu}*tpzYxSzRp&6BrTDjx=w)7FzFcf9`BAte5@0a5JCo zi4%Esf2LT&UYYHenYr8o^L4O5bP+5=Yc2)3_>^u!L-^q=B*bC>$HXxQk)Vq6K3v2f zikyjui{=LG?9l&=j>-=i*qg@b``!n{pG}yC%t)dK`llEow#a{O

sKux-I`Fu?osG+Jp%t9QQr_))|^^|5z$PpEqC{onBA7dDFD^u7T)(o z91J#aL|UqpcjT=de;Lfe=$fxY2C6ezJ8$JI~3Ys z2-P9)EzbrXOh8HxLE2&iNDjd?6^>Y#(w!+krb!4s^q_JKY->j1vrd^jLhXl z04)Z<1wjue0aF?6!4gynIfrYO&>iW6^Qu7auP}+rU`dir!xltX`j_oUcea@Rv{%I_ z%WClIxt^Id@SyAaOPaReMz6zoX*(DQnsCNIBskD7xUm_xi9gfK;MT6JZ6MKHj*419 z%iX|v5(lk;HdE%$8 z-n}&%Ja?mq8~sa4vuckf>nD<=*{BR)DQpGGtv?j?hjxwM zI2(TUWHV#{82+0o#(QY%cK2sYEUZusRFO>sr3#b`EIT{&+w6LN(zENV&{VVAmGC`> zFijCMv*fD4`tXbPWu8--|AKcR$;-=&k=?^if5%<^IUnyG6KDPzZ@_~94PxLUgJjtV z6JLV|R>q%CGKMdQ0QHy}(mo*pu!eXTqP_tf#NdNr&9~bNQ@4k^Qo{#n903EEYycjgHR?K0|MOV5WO(%H@u<(2ih~G9~udJ;u#!qN0mf+ z>Vs=@u3b`FziGqn*oo$+Pp3d9rUCjR#-XWe5-R^B$b{SeZtpa^{npU)0W8Pl`FAbh zsL|;%S-xj?O2ng@LaKAYNn$2eyfbGF_1304%P!XVI?Js>{31AkGxXS3+Kcq}Yy|gl z17i(1M}BQRx=3JhpnSD-T5PD#U-Q#vNg*&232AA+_0A(1f6l;VM?E?`NlhOUUvFG5=eI&J=F<*WYzJ9cJvB^O0 zA{twwfo8l=h-|jsa4s}N$NT2nBeX^r)8mRGN5cyghs7QT?-`Nb7Pp22a5k+fJEr4rj4Bby6{sSse9!A*NA(Hd3rPklpd82=lbk6{e?VW=ZP zU~#9`pRg#72>mi8OZ0Uu-QAaQvfT$ipO)gE_eQ&`sd(Lnv0q!DwFp~vsm8qZH@HaG z5p5(lcv={%Y#3^pf+`>Qk5_M_S@^l=u=}PrO=+Nb|2DR zqoRZ#N9N@>mjog?9__=h!9OD-qf>0Z+wsfI<_eg6uq70?D57(K2>JuA&xX++W%??N z7f~<-V35yfJq6I7_P*>#&qya)i&1YYkHROpqvGa-#;SyK^7hM~?WygaJp% z5EN;otwZ+TVCbp|BbcghHvIwE^z(;&Zkfjj7THdXtKEmcMhU`L0!JnY)$l)(VEcPn z)g%lN$ZbQ)Fq&X+^s@m%O~CeasH4T%B?ESV4|wWC??sk=f+uB+MUrw2me2R!X~uAP zG18&b?lW5BY4YAn=gGcp9=U@W;2BoV0i>Wg4_8*v(A6=EUmw4#54AoWI8DViL)>C* zZK?G^=uU;;Y$W$<5OZ_%OG(sAwGWmyMu2a}cp*ijqu?S61PCj&aBxPAl8oT>@4plm z(@r15Yik7<`j+@3ad=(>^3neIx`;LVT>jm=qO^ZaeBIZYYzdKs*Wo-XzrAViyp6bo z*T4bdwXOYwl7dNEgTgEZbunS}V zW2hmF06PoHmTS&9|FN+#3@~y*5a?cC05YSQqx}=WB)@cbSB{h*gW$uKE&4PG$J2|U zKfuj+Xgn)s!7R7QM5i8%exE#fBB+;P``!G-#bRlKIhooU+m~vKT@4F07H-6Pc7~bH zG80a<1gFv)3J9#doJ?I~*5yJ=B`~E2PBaAo9yWqAzo2uFdza=K`P~tok)iFEMR+hT z!QL`FREoYEUv)JTkqb_E4DYpah-0ExIYjCif2*H*|Hq~T2NbRFNdWT{6LDnee@X_@ zc;2lBo&o(3;LcY+uf5}T!?6e4FW5Z+TD?+3Hs<=zxBTwx*ng)fP(WcmQM3WfK@NXJ ziQB@s;Cj1VisvAl%{1QFJg1(OR!;5K~nZ znn8=CG>vWR!Xk^Vd+rpmN=G#ofG0p?>D^yGX_OXrVp2*|Hb5VPF$=g;bw$CbISxD- zK&J=@AGAXr_)YZnt@ya)DucSiC(TuEaa42qg4BSQgI2?7{=qF@#9e5*OFZl&Zz|g3 z-8Amd#0^eT@GAT8+zDqL&k;kdD{;HTz7Jivq`e8o=nFAiMbXR29P-;SWgTX{IO}jU zzEr1u13d>V8z#~yXlsHE3jSiyTQLmnFX-+hmvz8+kf`z`u3QUJPAU!2aiZkk@wo2% zbt~+Luk^ZB9vO*GH&6ATu;#w}x zSTg3Ghb^|4<>`ENanSpaeLU9Z^bx`%$ohh%g^f{H*UuP<1)%^yvzfBbvL@HgG?SDB z6E1IclQ56N=*<}~jL8%62zmqo43KLB>~{2;TW7S@|IxkNwL>^Szd7uC_{%}zqA(c@ zl9+5zph>8LRE(JIc&7w@Doi525{zxE$J?~{%sYOD#otB>1cd_v2gQ0zfHkU^-~!*F zN&S^8GeNZyR0{^#ta`0%{NW%tp%c@G@q&c#5ldS69!XFss_^?t^wA89~_`T@39C+{|ZQJ@--P1o)fdlI%Wkjq+_N^E zQ$!jW@*=hapgDgnEoTs4y*m;v3l$5Ui^!Mct81;UKNSoI{u5Ran>*<{n)7F)ZiWAeL}Ry_KzCcf`TL z0HD()G*`5co4ljY0>o$r!p$#KT!-wv5TLn1zq*q5vZh7{$O`Sj1Ta9}rx+g=4>mzW zB1VELkR2e|gZ-kx!B5!yLe*Y z(XD~Gkx(~kd_;L-iCtPP#-XH|v^{uGNKfHc%k(Z#q^>6>dR0&B=*VV!$H`-JsaL7B z7o2i)H^T`|56+N|kH-J*VIE`P5fk{NaCxt3YD?wu3FYBKevfExAW@;wALhsG&OG#e zc7(ww`IBp=_dD=La8ujkaWA=`H-FX5_@ ztp_|0p+C@ZZTvlU*zU4vsUAHr6vI`e3ksS`wOuYG^O~|qCYK+*xF(_=9w%F~wuT0X z=puF5wh}SsY@1CU<4Ba)J+=s1#2a%(2XB?4H~s{|G#-+6%<_&A&c!7FJuj77KgR98 zqBMJz@T`RnsnhVp$ovwGhb6C&&U;Nv*Tr}^q4~p6@dMwNX#waQa@cqM_YaBv2}Q;5 z;Iio8UBBcf~j~@j7V8_|<@6-JAidGb}U#v*j zcsOm)f7l9ffFOmg2uXAZ$1dimq!!rT46|{3U>?k1=3=cm29_u& zpIEP?8Vp{zwrJyA$A<2%%K7X~yl5H=fUh9sqQ6BccVe8LxZ_3soIr zVa!VgZ9`~fe}9d#!fS*@RqSV#9ZDfhI`{)ZGGTL5E6f77unWJ^wKZg!f&000#K}PL zkI`HaY5GQF>G;;k?O8iOu0}fm|GH$Vl95s`z)C;_PXTotmD+QCkcQ=dZP8QGSt2@- z5&F_p-x($${+i}E3mEhPV153MZ#dTlrQeo|GfQDR1d7s=cB87f55|N=C%4g^m*zMK z`)F425VnM3a=r+QH!!2dvIt4kfSb{GiWdMEoc=$O?5`l^%&S(Zoc8ax+?BN@J2j0} z{1d+_Pib%2c%yuH+2L@t+R^cUyKAxF%jkqrgbH4-8e|P%EI!SVvDEU`#+M?$F=TJd zXsUic-Z;bU9a)c)e>;X&EnmK%1JnZL^gMJjD=TlZPo=TIQ|CSosv$kh57!lh#{_Si zq`sKVXrJW7{pB)dC9GpQ6X;aVw|iRRY-6(pJV#JNF|qoyH$T~utG!e`&lBA@|F@ZG z1!v(bKt#S|03g7m8z#GZ?FyjBc=aArmt4Cc_!){QD~B}*<-Ox##+oO`m%o+Vui+13Eki6ExuTckU9bW*n zuJGA8F|X@9vD;Ch1ty7gI~dn3J2)Q+v#739AU5iJ#nlb>Y9P~DK zHB9=_wNo8J<}K{-8^+KCv4xPr(7zjih`b9>z+8T}zBj!n zx{x$x3Mu)jrF9FQgv~R$v%^>G0RXo{r_Fc)O$4oE&OZ;jQ;S)sQZK0sf7ol9g}$Ed z1vchU#g(-?-{0xQu-F?68Gq|AkI#b0QQyJ2K}mOsq~^hy9+TsztK!$k<{LohMhi+s!IZTB z5X?mdMxT|0nvES(-mjdF#T0OA14mu6xyw}d(y_MD@ zV69Ao9&3D|&>RCV^wwB8p@ludadB~mO4FtfH{ORm0wi|R7*az4(*|3Q8*m6i+oulc zL`c!CSFFiEoDlV>bAUAJwPaT&5YzvLNeYa}Q`v&SZt zr(7(_$fgw}4~4BZulyIxK!L&!%Szn!mCh0Q*Oo7UDLA=yppbs%5^SH^h1vUIrFy@k zC#2yXwx6|+PB(cG@3^)+}u zvyEAI!Sw=bieM$oUs0|@^GN${0bR_+B%&j|HTbs5PaWfnF)0f$&f(col8M27pGvxySeWD!Iam}L@+i{^)Dkl>|D@-;M}QspMMdcXAaRDDze6Gm$l zUd2-M2^#1I3B>}c32mH^S}rZ^McsT}f;QeDfLyxVX4aj*Mg-d+*!H}rfuW8&eq0Go zdiE!r-1k&g>bm)k_8%QU-m(B4lxZOg$d4~0OeOB$PamV2X~l}5${Q8e`XW_=W2ZO; zQ}hE>xHai#Ec-hk3m&Ky(F?$4g~KSiTU?JHpW{Nklq3w?gdjF}X}3+f`|l4}s-Q%K zCVg{xB$-2~yK(-{X_XE+cB{GlRFo=-qMXB)7#bPL$+rg!%Fv@4XFBf@yMB%Qa?OW} zfZ9C;a{^#RwC^XF5*X%mBl=!KD@r7_4BvwoLIB<7q;89=ylu9=ym$@h#`b>hqk@#o_yskGKVW9I15}n5lP8uNGRH? zZ%h97lm(hC@p|!e4TEybM^6zE9-s*%f2DDAZKwNO7G5`2OcQF_eMM86D?xWtMm+`4 zLc*@}wx_9SxUs(I_?5{Lfynb{$ev@!bpT}M8p-8i%YxjBp8&c%0Nfl;^a&>a*(jV- z($YX%cgG^B9(B;TWyD(x9UBg?WexRU#hhB{)MuZ$Fq#i@++2dXw%_ZJ~=fE>S`Ndy9r`0OpfA!c9svf-CYEY1y@{W22Ai8W6b zCC4|ydVtdeJc+8AGj?pJtbjdcEO43u(c|^~Yv8;x|E$Jb_98g&Ene)z8h`G&VscbV zV@am&=96c!baFOw7flV&{BL1tjUmxg9jowJ_TSf75e>t|g#+g>(advhUP7yZaeK%^ zk#L*T9%2<~%CZYD5&afuSI3*gAAQiV5Gf8M)gh?)&EL#<+q4h&1V8~7l;?CFP@wEI8e4M+1VcuK4Df zH^=Cl&9!aie|PGiYqJ>X$YuWoyet7dHhkC&bxKmc?ryKSIN=GhG%;%|1Xx)~KYny{ zT*LuJrjM?69xK}3_z;~A#L9%bbJVSq$Iu)2TgXUqjLf_Q zA{U4}?Qzl|vIXMf*fd^zwQ$v22ti{R^1dt_o7;%SB&e7_RgRR1Q*D{Q%& zK@Sb!;0*oek047&M@f93G9GCM;j0I|mRV!E3Gw34C)N3-;|OsVxIEkpX<#2}@7c4*1gI0Lj;rGy0Quy1Mni8b ziNAWF`61vJIkAecH)DS&Xfw6&`4HdN1~iG2F)=ZLxcUZ+vPZQqhJVWbD!!+^#&BDQ zw7`Uke@?`t(+Z^x;3xyg*90^)N=t6=5mub6+h@jF8h^pDFnsu7x|;3^fxhPpowI)@ zARHlx$ZBWp$o#W?-7OZtgg9Lk6zv_L+K!znA@Cy#H^0~iTP?95FeiF`_9FNmq zUBcAvx>h~}804>+e1DJ3w>XW7#Y6-py&2Y2ti$^#tG^dL?>y8+@{P%$(OkLGnicGP zI@K%ja2b4NU}I}o@tz$X8fvxi98#DJc@+Qn;lx(_eap-FHFV!FmaxXSNy;S$m57QQ zkF-3`B0)hv^lB|At}tUx$8n}U2Nt?0m+LUAG}+?iJ~=)JT+EBYzTT^cV;{RaJGo(5 zftt$b(yHwtI~vO9tN)mu@O2UE#NImL>EJxBg?DdF$Ce{oV#5s13eEm+g@oMh5HE0WL7$>rLLulIGZ8W{S*hq$FfcFg9w&EwfEbWM z%QUTBh%$D)rm&Fd8>uXLAJwDX&CrEw!{G)JWs4(M1mq^x4Q=@T<;!{8PRz2a9;_+i zgywQH${;X&4zX|USkzKE8%uF0GbTwV0Y#uinLusp^6YoO3OQbn3txk<2)91pQ2y|- zdPww9a6d;)MjP7DA5BH5dX7uOFj(j6sH`<>3^>Um94+96py(;qTdQ8ef>b^RX?_Pu zd7Z|b@LI5~iU@zuShphm4e3mei<*W6v10E%FR*tZ2ouZ`jH*AK*;drE`EuxWX){01h>sTW8@68lggxO;dWM`qSCP;=aD3sw8sF%k?>HP=*b;`(E5F>IwKcWZ zK}Fc);VCEb)C6#9k_cEM4T}=zR`d+67+D=eIb8? zx;C!7Z)1evD2PvrkfD;Ci!jGwScm}mrcDnT>6i=6M1ffb9}&uVDiG;o1~~*dg`gXd zK%D$q@GD@!!w-{~F!`Xv=W*hZcWfNQX~AaE8`jeBLJQmli^V=5$q+{FccQa%<$ZDe zB^+{|X6onD5mOL5NG$8}D6c9Qr$!!=VblY5Z*N5yPs%KRZ*G#<9}c!9#Z)GH)2xp_ zNufeErTFzQnSQs6)>fjABzsux)`BB%dGU%#iB9paLXU5xn9VpnMR_MG{%_fm^07OS zQ^6-9ZWUtsV`f9Eg}-+IL! zLBxr$w0iYBX$Qj~jM5ot`44yU?!0WA02oTN!k`o?)JVN8g`A9rl;XYI0R!dR>5JkM z5)$;j{Hj+P+_SdJ2g+`9JP2-a+Qe%ho$kMv9cwK>OO4;xQo3Wwa z^g}~c`?z;S&JRcuft_T^%3H9f+;n$}h4HDC z*-yxp0bBSDxQZ+tVDB--$Xwzj1^_2)mivog6N_O{RR;vV)0e+G4gdHsX>+g@lkJYj ztH>@7=*BviGO=Yh7j1@g76_y<=v-*GIzkfP@7?XUMs7lAQnNtr+{CS_&-$kt@LAC~ z2gtUG8*~q460ixeHp^#=m7sLhDe8;mUM5~*3KSw0JMX?G6ievE$xc37`qGC&` zJo&`T;bTJ{BE7wU63>h9Wc&>uTc@CGt&e1iE&+D`*HLt)re#`6p{%e6Oa*Hm4%Um- ze7ibM+=C~eT*Ip4O8a!wy>gKcPIR06(rhE@d7@F8|3)3Xd7tx2sleGIp+8NyPp5*i`>+uOP9X;sozi-$pFS7 zk}kM-tT~<%sx8pFkdEK1<%x+{Lx8usBWEM!Kb0uHnnn$g$MRWDv(Z7G8xG~fLBf3uWUVL11H$&jHffIV7iB#6C^m`4fSZ-hZRHTy}@!1P-HM0jf_yRA4J!5?}H!#rRR zn#P)lzFSxSJaGliSZ2$Tae0`>Nxw%2x~BmDz+k##Z%XKDBtw~<1+i%JfXp1;R*y+> zDdySdRwV4X78i#flzn-sAOHSsmqj<^^rEdp{)ETP!DnoB{(g%VP%c1ttHcE+S;u*O z-qf@-rTYul7QU#Iw)EdFGb`rKj=l+)x6H5K2Z$3ls}CJk8^(zYZ)y#Mko{GmqYC^T z9ik%{VAz29HWO2S<(O+A-0nyGBX=28MH?3f=+`^+P4?F~_O(~yBOvYpMo`L8)<5FW zzW7|9OMTMaysjU&c_i-_W1iUx#29ZZNm_}!9u=nTkc7qs)HRMRNbqwIT)u-FmoLmt#*8WzNw|U-iF)q z`um|eJserDc4es#29&;8yEJ}_Nn~2GfT(n%pVbapRQY$MZ}cnC$(p8<*`onw3F{Xi%eMl?pc=9-Lj)9iboDy7T9y3XZ;aVmZFHPnWJy74|8`k#ke({j*l8I2G<_(`6&wYl>0GGE+Flg z+%%D_bvFYW33{(7<;OsI)ja-f0%(niXJ}X#MCkB$oeQCyf5D@B863+wcHT1G40~3I z?{4^E;pNMbFw%In_t88IK>F%*mVSY+NBwT_v{2}PV=|4%7C7TXc!6yX zKMsYW{+Qe{CnT8rsK8O{0&6hy@7SvM_-@(7Mcdpf)O5c80hJaAV>)(&wlDMBFw!N?JjOkJ>@kt{LkycGF%Gx{TUAuj&B8*R z-~y)?iC-H8hiT60>C%Cbw7wTnFYx(I_KJIpI+B{Zo5S1fFfzdwB(Su`mc*tqy>rc|EE#xc#{!D5}JhNZ%E?Z>7Z! zE!nxc1d@Tykm~qhHEW;r;zCJZUEvcrgl2}?wmyHnuu+2)BydamCAV)TOX?#o>mhP^ z0qOY?&?A)ibP4K(JJtk&TCZJyv>9lu@zimFWV8eEQTHI6QLV|G^r32AGT3m#6zIM6x7! z)^j@dFSS^zIoGywR`DM_A-^3>fc~?q$nY50O=VCXETp}av{RB9gI0~b4>ay2sJp>i zn~l6!c2i}%(wuH`XRjLZBIM-_zfA!;teUew z5)0J_KQizRxapdse=`_Xhwz#~{eW$VGr*Gv=A4z46&gsUDzvATHgVz^nG77l)i+e} zwvzb$aPLbiD2%{IZ$nqb^o51ipR&>0;?n*={n)C;8&z+8)MyzX^8AJkmSTH(?#8uD zmuy&-YIkTpV8AC}NAY2{0qjdZN6eAkh~E%fRmXzUHv&Bjt9m}&f8EpZYisA;(2`!9 zThs%!(0K$1-*%qTp}V^v&^$(Fq!4yxZGb4Ha-;`FpvT7@8X6rnC?9K95z?C?lKxC| zbol#4_w|p!WnzXaS7Ru`!3-h(rYgO?xue|f6W;-kIEZS*%cnUl#ck~V{9%WTLTidg zqjl#T$=IWJW%cTvqfTk3x7gxx2gSSK5%#5ey zyx;wDlZjX(f~+^L%R;T-4@N5cgB+J4En-WuUKE#W`0%;NV}L|?RE#eJz^GNPDU=kV&X6$(gmlDCVBS|i*c*|RZ>Kw)ZATf z8wBLl{b%}Y7fot#4dW69-1KV_>+X@)26jMTR`?rHm*5Q4b`oNjrMUJLIL2sYd|tC+ za%dEke7p9FkaYsm?d?~Qr^v(l*!&W7@}%w7Gejm&1xmoC)sS# z0n0q~nzNsF{hWEdx&QB9ZMYBMDe-qddzGuNbZ)>>;nw?Yx)big^XGFhw4o+EZhIs% z{7K$wS;b}7f*wjys6EM>F(Tpu_?x5w?Nf^$r>PkH{MmNy?mHLkp@9-3 zvjCJlBlq#qz-JgD{|81r73%;3?gH@13f*@H3NiFnC=86SDPo?^qxvCZeaEp3MYHgc zg8S-Qr|Eq$%`l%!wZx-y4~;GPgI?Ac0Am1`gqPv~0Ql$W#>p!^#$1s;TQCt^bE+p= zGcmU&BKSP&alP{@0}}&h400yl%{)>_8xDx51!se{rc*JFt}bL)45l8`o|cUt`B}Lx zZxJg1G0(uB`>2O94rBZF?yUgkLDml9_op~qnb>)zki4pqNu~cEPv0Gnb=&^`QBe|Q zM1`_fBq}8%dlO0-kwhpe6xk~>A}+JcjFb{CqeRlMsc0ajMX02R67hSV-Ou;;yq@Qe z`{{MZ^|{W^aUREe9mtsL-*wLIp`rRf{m%tx_(?uDiRBxNee;2v--?d})Zk`BJ^qxU z`?7_nXDhw&wVWZQ5~4XrJ%cV9qyZTbumH&5{I6E=oHgz=#du+~$lB;6NKO~fiN%CA zc5^J+E0w}tbI=m8>`AZ{A>2}L?NE->N10GJaglpp3B?MK5YL>f8pX)qTxnKpT_pwF z8q@_VV3z*gO&TcN3$UKYsPsKPex$EcB88oVsh~EqDaiX`wlEhm^ZN@rf9e0<$%Ir0 zqMjsa#P}X)uVcHLepwA2-hO|)es&WjfH3(93Fnz3R_lj?&_O{5MEZXuR1hKqH+z!M zZN#T70UU`@g(v<&Fux_;F?#T8O238Iv~^)yk8m(n1Bq~YBOD5>nBm~gCzIGP(S*K( zWYXq2G{(@zs^NE!`FWT*YS`*tdzEM3p5@3v$8l|&67<>0@^-KxDI?K$?Cl?Zl5!^HQ`&a9$#o&`Irsn5oPc-^%)*Y!o z-qJcva6P2X0MCqZ^!H#;ac^$BxGGOsXDHACq8cg$)2$)9&)046%MdIH45ZU@o`Hho z70p9AueYvQu?DO$uW0|!j1g$gYp{oI4FcRz@J#LJUWk&s3eUa+o*8){DQn=IwmMD9 zzzvy}&H{w}%N2rmxRXr6Ri`b?>k5-23LYM-V z3PK?97a*IR2MR!}xZECds#@oqxd)T`6Br%Ap%930pR(s{adu&4LlWnqub+rhor%f> zD)ct+7eDX|rn`102AAZKRhe63b|bV4RY~k@_asg7ROsvfCT_%Nx9NRSQ#~#n_o%=L z016vcnZfYHm48ApK1tyU^d{LdckV8`EI|K@)G-T@{-L)O`^sW%W|Xl+4xGvf>LN$< zyZ_SyP-rT2ae1rh1(~auad`m!PnNOq=6jQS2XU6zMFex=dnhKLXe0YiDGE~mhFA{9 z2{ksqKZAs%HdxbHf9`dV2cIq(SkXI?t07t1iUtUih8Z35IqR4kSLEfvM3+a&J@()u z4skF6n5;CfnHO%ou{GXYWYrhtzS|!8S{kM`;rK=_L)A{wHsGr~pVyW$Fyj>Fl>CXI zx`VK<=-1I&n86U#_*X;SivrYw^{+E|>bd>s^JSP8@O(mODt>RA5*9k* zBhZSIy8z=G3Kt`mX+cKM@uL>Gk7VJCqiW*tK~uU7V_#h!Hd5%}N;nA@Em375{8?_@ z^-nF{NS}qPHWb%Ca1*^EnPsg$C(-$RfNlX5eGQ~*Wk8Hf*V6DYNEYED>MPi-uk+V{ zy?;Y3(4nt}dW#A{&AB0v=A4K2_CN`fVPL6Jl{#{Gm`$2RH z%gC_K&(CwGG!&vn(qDz@hEtx^sotwmxd0>sS*snXZZ7?X%idu%j0!ZY-B90Dz!YBhbs zOMmUuf6?E+$U_hi$m{KiERx3^{SR#h&`8bftb&#_?dO)1G{M1pch&*wLBHKVgq>~+ z!P$+KbL&tiD6%b%rK&nPn51ig-H@=oQ>i3)_Q36v3{ZGUSkXnmd5OTVL6u>@Z=+U# zr;1sji|KL&4l|RzQlqruy;2j8mB#HM)FY`02)9;WCWx^;Deorpj4aLKT7X;fEF|}f z(Ufs>N8+XouhLH4>|;h$QpnrDq!R%n&fri|`>MLO&h&sfGCM%3L`_r;^7HXY2dRk5 zrq$K^{TvC!WCQpOaR;X52cPBmV<97;C#c6_dut^bYo9K_Ji`Kv3_iznJ^em6pZ982 zGeeUC$F3j7v;|B*|6TcZ{ysa9r9__!2-nneGxw50dYf=7vs_L>@?DeTrK3Io%s`jn zmu12wnFm!dWD$i`ib_Q-Z{t|=BxC-jvKCyIfz|N@6=Yhse|=@(WxEla zV6vBmyWHOYjeQ*Haq%9IoZ0d-zdkIQX(BO!h!D@#-^wlc>GWZDi95d`n*EN2xua1T zqE#hGFLmuMU?xKn?g)cFU!Je;qQgN&hI`^7rWycvcIs5~D0t?SoWuJ;Q5fKfTpvEe z#Yjq^ac7ucOg34NVE_5`idM-!UUn1N#nTdBKtUBqw1W`lN|r9-3RPo5rJ&z=nu{S=p|T+f+cwrVb|1x zR&syJF6nYvZe&;d(-wUK0)mmLxJp>O5vn_T2&K_sksMvH<>_B?whAf;&L|WjFCEmp zyz9ooi8|w_QH{`svr<^HEy}pLOd%(1>_lnXsDb`EgPR0V2gpl>fC7OEtjlHcmsoVu zgNDp4Y8W)(H({XA<>Ht*SGFeQqf{r-%+sd;bZ8iYS|ACuT)n`?PdBKkUoI#qA z=Y(yvQ|_a*{PR0KNp3weunQLecp&VXJ{YI-# z0$`{Hbic+l`?f5iX5>>ovkG~437!wio?M>3$aL>%ru5mtoS<05@*sJ2PylKSVH7|# zocK`>aV?RvrEgM!60M`Z&wcu1XFjeIkk-WZ**I42ekSk?m}(3aun{}G5I4l#B|Mbw zD>{ySs(N^pu~l0CQRudCn*7#7&1lSGJ8Nx}l7-ge0Y+e&kkq;VN>BdFLp(<)(pT=@ z{lwC+zr&24Tb7uIr}YRO62qXO+(|xX4%Zpw!1SXLNB?s(fYCPWX2h+93kpu&2e__? zM`*v+$6NrANU2{fA}DjvKwC^&Y=J5#S~E)<{A-O-g%N-*>-vLqicrs>cTU=hA)09|n_re@q)#=q`n80T}#otrkfc z(%lo@40kky>Bf1A%MHcK-ojCO5C~s#(g!R1EI%A2JT-_>Q=#}7DeD`tdn47!%ja@%$ zZcBO{90}7HKgWKXn;TCsC-lT&=EDJiSe}W(a?JiHqrA6=-uh$gM3DpxRJnt$c}*IT zJlIgvh%U{Y?pswrw~@z^LM%4ZwzJC~Jn1+~%rq25@UKB;nYU;E=8joj)?ic)_`M2& zs%JLQ-yCqfW4^GNN91zlfOBc#^ffe2@OP)?-9~7y>77%qK6sy%Ra6eI$#-}2vYOCCLs2K#f@8~Wt&6oo9moz`8(~}7n$iPd#NgJIw&EE4 z*!A$Cup@Lw_?FS&k_Aw4LhJj+;k&Vo5lMRc%amn*wL!994e2 ztXgpDD74vLe~|}lB$v&2MrFwRFzzENWY_%f#mTw3L*f0#J@?N=wFxKev*JL?QzOF#cq{yk>F#n>Tm8vB4Hs9n2E! z4zAOt*OZA@113>|{s{LUTm>q}5y&Y{12GVDd3;5H4e^ga8VaIBBKuunl7v~_65_eQ zwK8|WEc8>;M*qC0nFr3lmehBzQ%(r0(gXmwlp@=w@D%Yjq-3rEf(YzqF@;8z*juF9 z#LE5*rm_`=umdqbpfLkwiBQPi8aaH`)Ml?wM)}@*msjF?WF^4*((T%?;1wL$jBefh z9b3El)VDV_)@c=SJ4R#|nb?TC1ajW*1{^ zZ(egB@Jw;h@Z^Utzr9@iDiu9*6Ixw?G#20f1l=#`O7ho5S~Z3oT|Q`P*v&=N9njrl zQV(u95pm1}h{inv3@WK|O`kE--@A^jG?FfW^75}g^*7qPUht1R(e?fFxsAYcI>;-z zeA;XJ4ap`T5pvi;gbNb89=8Y%jQCjQ-MH#@I-wx1H)sqj3-JJhY+`4r%pP@3cMh$p!U`jF%=pxJC|pPHOKph7aH|7g1|%Is zOy@D}=oeX00&s(o7<#fOw(?jH8%SVXyiQNCU4Tf5Q0}o3T{U10%Bh(5O#$Y$`u&SDgjK(T5d;WD5&qOj0+uTs6M@q;Pr&C7uc)k-vk33@nD}_mWLW zvE{{GMq#DqhqUYryGkRR9kOhPLS~$__OvyvE?iOf8xU@iPlid3=(^(}cc|T%W&{yMys65lyHCJ+QhYl5#YLla|97%H$2;0&n7K*Z8w9 z*Vc-ygcVI-*^YqUWv#WoXU?1j-a}&hakp=~_-jN3m5czt`}U zq^C8j1?@H2bB6c~B5#Gno-Dg21rO54fa)B!BV{?wWfa_P!r#P}F} z>v?1z^UMPylpTzy@EEMdZu_gfQ{x|)Udp5l>GLgeG_7HI2Nxq^r z>Lz-s%=H+~s05RS7JcuR?E)_)B8 zLb6GeBQbv5HpOj7|lK8(_JNmi^j4R^~X~OdBJ~Ih3B?u(WjaDe=L3 zGly}jbf3weRE#*F0p}Y#l6H|Hh3k6X=h)Ql?UpU=dyyZ8$&W1RmG>|1hzwJo4ae+B z9N-k*e%xLNUEl`%QV{fH`q%!HEk&Di*`5()+=oBf(>(^>4`C>WSioE)sBgTr=g5SF z4*HUo7vt(S<61kIMY-1q3zQ8f5X(0UeB`C>3c z!=TnrCUXeobG=ID&R*+|7UZ?t0CbU{QPNiQM@?C)g$etP2Cxx*a$Jk|v600$SYWgo}pYqALefD-rB z7jEIFvD`cXl33^~Y+gIsZ{o=KYvj84Gk6j*+M)3U+Kc@2Ni#EM2zbxYA*7Xfw4@QM9Pv4V4mXiR7pgF-9pv7h; z9y<31_u5Io!q5Zq;UVbYdN2bm;^FqDT%?Y*bS0X8B?GR6L7GQD?mRbQxhyc3%PN2@XK^7zHQsCKtO@6H7! zy!rE=DB1i!mD@^Id=Lwk2BAnpHl?~Pw>B}sAZHIh9G01 z$z(l8QAs6@^Q|asO6CxEQ4LU#7@cy-*|xvVSR$nvSuSv4AeKZEdNDhRyeX@M9@gt? zjr8jGoIN^P3)N8kN28nNOP+eKN08;eSXhbuH!~~k5|z%6A{iaND#D*3JBPD-++EXE zx%=1UbsZ4jY!~d8DG9cb8tm#P^Z~BI)rE?KZ+-XVIuS$*75)Cs^VvMo(NG?{{vrSt zZhPt^0yrxt!uN&LtHFiG`$dr^VTzr2M|+_R9*xdN>Own&`* zsKKsmw0k~gR7nm%UBtbX9xOeh5&75)?PvksU$UIE(i^lIv4I!(1|sGO?;OOXazY3D zs!f+i9^Ul}EEzu%Ysije^zGCPhZA0vR9ca0d#L-v40R9^BzlJ|cE7UR?EE{NNy3Q$ z+E=S*y2bSL7u-?;*!t3dZ~?pe;AgyqY6dQL?$#}za)Hdgk!JKH;lsYzesJ5e4q$#Q zgCuzs;8De!$K-o9-Tgtes_EEE7$ak!(~?f)b|!X9>p#6>%u6)@hbf4V-uThHS9tc5 z!o#n>#?U)Dy5W}E5crcFpY{Sy$zgypL?8?LIhGj1F+yO_#P;aTT{=N2UB@s1GE&%m zc1P`vPPN^T<0CM_3>4@aEtFZ}0E8ZSJ(=WC12apc0d_Z(A#`DBWy+;HOr{(hUKW!bJAnl~ooKNv4Q|tK#a?eY<k^T1#%ojBvbwE&@=^$h8&duTx z)sRo4U&5tqGk7U`r!zl4KXT>xaFvSO%@z656r_r`Gyil*VTn^`iR|9uxq>8!fbN5> zGUk(edD}HO%e2^eW+z=B`bLk{razfT<6Eb=BWHO1gb!Be6kwjBLU2&Xvb!(=0bKZ) z)qg|(LI}FA*gcGa>>;wcMHFR4ou%%U64NHsybca|gJr07Y?$fKkuMEuN}YAzgmum% zq=BrsFa_h1@6XNH($fJ+edK-ngwHWmeG@7S+CX8sW$mOHuah`*pdu^QY*xE;IOUU) zg0%kkl;fc2-!Z}!e>Sb6Ms8H-FJ~F!imn+f1<0PJgt|8p^ zi#1Cxrx^m&8xZSt@Y^7h5_>69XrZP;_VxKdMyD#579KH-tkjc9=u^*i+FLRtb7vE# z6>zOiYyj^KPDr8r5-~l=();K`LxmzuSi;q@-qml-ER0v>0> zQ{_GMB}?L#oR5l15Skl$yGO{;vX-djP>h8;eY|)rR<682Tr3V~fH07N=rb1yK(j@l zat=|0HAwrbW{a`j5MX2a>Z--f18Z@4KyY~|k^fohBJVnAKzGLo*xdEo)_Z^j+{}7P z;`89MIP$Z}V1&S@cSI|2`ZE`K3UC8|eO1K*I8OrvA?{0(3y@#_-~sv<(vW9bqcmz@ z`Zb0AKKHpS|*0&NaS)Y*&P}gmyd#&P23fk;Dj!`6E|6mtglF zPHh`LDbZ3R;B%zyi?yU;Hfd9EX%bkc^|ZpoyNqZGF`x`b{GyV z&(bK`H$%%=}Oyv~rp_X~{JG8UFC%np_J{YW$ox>OIOu@Z_V z$7*r!=~&zpik^eZkOy!ue!C-27(`@RXfu%JaqQkH*Vttvv%zRlq14c&ReHF4DwRvf6^1tISOCrjvjk{5>V zPt(NUnX}49`JK3w@R#35d{`_n_76z*OA)zEO!{y(J-oWol!Ms5&`fVhY&z6#yzQ^# zNYy73wlesAcMJCHW`pNeB8K?bxkB4iW2>eN{`~#4EYDeSWACw%1$Z_d;x0W2?q5q+ z_am}{7*4+_8px(|&Wc{Z@?3-qli9PhJNA&(mS zl775S95cve>w&g_4beZC8YFD0zwRIipR%$B`5#d&J_blrjqhA9M>UY5&=xMPO%;B0 zyHs(Gx$p<{NUCf-ca({#U6Os9%eg9NAixr>hrwY*&r9~{)6g& zQ-^-RVEiw(1fglO@jOagHFy&(D~`ks*1?O$MClL2KssXnruVz2{csCVA6{gGK!HlH z@{oIgP@R8-GGv^ZQBHmJX^cv{Ap+wB5F=J|%Ih|HMA9i-ek=YC{PLcMevn*jXA3x&CLW%+TeH^xSW(4d~ zRFs3_g=)a;SD>v>dtYAHK0VeX`5udCwbCDpRK}EcCDz))VO@vf$vZ zX#h$flB{EmTdHP5Iivm&NzMp_anmo5sz?3(A*AW;x2(13(J>i z2rzKX%*u+!=IS44XrQ9;KzlFw>N|pHuxWLhpz{V5zq8Pz31Gb+zA~E7*OaKI&g$P@ z=e?c$PnbZGC82Y7)mmoCt0w&L4Jr+C0GZLt~(MBMa^64xp-_X>&Z`tI0~ zE(Zf3aa z_1^CfEFcS9JH>;i`xp5y4+(n}f$sG^4mt3Va1hk6+2LX<8tD}q+%?ok zL{J8vB*gg6F=uCS#wDz4wA`* zmP+;BW7dF98vra1ru3E(u_{<=1O3Sx{FuJUo(;eoj)oKVjHcFuJy*>Zqy6{|br9Hi zIW-IXamwwfMBDJ>3q?wsqCD5b;v-dAHjSJv-cK~+?M*saR}aYR04a849fq(hI6i@pO8E%-=AGY#Kb?b z_zM{TjD}CUh9x@LF-IC*P8FS;`c?_96-J74ICsuE zhFC|jiyJkPyP3C4;rz!P=4e$(RshZRV71#?;7=W&`zn6rLRP+>pwMPqAW7jzl4z~% zQmlrJa+S8(@;kmSB?Wn7)(_6g700M2`S+VDm$~)HBlBm?7{d+58)G(gXXMaZ+1aW- zgJ=+I!8%&@xRxZGcujV!;CT@>oUC}*iEwQ-*YKBsUEqs)7rZc=^I|O%uhDzZm++{@ zKHn=XJ;)LW)i=&J$yf%!!69lsdCVI~N4d^Ba zLGQ8$MaL6LPvNyp(=%KM;^z1M#N}qGTdNBKz$`xe_vtxLEO*tNB5&5pLb5T3%vg`jILU}0O^cr@;TRyqh+9S&|hsSj9tl)y17IpyQ^Q8H~ce3Eh)jEp}twg#A zg7NICJz1}|GkRweYoExT1`hX$t4gBskso*D8?%a@8eGC()h=p~EPxwE9oQrK3-q8U zIxQ&930YZjAhXtKe|hSeDtmDKe>mTkn9qA@cMc(~kU1q9Tb_%$t{7aaH%yT#y!@&M z^RFf#&$yQ2yypu83WK~g(4`V!4#0;(-|JVOtG=7QGHq{`;@^(N3nZuc()JVa7}PaP z*t-4E?<((ksbNyq-YoG~1qo*8Xzhj5hJHycyYVI=En4&_JWkOtt5Fn*2*~mlAF#;^pZA42MI;O#lV5J)@yP z4a+Im@uoHdEmN;b4V~j$N1!yoIhTLNSC(L$#0T3#+jYm|W2EC-SG8t5O@=|GB2+#n zjyjRFLbI6pBX2K)e1kTTYT!8-!0>0@py0CTjsYu)IA#LbU>!1K4o}g!$hp{T0`iQ0 zScmU| z6qbNf@H_5RPMK7=|B8-uK0SI37s?8_Q6|B&Sf$zY7(vrG2oF&{kdMU-oOChH1SuG1 zP{or_Hyj8QQk|lUBX>fLuRflvOXh5>R;>tkKMb8N2tyAv%2AUclh-B@pj=e9Gvu4y|nO_|UF4jUuW#e}{U=?xbYK~O` zoj??kD2qI;+8YcNDi)AMu$n4}px2Dt+s=pv0AGe=qSmL+`%NAIuugzp)E*vhM7GP| zl`Bk4%Mv|L8dtmvk-&{ZhJEoF?9oVfTcWp;hmsfQlo`YZ`C5e_E{PMK0?4+aP~_@F z3vX&!o>{(hpGj$ATGGdfNli#|(ab>5um)tXxnn+S#JS8~q-tqm5*J zwG%UH^CtL~9zvx`*aAWy5FQv0@O+fS@|r#SC68LMDKhWYtLM58kL4OS=|>^gAnpc=oE->?be04m5$eG$z1Ac zHPXw6rBc$ZU5%+&X)An|=?Wf!zF3F-=3J;~f!YXz9bL}-b&iR`zFHW=)~E*DcHpHS z8^VY~noCp&ZO^w(n(rH`hmlBe4$-fY-y4{|EQRbCau+-fVpg}Ps#R`qf-uB9VSwdr z@;WEt7QsMHY-Sb~uPW5;eneT+!UQjKKRiV(+6~k+T0!g%GcEu8v2itSRt>ZtT#Po) zxdXp8CD|(PnfVu3o}!xAo?9nrN*}~m^iL|iMwET4xbQ;{zP?(Y(#o>ni9u$-dn+Lv zC=8-)5NIHgzJ!ac^&neT`RJrgYQ+olm}(w`Q|&s&5S5;Ke=xrnr&FCc#x=$8=eb z%89i@Y=yvQ7gNN+s<@X~POKK-R20xnt*1RDHv4J8X0%s{Peebp=|}g6H6>=&r5%(N z&$=qJeg7suV;Iatc<(_tihg2G;@ssKg^-232}oPwKph}gEK}LHTPv+b;94rdnK1~H zcrJluDEDL$6}oNQkkKIOQ0Oq4;Iq@_((#XAC`ph+AsrspwfKjWOf3VyZxepzLM^U$ z5z8hf0rdsg4QQAm171J3*)BM##$W)rc#RTXct%FtV2K+o8+B$ znxrGckk>2;b7vT4*f6F9>q2LS4%0AIzdd&2t31|r7_NI6(+DwNcUHgkqERw#0uF;g|() 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

6Y==MScw6or*=iv9hkaA9wJ=pJxZ zHI=twK*@pe^8`QXm5FiyWWJhpl) z#q8bi(WYHv3W?nmiTC1o$f%Fr9qgtkNJf`2tRl^R067Y9M1z=I$LHH%7-DqHPW|}t zm{}b{?MJ!e6BWJO|6HAyscET;T_5CU79Mp!rTxeIGbe^4$U* zGvY2th+BJU5R35{K;_P}eGe!%@I8$H)SDvBVV z8vU;Q5A(B!JL2}>u$DQv^e~_Vz)InW7_ccNmLK4^fXCw4cYht1wI9bW4KfUg0h|!T zXKF12Rnq_&dsP_=YsMuAK}mN66@UaH%XZQUaXH23Dox;3^QaU&5Ypo9g%|@L7#m^P zy4;A)3<&**Scd0a&nm$89A!aU9`M;JYGFYX;$J*wIz768`~IDA z001IsVtY{!keU>wdap&5VYr!lYf4RlSpvN4R_AlVdYDv z_TEpc50zcoTS0$#W~8P6HQ(JB;VVja`?|_3QCgsuE<-~d0_q2L0MG8-Uxrf8Us-;( z^YH&4#(kQ!Xg_Uw+2uScVjv{*u%H($I@|uvfcL;jtV|*?g6fiOW3AtcV>Jh7IH~ft zE38cpOQ$&{V3#>)D6B5}Yl41z3qX^yl%g7J z;S7Pf)gH%b^Vv-N>?0~C;-U>W5+f55Jb=u?u!Ihf9s=O3PWWE`Cd#WeiIrFEabjut zB&`E(Q4#hC31jHSSDUcWrHITxS1$aB^cvj%Wkwj=Qf@eg`p)kzJCXXHI3Ki(kb!pl zjr~5@{M`4Yt)V@mU!SZE)KQ^d&b7bx*Z+- z8J-+-#3UAZ@D>;+LJMG(qV;sA_*fNj0mDx)ImvU1uJodRr7+6^7=aBta-!jQ-(2g$ zaKJAHAu#pH&;h@gyxvkffBJiPIPV=CU2obQR)UX63EkwT*tXeA^}1Cq9`I1&R~h(Io|;L9xOGc$JX@QuTB!UexQ?WFO}3}*~xy?AerPq#&LnK!)Hh%=pRDGVqo ziZjoRTIuH)U$K@&EYG)2_AK8$j$3wRcf7Mcx;~c4X!9OdE~ngd&PqZea8XKZ8!0tx zEUG&!Gt+uY|4r-$jI(j9mElx4IoU8|cL@I(17*L;hU3)B6B1~S02(2fmJsTj4OSa+s@eYc(p%+ zmR1oXa|mJx)g2ZR66~+~=Je6*rNfy+C+x=e*C|K0vQ10Mw%yHL97<)Z#E~A)T~!c66xjUCtvJ*_>3(Phc2mU zNRsXZ@7*_lVjvnI#0=;xtXMTY@dg3j9LC)Q9g9`^0&B^rH4qD`6L{D+&mt>s#(e#C zwb3q)m`H$zxRsYzfjif)cvRtK@f;h;(ZF~}((*}EgSzmwin;Y@3Z>ADLLP)Qo+W;RQIYR4DE7fzLd0BYi9NtjhYldOla|JFUCH_`;1C|vnsR^rzHZ4l>ANlBP zdVHf#Ku{#uS{UrZc!6R5S;jg37@#`X7sTM@5Bf_Q3dPDDv|{r%v^X(!G_J^*q3iv4{hiUGoRFUadyN8$UO8vF6ag>RH zsW_VR3%bYSa8BbfnO63{-tJ#`l_PEDlSJqOU z5Xmuw3>ofRdK9F;H?j(Lv5?+gMTuj{

T$q~uxkYmyO7t}Oz|JH$OA&xsB924Vl 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�gG;yiLngLI+ZGjWUJ*A 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Ð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 + + world.convex + convex + 0.7.0-rc3 + + + 4.0.0 + + convex-peer + + Convex Peer + Convex Peer implementation and APIs + https://convex.world + + + + + + + + + + + world.convex + convex-core + ${convex.version} + + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.junit.vintage + junit-vintage-engine + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + + + + org.slf4j + slf4j-jdk14 + ${slf4j.version} + test + + + + diff --git a/convex-peer/src/main/java/convex/api/Applications.java b/convex-peer/src/main/java/convex/api/Applications.java new file mode 100644 index 000000000..4a663b18f --- /dev/null +++ b/convex-peer/src/main/java/convex/api/Applications.java @@ -0,0 +1,37 @@ +package convex.api; + +import java.io.IOException; + +public class Applications { + + /** + * Helper function to launch a different JVM process with the same classpath. + * + * @param c Main class to launch + * @param args Command line args for launched process + * @return Process instance that can be used to observe exit value etc. + * @throws IOException if IO error occurs + */ + public static Process launchApp(Class c, String... args) throws IOException { + // construct path to java executable + String separator = System.getProperty("file.separator"); + String classpath = System.getProperty("java.class.path"); + String path = System.getProperty("java.home") + separator + "bin" + separator + "java"; + + // construct process arguments + String mainClassName=c.getName(); + int nargs=args.length; + String[] pargs=new String[4+nargs]; + pargs[0]=path; + pargs[1]="-cp"; + pargs[2]=classpath; + pargs[3]=mainClassName; + System.arraycopy(args, 0, pargs, 4, nargs); + ProcessBuilder processBuilder = new ProcessBuilder(pargs); + + // Execute process and return Process instance + // Calling code can use Process.wairFor(), exitValue() etc. + Process process = processBuilder.start(); + return process; + } +} diff --git a/convex-peer/src/main/java/convex/api/Convex.java b/convex-peer/src/main/java/convex/api/Convex.java new file mode 100644 index 000000000..f3bdfba95 --- /dev/null +++ b/convex-peer/src/main/java/convex/api/Convex.java @@ -0,0 +1,898 @@ +package convex.api; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.HashSet; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import convex.core.Constants; +import convex.core.ErrorCodes; +import convex.core.Result; +import convex.core.crypto.AKeyPair; +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.Address; +import convex.core.data.Hash; +import convex.core.data.Keywords; +import convex.core.data.Ref; +import convex.core.data.SignedData; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.MissingDataException; +import convex.core.lang.RT; +import convex.core.lang.Reader; +import convex.core.lang.Symbols; +import convex.core.lang.ops.Lookup; +import convex.core.lang.ops.Special; +import convex.core.store.AStore; +import convex.core.store.Stores; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Invoke; +import convex.core.transactions.Transfer; +import convex.core.util.Utils; +import convex.core.State; +import convex.net.Connection; +import convex.net.Message; +import convex.net.ResultConsumer; +import convex.peer.Server; + +/** + * Class representing the client API to the Convex network when connected + * directly using the binary protocol. This can be more efficient than using a + * REST API. + * + * An Object of the type Convex represents a stateful client connection to the + * Convex network that can issue transactions both synchronously and + * asynchronously. This can be used by both peers and JVM-based clients. + * + * "I'm doing a (free) operating system (just a hobby, won't be big and + * professional like gnu)" - Linus Torvalds + */ +@SuppressWarnings("unused") +public class Convex { + + private static final Logger log = LoggerFactory.getLogger(Convex.class.getName()); + + private long timeout=Constants.DEFAULT_CLIENT_TIMEOUT; + + /** + * Key pair for this Client + */ + protected AKeyPair keyPair; + + /** + * Current address for this Client + */ + protected Address address; + + /** + * Current Connection to a Peer, may be null or a closed connection. + */ + protected Connection connection; + + /** + * Determines if auto-sequencing should be attempted + */ + private boolean autoSequence = true; + + /** + * Sequence number for this client, or null if not yet known + */ + protected Long sequence = null; + + /** + * Map of results awaiting completion. May be pending missing data. + */ + private HashMap> awaiting = new HashMap<>(); + + private final Consumer internalHandler = new ResultConsumer() { + @Override + protected synchronized void handleResult(long id, Result v) { + + if ((v!=null)&&(ErrorCodes.SEQUENCE.equals(v.getErrorCode()))) { + // We probably got a wrong sequence number. Kill the stored value. + sequence=null; + } + + // TODO: maybe extract method? + synchronized (awaiting) { + CompletableFuture cf = awaiting.get(id); + if (cf != null) { + awaiting.remove(id); + cf.complete(v); + log.debug( + "Completed Result received for message ID: {}", id); + } else { + log.warn( + "Ignored Result received for unexpected message ID: {}", id); + } + } + } + + @Override + public void accept(Message m) { + super.accept(m); + + if (delegatedHandler != null) { + try { + delegatedHandler.accept(m); + } catch (Throwable t) { + log.warn("Exception thrown in user-supplied handler function: {}", t); + } + } + } + }; + + private Consumer delegatedHandler = null; + + private Convex(Address address, AKeyPair keyPair) { + this.keyPair = keyPair; + this.address = address; + } + + /** + * Creates an anonymous connection to a Peer, suitable for queries + * @param hostAddress Address of Peer + * @return New Convex client instance + * @throws IOException If IO Error occurs + * @throws TimeoutException If connection attempt times out + */ + public static Convex connect(InetSocketAddress hostAddress) throws IOException, TimeoutException { + return connect(hostAddress,(Address)null, (AKeyPair)null); + } + + /** + * Create a Convex client by connecting to the specified Peer using the given + * key pair + * + * @param peerAddress Address of Peer + * @param address Address of Account to use for Client + * @param keyPair Key pair to use for client transactions + * @return New Convex client instance + * @throws IOException If connection fails + * @throws TimeoutException If connection attempt times out + */ + public static Convex connect(InetSocketAddress peerAddress, Address address, AKeyPair keyPair) throws IOException, TimeoutException { + return Convex.connect(peerAddress, address, keyPair, Stores.current()); + } + + /** + * Create a Convex client by connecting to the specified Peer using the given + * key pair and using a given store + * + * @param peerAddress Address of Peer + * @param address Address of Account to use for Client + * @param keyPair Key pair to use for client transactions + * @param store Store to use for this connection + * @return New Convex client instance + * @throws IOException If connection fails + * @throws TimeoutException If connection attempt times out + */ + public static Convex connect(InetSocketAddress peerAddress, Address address, AKeyPair keyPair, AStore store) throws IOException, TimeoutException { + Convex convex = new Convex(address, keyPair); + convex.connectToPeer(peerAddress, store); + return convex; + } + + /** + * Sets the Address for this connection. This will be used by default for + * subsequent transactions and queries + * + * @param address Address to use + */ + public synchronized void setAddress(Address address) { + if (this.address == address) return; + this.address = address; + // clear sequence, since we don't know the new account sequence number yet + sequence = null; + } + + public synchronized void setAddress(Address addr, AKeyPair kp) { + setAddress(addr); + setKeyPair(kp); + } + + public synchronized void setKeyPair(AKeyPair kp) { + this.keyPair = kp; + } + + /** + * Gets the next sequence number for this Client, which should be used for + * building new signed transactions + * + * @return Sequence number as a Long value greater than zero + */ + private long getIncrementedSequence() { + long next = getSequence() + 1L; + sequence = next; + return next; + } + + public void setNextSequence(long nextSequence) { + this.sequence = nextSequence - 1L; + } + + public void setHandler(Consumer handler) { + this.delegatedHandler = handler; + } + + /** + * Gets the current sequence number for this Client, which is the sequence + * number of the last transaction observed for the current client's Account. + * + * @return Sequence number as a Long value, zero or positive + */ + public long getSequence() { + if (sequence == null) { + try { + Future f = query(Special.forSymbol(Symbols.STAR_SEQUENCE)); + Result r = f.get(); + if (r.isError()) throw new Error("Error querying *sequence*: " + r.getErrorCode() + " " + r.getValue()); + ACell result=r.getValue(); + if (!(result instanceof CVMLong)) throw new Error("*sequence* query did not return Long, got: "+result); + sequence = RT.jvm(result); + } catch (IOException | InterruptedException | ExecutionException e) { + throw new Error("Error trying to get sequence number", e); + } + } + return sequence; + } + + private void connectToPeer(InetSocketAddress peerAddress, AStore store) throws IOException, TimeoutException { + setConnection(Connection.connect(peerAddress, internalHandler, store)); + } + + /** + * Signs a value on behalf of this client. + * + * @param Type of value to sign + * @param value Value to sign + * @return SignedData instance + */ + public SignedData signData(T value) { + return keyPair.signData(value); + } + + /** + * Gets the Internet address of the currently connected remote + * + * @return Remote socket address + */ + public InetSocketAddress getRemoteAddress() { + return connection.getRemoteAddress(); + } + + /** + * Creates a new account with the given public key + * + * @param publicKey Public key to set for the new account + * @return Address of account created + * @throws TimeoutException If attempt times out + * @throws IOException If IO error occurs + */ + public Address createAccountSync(AccountKey publicKey) throws TimeoutException, IOException { + Invoke trans = Invoke.create(address, 0, "(create-account 0x" + publicKey.toHexString() + ")"); + Result r = transactSync(trans); + if (r.isError()) throw new Error("Error creating account: " + r.getErrorCode()+ " "+r.getValue()); + return (Address) r.getValue(); + } + + /** + * Creates a new account with the given public key + * + * @param publicKey Public key to set for the new account + * @return Address of account created + * @throws TimeoutException If attempt times out + * @throws IOException If IO error occurs + */ + public CompletableFuture
createAccount(AccountKey publicKey) throws TimeoutException, IOException { + Invoke trans = Invoke.create(address, 0, "(create-account 0x" + publicKey.toHexString() + ")"); + CompletableFuture fr = transact(trans); + return fr.thenApply(r->r.getValue()); + } + + /** + * Checks if this Convex client instance has an open connection. + * + * @return true if connected, false otherwise + */ + public boolean isConnected() { + Connection c = this.connection; + return (c != null) && (!c.isClosed()); + } + + /** + * Gets the underlying Connection instance for this Client. May be null if not + * connected. + * + * @return Connection instance or null + */ + public Connection getConnection() { + return connection; + } + + /** + * Updates the given transaction to have the next sequence number. + * + * @param t Any transaction, for which the correct next sequence number is + * desired + * @return The updated transaction + */ + private synchronized ATransaction applyNextSequence(ATransaction t) { + if (sequence != null) { + // if already we know the next sequence number to be applied, set it + return t.withSequence(++sequence); + } else { + return t.withSequence(getIncrementedSequence()); + } + } + + /** + * Submits a transaction to the Convex network, returning a future once the + * transaction has been successfully queued. Signs the transaction with the + * currently set key pair + * + * Should be thread safe as long as multiple clients do not attempt to submit + * transactions for the same account concurrently. + * + * @param transaction Transaction to execute + * @return A Future for the result of the transaction + * @throws IOException If the connection is broken, or the send buffer is full + */ + public synchronized CompletableFuture transact(ATransaction transaction) throws IOException { + if (transaction.getAddress() == null) { + transaction = transaction.withAddress(address); + } + if (autoSequence || (transaction.getSequence() <= 0)) { + // apply sequence if using expected address + if (Utils.equals(transaction.getAddress(), address)) { + transaction = applyNextSequence(transaction); + } else { + // ignore?? + } + } + SignedData signed = keyPair.signData(transaction); + return transact(signed); + } + + /** + * Submits a signed transaction to the Convex network, returning a future once + * the transaction has been successfully queued. + * + * @param signed Signed transaction to execute + * @return A Future for the result of the transaction + * @throws IOException If the connection is broken + */ + public synchronized CompletableFuture transact(SignedData signed) throws IOException { + CompletableFuture cf; + long id = -1; + + synchronized (awaiting) { + // loop until request is queued + while (id < 0) { + id = connection.sendTransaction(signed); + if (id<0) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + // Ignore + } + } + } + + // Store future for completion by result message + cf = awaitResult(id); + } + + log.debug("Sent transaction with message ID: {} awaiting count = {}",id,awaiting.size()); + return cf; + } + + /** + * Submits a transfer transaction to the Convex network, returning a future once + * the transaction has been successfully queued. + * + * @param target Destination address for transfer + * @param amount Amount of Convex Coins to transfer + * @return A Future for the result of the transaction + * @throws IOException If the connection is broken, or the send buffer is full + */ + public CompletableFuture transfer(Address target, long amount) throws IOException { + ATransaction trans = Transfer.create(getAddress(), 0, target, amount); + return transact(trans); + } + + /** + * Submits a transfer transaction to the Convex network peer, and waits for + * confirmation of the result + * + * @param target Destination address for transfer + * @param amount Amount of Convex Coins to transfer + * @return Result of the transaction + * @throws IOException If the connection is broken, or the send buffer is + * full + * @throws TimeoutException If the transaction times out + */ + public Result transferSync(Address target, long amount) throws IOException, TimeoutException { + ATransaction trans = Transfer.create(getAddress(), 0, target, amount); + return transactSync(trans); + } + + /** + * Submits a transaction synchronously to the Convex network, returning a Result + * + * @param transaction Transaction to execute + * @return The result of the transaction + * @throws IOException If the connection is broken + * @throws TimeoutException If the attempt to transact with the network is not + * confirmed within a reasonable time + */ + public Result transactSync(SignedData transaction) throws TimeoutException, IOException { + return transactSync(transaction, timeout); + } + + /** + * Submits a transaction synchronously to the Convex network, returning a Result + * + * @param transaction Transaction to execute + * @return The result of the transaction + * @throws IOException If the connection is broken + * @throws TimeoutException If the attempt to transact with the network is not + * confirmed within a reasonable time + */ + public Result transactSync(ATransaction transaction) throws TimeoutException, IOException { + return transactSync(transaction, timeout); + } + + /** + * Submits a signed transaction synchronously to the Convex network, returning a + * Result + * + * @param transaction Transaction to execute + * @param timeout Number of milliseconds for timeout + * @return The result of the transaction + * @throws IOException If the connection is broken + * @throws TimeoutException If the attempt to transact with the network is not + * confirmed by the specified timeout + */ + public Result transactSync(ATransaction transaction, long timeout) throws TimeoutException, IOException { + // sample time at start of transaction attempt + long start = Utils.getTimeMillis(); + + Future cf = transact(transaction); + + // adjust timeout if time elapsed to submit transaction + long now = Utils.getTimeMillis(); + timeout = Math.max(0L, timeout - (now - start)); + try { + Result r = cf.get(timeout, TimeUnit.MILLISECONDS); + return r; + } catch (InterruptedException | ExecutionException e) { + throw new Error("Not possible? Since there is no Thread for the future....", e); + } + } + + /** + * Submits a signed transaction synchronously to the Convex network, returning a + * Result + * + * @param transaction Transaction to execute + * @param timeout Number of milliseconds for timeout + * @return The result of the transaction + * @throws IOException If the connection is broken + * @throws TimeoutException If the attempt to transact with the network is not + * confirmed by the specified timeout + */ + public Result transactSync(SignedData transaction, long timeout) + throws TimeoutException, IOException { + // sample time at start of transaction attempt + long start = Utils.getTimeMillis(); + + Future cf = transact(transaction); + + // adjust timeout if time elapsed to submit transaction + long now = Utils.getTimeMillis(); + timeout = Math.max(0L, timeout - (now - start)); + try { + return cf.get(timeout, TimeUnit.MILLISECONDS); + } catch (InterruptedException | ExecutionException e) { + throw new Error("Not possible? Since there is no Thread for the future....", e); + } + } + + /** + * Submits a query to the Convex network, returning a Future once the query has + * been successfully queued. + * + * @param query Query to execute, as a Form or Op + * @return A Future for the result of the query + * @throws IOException If the connection is broken, or the send buffer is full + */ + public Future query(ACell query) throws IOException { + return query(query, getAddress()); + } + + /** + * Attempts to acquire a complete persistent data structure for the given hash + * from the remote peer. Uses the store configured for the calling thread. + * + * @param hash Hash of value to acquire. + * + * @return Future for the cell being acquired + */ + public Future acquire(Hash hash) { + return acquire(hash, Stores.current()); + } + + /** + * Attempts to acquire a complete persistent data structure for the given hash + * from the remote peer. Uses the store provided as a destination. + * + * @param hash Hash of value to acquire. + * @param store Store to acquire the persistent data to. + * + * @return Future for the Cell being acquired + */ + public Future acquire(Hash hash, AStore store) { + CompletableFuture f = new CompletableFuture(); + new Thread(new Runnable() { + @Override + public void run() { + Stores.setCurrent(store); // use store for calling thread + try { + Ref ref = store.refForHash(hash); + HashSet missingSet = new HashSet<>(); + while (!f.isDone()) { + missingSet.clear(); + + if (ref == null) { + missingSet.add(hash); + } else { + if (ref.getStatus() >= Ref.PERSISTED) { + // we have everything! + f.complete(ref.getValue()); + return; + } + ref.findMissing(missingSet); + } + for (Hash h : missingSet) { + // send missing data requests until we fill pipeline + log.debug( "Request missing data: {}" , h); + boolean sent = connection.sendMissingData(h); + if (!sent) { + log.debug("Send Queue full!"); + break; + } + } + // if too low, can send multiple requests, and then block the peer + Thread.sleep(100); + ref = store.refForHash(hash); + if (ref != null) { + if (ref.getStatus() >= Ref.PERSISTED) { + // we have everything! + f.complete(ref.getValue()); + return; + } + // maybe complete, but not sure + try { + ref = ref.persist(); + f.complete(ref.getValue()); + } catch (MissingDataException e) { + Hash missing = e.getMissingHash(); + log.debug("Still missing: {}", missing); + connection.sendMissingData(missing); + } + } + } + } catch (Throwable t) { + // catch any errors, probably IO? + f.completeExceptionally(t); + } + } + }).start(); + return f; + } + + /** + * Request status using a sync operation. This request will automatically get any missing data with the status request + * + * @param timeoutMillis Milliseconds to wait for request timeout + * @return Status Vector from target Peer + * + * @throws IOException If an IO Error occurs + * @throws InterruptedException If execution is interrupted + * @throws ExecutionException If a concurrent execution failure occurs + * @throws TimeoutException If operation times out + * + */ + @SuppressWarnings("unchecked") + public AVector requestStatusSync(long timeoutMillis) throws IOException, InterruptedException, ExecutionException, TimeoutException { + AVector status = null; + int retryCount = 10; + Future statusFuture=requestStatus(); + while (status == null && retryCount > 0 ) { + try { + status=statusFuture.get(timeoutMillis,TimeUnit.MILLISECONDS).getValue(); + } catch (MissingDataException e) { + status = (AVector) acquire(e.getMissingHash()).get(timeoutMillis,TimeUnit.MILLISECONDS); + } + retryCount -= 1; + } + return status; + } + + /** + * Submits a status request to the Convex network peer, returning a Future once the + * request has been successfully queued. + * + * @return A Future for the result of the requestStatus + * @throws IOException If the connection is broken, or the send buffer is full + */ + public Future requestStatus() throws IOException { + synchronized (awaiting) { + long id = connection.sendStatusRequest(); + if (id < 0) { + throw new IOException("Failed to send status request due to full buffer"); + } + + // TODO: ensure status is fully loaded + // Store future for completion by result message + CompletableFuture cf = awaitResult(id); + + return cf; + } + } + + /** + * Method to await a complete result. Should be called with lock on `awaiting` map + * @param + * @param id + * @return + */ + protected CompletableFuture awaitResult(long id) { + CompletableFuture cf = new CompletableFuture(); + awaiting.put(id,cf); + return cf; + } + + /** + * Request a challenge. This is request is made by any peer that needs to find out + * if another peer can be trusted. + * + * @param data Signed data to send to the peer for the challenge. + * + * @return A Future for the result of the requestChallenge + * + * @throws IOException if the connection fails. + * + */ + public Future requestChallenge(SignedData data) throws IOException { + synchronized (awaiting) { + long id = connection.sendChallenge(data); + if (id < 0) { + // TODO: too fragile? + throw new IOException("Failed to send challenge due to full buffer"); + } + + // Store future for completion by result message + return awaitResult(id); + } + } + + /** + * Submits a query to the Convex network, returning a Future once the query has + * been successfully queued. + * + * @param query Query to execute, as a Form or Op + * @param address Address to use for the query + * @return A Future for the result of the query + * @throws IOException If the connection is broken, or the send buffer is full + */ + public Future query(ACell query, Address address) throws IOException { + synchronized (awaiting) { + long id = connection.sendQuery(query, address); + if (id < 0) { + throw new IOException("Failed to send query due to full buffer"); + } + + return awaitResult(id); + } + } + + /** + * Executes a query synchronously and waits for the Result + * @param query Query to execute. Map be a form or Op + * @return Result of synchronous query + * @throws TimeoutException If the synchronous request timed out + * @throws IOException In case of network error + */ + public Result querySync(ACell query) throws TimeoutException, IOException { + return querySync(query, getAddress()); + } + + /** + * Executes a query synchronously and waits for the Result + * + * @param timeoutMillis Timeout to wait for query result. Will throw + * TimeoutException if not received in this time + * @param query Query to execute, as a Form or Op + * @return Result of query + * @throws TimeoutException If the synchronous request timed out + * @throws IOException In case of network error + */ + public Result querySync(ACell query, long timeoutMillis) throws IOException, TimeoutException { + return querySync(query, getAddress(), timeoutMillis); + } + + /** + * Executes a query synchronously and waits for the Result + * + * @param address Address to use for the query + * @param query Query to execute, as a Form or Op + * @return Result of query + * @throws TimeoutException If the synchronous request timed out + * @throws IOException In case of network error + */ + public Result querySync(ACell query, Address address) throws IOException, TimeoutException { + return querySync(query, address, timeout); + } + + /** + * Executes a query synchronously and waits for the Result + * + * @param timeoutMillis Timeout to wait for query result. Will throw + * TimeoutException if not received in this time + * @param address Address to use for the query + * @param query Query to execute, as a Form or Op + * @return Result of query + * @throws TimeoutException If the synchronous request timed out + * @throws IOException In case of network error + */ + public Result querySync(ACell query, Address address, long timeoutMillis) throws TimeoutException, IOException { + Future cf = query(query, address); + + try { + return cf.get(timeoutMillis, TimeUnit.MILLISECONDS); + } catch (InterruptedException | ExecutionException e) { + throw new Error("Not possible? Since there is no Thread for the future....", e); + } + } + + /** + * Returns the current AcountKey for the client using the API. + * + * @return AcountKey instance + */ + public AccountKey getAccountKey() { + return keyPair.getAccountKey(); + } + + /** + * Returns the current Address for the client using the API. + * + * @return Address instance + */ + public Address getAddress() { + return address; + } + + /** + * Sets the current Connection for this Client + * + * @param conn Connection value to use + */ + private void setConnection(Connection conn) { + if (this.connection == conn) return; + close(); + this.connection = conn; + } + + /** + * Disconnects the client from the network, closing the underlying connection. + */ + public synchronized void close() { + Connection c = this.connection; + if (c != null) { + c.close(); + } + connection = null; + awaiting.clear(); + } + + @Override + public void finalize() { + close(); + } + + /** + * Determines if this Client is configured to automatically generate sequence + * numbers + * + * @return + */ + protected boolean isAutoSequence() { + return autoSequence; + } + + /** + * Configures auto-generation of sequence numbers + * + * @param autoSequence true to enable auto-sequencing, false otherwise + */ + protected void setAutoSequence(boolean autoSequence) { + this.autoSequence = autoSequence; + } + + public Long getBalance(Address address) throws IOException { + try { + Future future = query(Reader.read("(balance " + address.toString() + ")")); + Result result = future.get(timeout, TimeUnit.MILLISECONDS); + if (result.isError()) throw new Error(result.toString()); + CVMLong bal = (CVMLong) result.getValue(); + return bal.longValue(); + } catch (ExecutionException | InterruptedException | TimeoutException ex) { + throw new IOException("Unable to query balance", ex); + } + } + + /** + * Connect to a local Server, using the Peer's address and keypair + * @param server Server to connect to + * @return New Client Connection + * @throws TimeoutException If connection attempt times out + * @throws IOException If IO error occurs + */ + public static Convex connect(Server server) throws IOException, TimeoutException { + return connect(server.getHostAddress(),server.getPeerController(),server.getKeyPair()); + } + + /** + * Wraps a connection as a Convex client instance + * @param c Connection to wrap + * @return New Convex client instance using underlying connection + */ + public static Convex wrap(Connection c) { + Convex convex=new Convex(null,null); + convex.setConnection(c); + return convex; + } + + /** + * Gets the consensus state from the remote Peer + * @return Future for consensus state + * @throws TimeoutException If initial status request times out + */ + public Future acquireState() throws TimeoutException { + try { + Future sF=requestStatus(); + AVector status=sF.get(timeout, TimeUnit.MILLISECONDS).getValue(); + Hash stateHash=RT.ensureHash(status.get(4)); + + if (stateHash==null) throw new Error("Bad status response from Peer"); + return acquire(stateHash); + } catch (InterruptedException|ExecutionException|IOException e) { + throw Utils.sneakyThrow(e); + } + } + + /** + * Close without affecting the connection + */ + public void closeButMaintainConnection() { + this.connection=null; + close(); + } + + + + +} diff --git a/convex-peer/src/main/java/convex/net/Connection.java b/convex-peer/src/main/java/convex/net/Connection.java new file mode 100644 index 000000000..263c0f1a1 --- /dev/null +++ b/convex-peer/src/main/java/convex/net/Connection.java @@ -0,0 +1,770 @@ +package convex.net; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.StandardSocketOptions; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.Channel; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import convex.core.Constants; +import convex.core.Result; +import convex.core.data.ACell; +import convex.core.data.AccountKey; +import convex.core.data.AVector; +import convex.core.data.Address; +import convex.core.data.Format; +import convex.core.data.Hash; +import convex.core.data.IRefFunction; +import convex.core.data.SignedData; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.store.AStore; +import convex.core.store.Stores; +import convex.core.transactions.ATransaction; +import convex.core.util.Counters; +import convex.core.util.Utils; + +/** + *

+ * Class representing a low-level Connection between network participants. + *

+ * + *

+ * Sent messages are sent asynchronously via the shared client selector. + *

+ * + *

+ * Received messages are read by the shared client selector, converted into + * Message instances, and passed to a Consumer for handling. + *

+ * + *

+ * A Connection "owns" the ByteChannel associated with this Peer connection + *

+ */ +@SuppressWarnings("unused") +public class Connection { + + final ByteChannel channel; + + /** + * Counter for IDs of all messages sent from this JVM + */ + private static long idCounter = 0; + + /** + * Store to use for this connection. Required for responding to incoming + * messages. + */ + private final AStore store; + + /** + * If trusted, the Account Key of the remote peer. + */ + private AccountKey trustedPeerKey; + + private static final Logger log = LoggerFactory.getLogger(Connection.class.getName()); + + /** + * Pre-allocated direct buffer for message sending TODO: is one per connection + * OK? Users should synchronise on this briefly while building message. + */ + private final ByteBuffer frameBuf = ByteBuffer.allocateDirect(Format.LIMIT_ENCODING_LENGTH + 20); + + private final MessageReceiver receiver; + private final MessageSender sender; + + private Connection(ByteChannel clientChannel, Consumer receiveAction, AStore store, + AccountKey trustedPeerKey) { + this.channel = clientChannel; + receiver = new MessageReceiver(receiveAction, this); + sender = new MessageSender(clientChannel); + this.store = store; + this.trustedPeerKey = trustedPeerKey; + } + + /** + * Create a PeerConnection using an existing channel. Does not perform any + * connection initialisation: channel should already be connected. + * + * @param channel Byte channel to wrap + * @param receiveAction Consumer to be called when a Message is received + * @param store Store to use when receiving messages. + * @param trustedPeerKey Trusted peer account key if this is a trusted + * connection, if not then null* + * @return New Connection instance + * @throws IOException If IO error occurs + */ + public static Connection create(ByteChannel channel, Consumer receiveAction, AStore store, + AccountKey trustedPeerKey) throws IOException { + return new Connection(channel, receiveAction, store, trustedPeerKey); + } + + /** + * Gets the global message ID counter + * @return Message ID counter for last message sent + */ + public static long getCounter() { + return idCounter; + } + + /** + * Create a PeerConnection by connecting to a remote address + * + * @param hostAddress Internet Address to connect to + * @param receiveAction A callback Consumer to be called for any received + * messages on this connection + * @param store Store to use for this Connection + * @return New Connection instance + * @throws IOException If connection fails because of any IO problem + * @throws TimeoutException If connection cannot be established within an + * acceptable time (~5s) + */ + public static Connection connect(InetSocketAddress hostAddress, Consumer receiveAction, AStore store) + throws IOException, TimeoutException { + return connect(hostAddress, receiveAction, store, null); + } + + /** + * Create a Connection by connecting to a remote address + * + * @param hostAddress Internet Address to connect to + * @param receiveAction A callback Consumer to be called for any received + * messages on this connection + * @param store Store to use for this Connection + * @param trustedPeerKey Trusted peer account key if this is a trusted + * connection, if not then null + * @return New Connection instance + * @throws IOException If connection fails because of any IO problem + * @throws TimeoutException If the connection cannot be established within the + * timeout period + */ + public static Connection connect(InetSocketAddress hostAddress, Consumer receiveAction, AStore store, + AccountKey trustedPeerKey) throws IOException, TimeoutException { + return connect(hostAddress,receiveAction,store,trustedPeerKey,Constants.SOCKET_SEND_BUFFER_SIZE,Constants.SOCKET_RECEIVE_BUFFER_SIZE); + } + + /** + * Create a Connection by connecting to a remote address + * + * @param hostAddress Internet Address to connect to + * @param receiveAction A callback Consumer to be called for any received + * messages on this connection + * @param store Store to use for this Connection + * @param trustedPeerKey Trusted peer account key if this is a trusted + * connection, if not then null + * @param sendBufferSize Size of connection send buffer in bytes + * @param receiveBufferSize Size of connection receive buffer in bytes + * @return New Connection instance + * @throws IOException If connection fails because of any IO problem + * @throws TimeoutException If the connection cannot be established within the + * timeout period + */ + public static Connection connect(InetSocketAddress hostAddress, Consumer receiveAction, AStore store, + AccountKey trustedPeerKey, int sendBufferSize, int receiveBufferSize) throws IOException, TimeoutException { + if (store == null) + throw new Error("Connection requires a store"); + SocketChannel clientChannel = SocketChannel.open(); + clientChannel.configureBlocking(false); + clientChannel.socket().setReceiveBufferSize(receiveBufferSize); + clientChannel.socket().setSendBufferSize(sendBufferSize); + + // TODO: reconsider this + clientChannel.socket().setTcpNoDelay(true); + clientChannel.connect(hostAddress); + + long start = Utils.getCurrentTimestamp(); + while (!clientChannel.finishConnect()) { + long now = Utils.getCurrentTimestamp(); + long elapsed=now-start; + if (elapsed > Constants.DEFAULT_CLIENT_TIMEOUT) + throw new TimeoutException("Couldn't connect"); + try { + Thread.sleep(10+elapsed/5); + } catch (InterruptedException e) { + throw new IOException("Connect interrupted", e); + } + } + + Connection pc = create(clientChannel, receiveAction, store, trustedPeerKey); + pc.startClientListening(); + log.debug("Connect succeeded for host: {}", hostAddress); + return pc; + } + + public long getReceivedCount() { + return receiver.getReceivedCount(); + } + + /** + * Returns the remote SocketAddress associated with this connection, or null if + * not available + * + * @return An InetSocketAddress if associated, otherwise null + */ + public InetSocketAddress getRemoteAddress() { + if (!(channel instanceof SocketChannel)) + return null; + try { + return (InetSocketAddress) ((SocketChannel) channel).getRemoteAddress(); + } catch (Exception e) { + // anything fails, we have no address + return null; + } + } + + /** + * Gets the store associated with this Connection + * @return Store instance + */ + public AStore getStore() { + return store; + } + + /** + * Returns the local SocketAddress associated with this connection, or null if + * not available + * + * @return A SocketAddress if associated, otherwise null + */ + public InetSocketAddress getLocalAddress() { + if (!(channel instanceof SocketChannel)) + return null; + try { + return (InetSocketAddress) ((SocketChannel) channel).getLocalAddress(); + } catch (Exception e) { + // anything fails, we have no address + return null; + } + + } + + /** + * Sends a DATA Message on this connection. + * + * Does not send embedded values. + * + * @param value Any data object, which will be encoded and sent as a single cell + * @return true if buffered successfully, false otherwise (not sent) + * @throws IOException If IO error occurs + */ + public boolean sendData(ACell value) throws IOException { + log.trace("Sending data: {}", value); + ByteBuffer buf = Format.encodedBuffer(value); + return sendBuffer(MessageType.DATA, buf); + } + + /** + * Sends a DATA Message on this connection. + * + * @param value Any data object + * @return true if buffered successfully, false otherwise (not sent) + * @throws IOException If IO error occurs + */ + public boolean sendMissingData(Hash value) throws IOException { + log.trace("Requested missing data for hash {} with store {}", value.toHexString(), Stores.current()); + return sendObject(MessageType.MISSING_DATA, value); + } + + /** + * Sends a QUERY Message on this connection with a null Address + * + * @param form A data object representing the query form + * @return The ID of the message sent, or -1 if send buffer is full. + * @throws IOException If IO error occurs + */ + public long sendQuery(ACell form) throws IOException { + return sendQuery(form, null); + } + + /** + * Sends a QUERY Message on this connection. + * + * @param form A data object representing the query form + * @param address The address with which to run the query, which may be null + * @return The ID of the message sent, or -1 if send buffer is full. + * @throws IOException If IO error occurs + */ + public long sendQuery(ACell form, Address address) throws IOException { + AStore temp = Stores.current(); + try { + long id = ++idCounter; + AVector v = Vectors.of(id, form, address); + boolean sent = sendObject(MessageType.QUERY, v); + return sent ? id : -1; + } finally { + Stores.setCurrent(temp); + } + + } + + /** + * Sends a STATUS Request Message on this connection. + * + * @return The ID of the message sent, or -1 if send buffer is full. + * @throws IOException If IO error occurs + */ + public long sendStatusRequest() throws IOException { + AStore temp = Stores.current(); + try { + long id = ++idCounter; + CVMLong idPayload = CVMLong.create(id); + sendObject(MessageType.STATUS, idPayload); + return id; + } finally { + Stores.setCurrent(temp); + } + } + + /** + * Sends a CHALLENGE Request Message on this connection. + * + * @param challenge Challenge a Vector that has been signed by the sending peer. + * + * @return The ID of the message sent, or -1 if the message cannot be sent. + * + * @throws IOException If IO error occurs + * + */ + public long sendChallenge(SignedData challenge) throws IOException { + AStore temp = Stores.current(); + try { + long id = ++idCounter; + boolean sent = sendObject(MessageType.CHALLENGE, challenge); + return (sent) ? id : -1; + } finally { + Stores.setCurrent(temp); + } + } + + /** + * Sends a RESPONSE Request Message on this connection. + * + * @param response Signed response for the remote peer + * @return The ID of the message sent, or -1 if the message cannot be sent. + * + * @throws IOException If IO error occurs + * + */ + public long sendResponse(SignedData response) throws IOException { + AStore temp = Stores.current(); + try { + long id = ++idCounter; + boolean sent = sendObject(MessageType.RESPONSE, response); + return (sent) ? id : -1; + } finally { + Stores.setCurrent(temp); + } + } + + /** + * Sends a transaction if possible, returning the message ID (greater than zero) + * if successful. + * + * Uses the configured CLIENT_STORE to store the transaction, so that any + * missing data requests from the server can be honoured. + * + * Returns -1 if the message could not be sent because of a full buffer. + * + * @param signed Signed transaction + * @return Message ID of the transaction request, or -1 if send buffer is full. + * @throws IOException In the event of an IO error, e.g. closed connection + */ + public long sendTransaction(SignedData signed) throws IOException { + AStore temp = Stores.current(); + try { + Stores.setCurrent(store); + long id = ++idCounter; + AVector v = Vectors.of(id, signed); + boolean sent = sendObject(MessageType.TRANSACT, v); + return (sent) ? id : -1; + } finally { + Stores.setCurrent(temp); + } + } + + /** + * Sends a RESULT Message on this connection with no error code (i.e. a success) + * + * @param id ID for result message + * @param value Any data object + * @return True if buffered for sending successfully, false otherwise + * @throws IOException If IO error occurs + */ + public boolean sendResult(CVMLong id, ACell value) throws IOException { + return sendResult(id, value, null); + } + + /** + * Sends a RESULT Message on this connection. + * + * @param id ID for result message + * @param value Any data object + * @param errorCode Error code for this result. May be null to indicate success + * @return True if buffered for sending successfully, false otherwise + * @throws IOException In case of IO Error + */ + public boolean sendResult(CVMLong id, ACell value, ACell errorCode) throws IOException { + Result result = Result.create(id, value, errorCode); + return sendObject(MessageType.RESULT, result); + } + + /** + * Sends a RESULT Message on this connection. + * + * @param result Result data structure + * @return true if message queued successfully, false otherwise + * @throws IOException If IO error occurs + */ + public boolean sendResult(Result result) throws IOException { + return sendObject(MessageType.RESULT, result); + } + + private IRefFunction sender() { + return sendAll; + } + + private final IRefFunction sendAll = (r -> { + // TODO: halt conditions to prevent sending the whole universe + ACell o = r.getValue(); + if (o == null) + return r; + + // send children first + o.updateRefs(sender()); + + // only send this value if not embedded + if (!o.isEmbedded()) { + try { + sendData(o); + } catch (IOException e) { + throw Utils.sneakyThrow(e); + } + } + + return r; + }); + + /** + * Sends a message over this connection + * + * @param msg Message to send + * @return true if message buffered successfully, false if failed + * @throws IOException If IO error occurs + */ + public boolean sendMessage(Message msg) throws IOException { + return sendObject(msg.getType(), msg.getPayload()); + } + + /** + * Sends a payload for the given message type. Should be called on the thread + * that responds to missing data messages from the destination. + * + * @param type Type of message + * @param payload Payload value for message + * @return true if message queued successfully, false otherwise + * @throws IOException If IO error occurs + */ + public boolean sendObject(MessageType type, ACell payload) throws IOException { + Counters.sendCount++; + + // Need to ensure message is persisted at least, so we can respond to missing + // data messages using the current thread store + // We pre-send any novelty to the destination + ACell sendVal = payload; + ACell.createPersisted(sendVal, r -> { + try { + ACell data = r.getValue(); + if (data==sendVal) return; // skip sending top payload + if (!Format.isEmbedded(data)) sendData(data); + } catch (IOException e) { + throw Utils.sneakyThrow(e); + } + }); + + ByteBuffer buf = Format.encodedBuffer(sendVal); + if (log.isTraceEnabled()) { + log.trace("Sending message: " + type + " :: " + payload + " to " + getRemoteAddress() + " format: " + + Format.encodedBlob(payload).toHexString()); + } + boolean sent = sendBuffer(type, buf); + return sent; + } + + /** + * Sends a message with the given message type and data buffer. + * + * @param type MessageType value + * @param buf Buffer containing raw wire data for the message + * @return true if message sent, false otherwise + * @throws IOException + */ + private boolean sendBuffer(MessageType type, ByteBuffer buf) throws IOException { + int dataLength = buf.remaining(); + + // Total length field is message code + encoded object length + int messageLength = dataLength + 1; + boolean sent; + int headerLength; + + // synchronize in case we are sending messages from different threads + // This is OK but need to avoid corrupted messages. + synchronized (frameBuf) { + // ensure frameBuf is clear and ready for writing + frameBuf.clear(); + + // write message header + Format.writeMessageLength(frameBuf, messageLength); + frameBuf.put(type.getMessageCode()); + headerLength = frameBuf.position(); + + // now write message + frameBuf.put(buf); + frameBuf.flip(); // ensure frameBuf is ready to write to channel + + sent = sender.bufferMessage(frameBuf); + } + + if (sent) { + if (channel instanceof SocketChannel) { + SocketChannel chan = (SocketChannel) channel; + // register interest in both reads and writes + try { + chan.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, this); + } catch (CancelledKeyException e) { + // ignore. Must have got cancelled elsewhere? + } + // wake up selector + selector.wakeup(); + } + + if (log.isTraceEnabled()) { + log.trace("Sent message " + type + " of length: " + dataLength + " Connection ID: " + + System.identityHashCode(this)); + } + } else { + log.debug("sendBuffer failed with message {} of length: {} Connection ID: {}" + , type, dataLength, System.identityHashCode(this)); + } + return sent; + } + + public synchronized void close() { + if (channel != null) { + try { + channel.close(); + } catch (IOException e) { + // TODO OK to ignore? + } + } + } + + /** + * Checks if this connection is closed (i.e. the underlying channel is closed) + * + * @return true if the channel is closed, false otherwise. + */ + public boolean isClosed() { + return !channel.isOpen(); + } + + /** + * Starts listening for received events with this given peer connection. + * PeerConnection must have a selectable SocketChannel associated + * + * @throws IOException If IO error occurs + */ + private void startClientListening() throws IOException { + SocketChannel chan = (SocketChannel) channel; + chan.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, this); + + // start running selector loop after we register for reading! + ensureSelectorLoop(); + + // seems to be needed to ensure selector sees new connection? + selector.wakeup(); + } + + private static final Selector selector; + + static { + try { + selector = Selector.open(); + } catch (IOException e) { + throw new Error(e); + } + } + + public void wakeUp() { + selector.wakeup(); + } + + private static Thread loopThread; + + private static void ensureSelectorLoop() { + // double checked initialisation + if (loopThread == null) { + synchronized (Connection.class) { + if (loopThread == null) { + loopThread = new Thread(selectorLoop, "PeerConnection NIO client selector loop"); + // make this a daemon thread so it shuts down if everything else exits + loopThread.setDaemon(true); + loopThread.start(); + } + } + } + } + + private static Runnable selectorLoop = new Runnable() { + @Override + public void run() { + + log.debug("Client selector loop started"); + while (true) { + try { + selector.select(1000); + Set keys = selector.selectedKeys(); + Iterator it = keys.iterator(); + while (it.hasNext()) { + final SelectionKey key = it.next(); + it.remove(); // always remove key from selection set + + // log.finest("PeerConnection key received: "+key); + if (!key.isValid()) { + continue; + } + + try { + if (key.isReadable()) { + selectRead(key); + } else if (key.isWritable()) { + selectWrite(key); + } + } catch (ClosedChannelException e) { + // channel was closed, just lose the key? + log.debug("Unexpected ChannelClosedException, cancelling key: {}", e); + key.cancel(); + } catch (IOException e) { + log.debug("Unexpected IOException, cancelling key: {}", e); + key.cancel(); + } catch (CancelledKeyException e) { + log.debug("Cancelled key"); + } + } + } catch (Throwable t) { + log.error("Uncaught error in PeerConnection client selector loop: {}", t); + t.printStackTrace(); + } + } + } + }; + + /** + * Handles channel reads from a SelectionKey for the client listener + * + * SECURITY: Called on Connection Selector Thread + * + * @param key + * @throws IOException + */ + protected static void selectRead(SelectionKey key) throws IOException { + Connection conn = (Connection) key.attachment(); + if (conn == null) + throw new Error("No PeerConnection specified"); + + try { + int n = conn.handleChannelRecieve(); + // log.finest("Received bytes: " + n); + } catch (ClosedChannelException e) { + log.debug("Channel closed from: {}", conn.getRemoteAddress()); + key.cancel(); + } catch (BadFormatException e) { + log.warn("Cancelled connection to Peer: Bad data format from: " + conn.getRemoteAddress() + " " + + e.getMessage()); + key.cancel(); + } + } + + /** + * Handles receipt of bytes from the channel on this Connection. + * + * Will switch the current store to the Connection-specific store if required. + * + * SECURITY: Called on NIO Thread (Server or client Connection) + * + * @return The number of bytes read from channel + * @throws IOException If IO error occurs + * @throws BadFormatException If there is an encoding error + */ + public int handleChannelRecieve() throws IOException, BadFormatException { + AStore tempStore = Stores.current(); + try { + // set the current store for handling incoming messages + Stores.setCurrent(store); + return receiver.receiveFromChannel(channel); + } finally { + Stores.setCurrent(tempStore); + } + } + + /** + * Handles writes to the channel. + * + * SECURITY: Called on Selector Thread + * + * @param key Selection Key + * @throws IOException + */ + static void selectWrite(SelectionKey key) throws IOException { + Connection pc = (Connection) key.attachment(); + boolean allSent = pc.sender.maybeSendBytes(); + + if (allSent) { + // deregister interest in writing + key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE); + } else { + // we want to continue writing + } + } + + /** + * Sends bytes buffered into the underlying channel. + * @return True if all bytes are sent, false otherwise + * @throws IOException If an IO Exception occurs + */ + public boolean flushBytes() throws IOException { + return sender.maybeSendBytes(); + } + + @Override + public String toString() { + return "PeerConnection: " + channel; + } + + public AccountKey getTrustedPeerKey() { + return trustedPeerKey; + } + + public void setTrustedPeerKey(AccountKey value) { + trustedPeerKey = value; + } + + public boolean isTrusted() { + return trustedPeerKey != null; + } +} diff --git a/convex-peer/src/main/java/convex/net/MemoryByteChannel.java b/convex-peer/src/main/java/convex/net/MemoryByteChannel.java new file mode 100644 index 000000000..9176faee8 --- /dev/null +++ b/convex-peer/src/main/java/convex/net/MemoryByteChannel.java @@ -0,0 +1,70 @@ +package convex.net; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; +import java.nio.channels.ClosedChannelException; + +/** + * ByteChannel implementation wrapping a fixed size in-memory buffer + * + * + */ +public class MemoryByteChannel implements ByteChannel { + /** + * ByteBuffer for channel contents. + * Maintained ready for writing + */ + private final ByteBuffer memory; + boolean open=true; + + private MemoryByteChannel(ByteBuffer buf) { + this.memory=buf; + } + + public static MemoryByteChannel create(int length) { + ByteBuffer bb=ByteBuffer.allocate(length); + return new MemoryByteChannel(bb); + } + + @Override + public int read(ByteBuffer dst) throws ClosedChannelException { + if (!open) throw new ClosedChannelException(); + synchronized (memory) { + memory.flip(); // position will be 0, limit is available bytes + int available=memory.remaining(); + int numRead=Math.min(available, dst.remaining()); + memory.limit(numRead); + dst.put(memory); + memory.limit(available); + memory.compact(); + return numRead; + } + } + + @Override + public boolean isOpen() { + return open; + } + + @Override + public void close() throws IOException { + open=false; + } + + @Override + public int write(ByteBuffer src) throws IOException { + if (!open) throw new ClosedChannelException(); + synchronized(memory) { + synchronized(src) { + int numPut=Math.min(memory.remaining(), src.remaining()); + int savedLimit=src.limit(); + src.limit(src.position()+numPut); + memory.put(src); + src.limit(savedLimit); + return numPut; + } + } + } + +} diff --git a/convex-peer/src/main/java/convex/net/Message.java b/convex-peer/src/main/java/convex/net/Message.java new file mode 100644 index 000000000..879602832 --- /dev/null +++ b/convex-peer/src/main/java/convex/net/Message.java @@ -0,0 +1,111 @@ +package convex.net; + +import convex.core.Belief; +import convex.core.Result; +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.SignedData; +import convex.core.data.prim.CVMLong; +import convex.core.util.Utils; + +/** + *

Class representing a message to / from a specific PeerConnection

+ * + *

This class is an immutable data structure, but NOT a representable on-chain + * data structure, as it is part of the peer protocol layer.

+ * + *

Messages may contain a Payload, which can be any Data Object.

+ */ +public class Message { + + private final Connection connection; + private final ACell payload; + private final MessageType type; + + private Message(Connection peerConnection, MessageType type, ACell payload) { + this.connection = peerConnection; + this.type = type; + this.payload = payload; + } + + public static Message create(Connection peerConnection, MessageType type, ACell payload) { + return new Message(peerConnection, type, payload); + } + + public static Message create(Connection peerConnection, ACell o) { + return create(peerConnection, MessageType.DATA, o); + } + + public static Message createData(ACell o) { + return create(null,MessageType.DATA,o); + } + + public static Message createBelief(SignedData sb) { + return create(null,MessageType.BELIEF,sb); + } + + public static Message createChallenge(SignedData challenge) { + return create(null,MessageType.CHALLENGE, challenge); + } + + public static Message createResponse(SignedData response) { + return create(null,MessageType.RESPONSE, response); + } + + public static Message createGoodBye(SignedData peerKey) { + return create(null,MessageType.GOODBYE, peerKey); + } + + public Connection getConnection() { + return connection; + } + + public Message withConnection(Connection peerConnection) { + return new Message(peerConnection, type, payload); + } + + @SuppressWarnings("unchecked") + public T getPayload() { + return (T) payload; + } + + public MessageType getType() { + return type; + } + + public ACell getErrorCode() { + ACell et=((AVector)payload).get(2); + return et; + } + + @Override + public String toString() { + // TODO. Are tags really needed in `.toString`? + return "#message {:type " + getType() + " :payload " + Utils.print(payload) + "}"; + } + + /** + * Gets the message ID for correlation, assuming this message type supports IDs. + * + * @return Message ID, or null if the message type does not use message IDs + */ + public CVMLong getID() { + switch (type) { + // Query and transact use a vector [ID ...] + case QUERY: + case TRANSACT: return (CVMLong) ((AVector)payload).get(0); + + // Result is a special record type + case RESULT: return (CVMLong)((Result)payload).getID(); + + // Status ID is the single value + case STATUS: return (CVMLong)(payload); + + default: return null; + } + } + + + + +} diff --git a/convex-peer/src/main/java/convex/net/MessageReceiver.java b/convex-peer/src/main/java/convex/net/MessageReceiver.java new file mode 100644 index 000000000..ceb907ab1 --- /dev/null +++ b/convex-peer/src/main/java/convex/net/MessageReceiver.java @@ -0,0 +1,174 @@ +package convex.net; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.ReadableByteChannel; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import convex.core.Constants; +import convex.core.data.ABlob; +import convex.core.data.ACell; +import convex.core.data.Blob; +import convex.core.data.Format; +import convex.core.exceptions.BadFormatException; + +/** + * Class responsible for buffered accumulation of messages received over a connection. + * + * ByteBuffers received must be passed in via @receiveFromChannel + * + * Passes any successfully received objects to a specified Consumer, using the same thread on which the + * MessageReceiver was called. + * + *
+ *

"There are only two hard problems in distributed systems: 2. Exactly-once + * delivery 1. Guaranteed order of messages 2. Exactly-once delivery" + *

+ *
- attributed to Mathias Verraes
+ *
+ * + * + */ +public class MessageReceiver { + // Receive buffer must be big enough at least for one max sized message plus message header + public static final int RECEIVE_BUFFER_SIZE = Constants.RECEIVE_BUFFER_SIZE; + + /** + * Buffer for receiving partial messages. Maintained ready for writing. + * + * Maybe use a direct buffer since we are copying from the socket channel? But probably doesn't make any difference. + */ + private ByteBuffer buffer = ByteBuffer.allocate(RECEIVE_BUFFER_SIZE); + + private final Consumer action; + private final Connection connection; + + private long receivedMessageCount = 0; + + private static final Logger log = LoggerFactory.getLogger(MessageReceiver.class.getName()); + + public MessageReceiver(Consumer receiveAction, Connection pc) { + this.action = receiveAction; + this.connection = pc; + } + + public Consumer getAction() { + return action; + } + + /** + * Get the number of messages received in total by this Receiver + * @return Count of messages received + */ + public long getReceivedCount() { + return receivedMessageCount; + } + + /** + * Handles receipt of bytes from a channel. Should be called with a + * ReadableByteChannel containing bytes received. + * + * May be called multiple times during receipt of a single message, i.e. can + * handle partial message receipt. + * + * Will consume enough bytes from channel to handle exactly one message. Bytes + * will be left unconsumed on the channel if more are available. + * + * This hopefully + * creates sufficient backpressure on clients sending a lot of messages. + * + * @param chan Byte channel + * @throws IOException If IO error occurs + * @return The number of bytes read from the channel + * @throws BadFormatException If a bad encoding is received + */ + public synchronized int receiveFromChannel(ReadableByteChannel chan) throws IOException, BadFormatException { + int numRead=0; + + // first read a message length + if (buffer.position()<2) { + buffer.limit(2); + numRead = chan.read(buffer); + + if (numRead < 0) throw new ClosedChannelException(); + + // exit if we don't have at least 2 bytes for message length (may also be a message code) + if (buffer.position()<2) return numRead; + } + + // peek message length at start of buffer. May throw BFE. + int len = Format.peekMessageLength(buffer); + int lengthLength = (len < 64) ? 1 : 2; + + // limit buffer to total message frame size including length + int totalFrameSize=lengthLength + len; + buffer.limit(totalFrameSize); + + // try to read more bytes up to limit of total message size + { + int n=chan.read(buffer); + if (n < 0) throw new ClosedChannelException(); + numRead+=n; + } + + // exit if we are still waiting for more bytes + if (buffer.hasRemaining()) return numRead; + + // Log.debug("Message received with length: "+len); + buffer.flip(); // prepare for read + + // position buffer ready to receive message content (i.e. skip length + // field). We still want to include the message code. + buffer.position(lengthLength); + byte mType=buffer.get(); + MessageType type=MessageType.decode(mType); + + byte[] bs=new byte[len-1]; // message length after type byte + buffer.get(bs); + assert(!buffer.hasRemaining()); // should consume entire buffer! + Blob encoding=Blob.wrap(bs); + + receiveMessage(type, encoding); + + // clear buffer + buffer.clear(); + return numRead; + } + + /** + * Reads exactly one message from the ByteBuffer, checking that the position is + * advanced as expected. Buffer must contain sufficient bytes for given message length. + * + * Expects a message code at the buffer's current position. + * + * Calls the receive action with the message if successfully received. Should be called with + * the correct store for this Connection. + * + * SECURITY: Gets called on NIO server thread + * + * @throws BadFormatException if the message is incorrectly formatted` + */ + private void receiveMessage(MessageType type, ABlob encoding) throws BadFormatException { + + ACell payload = connection.getStore().decode(encoding); + + Message message = Message.create(connection, type, payload); + receivedMessageCount++; + if (action != null) { + try { + log.trace("Message received: {}", message.getType()); + action.accept(message); + } catch (Throwable e) { + log.warn("Exception not handled from: " + connection.getRemoteAddress()); + e.printStackTrace(); + } + } else { + log.warn("Ignored message because no receive action set: " + message); + } + } + +} diff --git a/convex-peer/src/main/java/convex/net/MessageSender.java b/convex-peer/src/main/java/convex/net/MessageSender.java new file mode 100644 index 000000000..d4853f476 --- /dev/null +++ b/convex-peer/src/main/java/convex/net/MessageSender.java @@ -0,0 +1,81 @@ +package convex.net; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import convex.core.Constants; + +/** + * Message sender responsible for moving bytes from a ByteBuffer to a ByteChannel + * + * Must call maybeSendBytes to attempt to flush buffer to channel. + */ +public class MessageSender { + public static final int SEND_BUFFER_SIZE = Constants.SEND_BUFFER_SIZE; + + private final ByteChannel channel; + + /** + * Buffer for send bytes. Retained in a state ready for reading, so we flip on + * initialisation. Must be accessed holding lock on buffer. + */ + private final ByteBuffer buffer = ByteBuffer.allocate(SEND_BUFFER_SIZE).flip(); + + protected static final Logger log = LoggerFactory.getLogger(MessageSender.class.getName()); + + public MessageSender(ByteChannel channel) { + this.channel = channel; + } + + /** + * Buffers a message for sending. + * + * @param messageFrame Source ByteBuffer containing complete message bytes (including length) + * @return True if successfully buffered, false otherwise (insufficient send buffer + * size) + */ + public boolean bufferMessage(ByteBuffer messageFrame) { + synchronized (buffer) { + // compact buffer, ready for writing + buffer.compact(); + + // return false if insufficient space to send + if (buffer.remaining() < messageFrame.remaining()) { + // flip to maintain readiness for writing + buffer.flip(); + return false; + } + buffer.put(messageFrame); + // flip so ready for reading once again + buffer.flip(); + } + return true; + } + + /** + * Try to send bytes on the outbound channel. + * + * @return True if all bytes have been sent, false otherwise. + * @throws IOException If IO error occurs + */ + public boolean maybeSendBytes() throws IOException { + synchronized (buffer) { + if (!buffer.hasRemaining()) return true; + + // write to channel if possible. May write zero or more bytes + channel.write(buffer); + + if (buffer.hasRemaining()) { + log.debug("Send buffer full!"); + return false; + } else { + return true; + } + } + } + +} diff --git a/convex-peer/src/main/java/convex/net/MessageType.java b/convex-peer/src/main/java/convex/net/MessageType.java new file mode 100644 index 000000000..94838122c --- /dev/null +++ b/convex-peer/src/main/java/convex/net/MessageType.java @@ -0,0 +1,143 @@ +package convex.net; + +import convex.core.exceptions.BadFormatException; + +public enum MessageType { + + /** + * A message that requests the remote endpoint to respond with a signed + * response. + * + * The challenge must be signed to authenticate the challenger. + * + * The challenge is sent with a vector of [hash accountKey-of-the-challenged] + */ + CHALLENGE(1), + + /** + * A response to a challenge. The challengee must sign the response as proof of + * possession of the claimed address. + */ + RESPONSE(2), + + /** + * A message relaying data. + * + * Data is presented "as-is", and may be: - the result of a missing data request + * - data sent ahead of another message requiring composite data + */ + DATA(3), + + /** + * A control command to a peer. + * + * Should only be accepted and acted upon when originating from trusted, + * authenticated senders. + */ + COMMAND(4), + + /** + * A request to provide missing data. Peers should not send this message unless + * both: a) they are unable to locate the given data in their local store b) + * They have reason to believe the targeted peer may be able to provide it + * + * Excessive invalid missing data requests may be considered a DoS attack by + * peers. Peers under load may need to ignore missing data requests. + * + * Payload is the missing data hash. + * + * Receiver should respond with a DATA message if the specified data is + * available in their store. + */ + MISSING_DATA(5), + + /** + * A request to perform the specified query and return results. + * + * Payload is: [id form address?] + * + * Receiver may may determine policies regarding whether to accept or reject + * queries, typically receiver will want to authenticate the sender and ensure + * good standing? + */ + QUERY(6), + + /** + * A message requesting a transaction be performed by the receiving peer and + * included in the next available block. + * + * Payload is: [id signed-data] + */ + TRANSACT(7), + + /** + * Message containing the result for a corresponding COMMAND, QUERY or TRANSACT + * message. + * + * Payload is: [id result error-flag] + * + * Where: + * - Result is the result of the request, or the message if an error occurred + * - error-flag is nil if the transaction succeeded, or error code if it failed + */ + RESULT(8), + + /** + * Communication of a latest Belief by a Peer. + * + * Payload is a SignedData + */ + BELIEF(9), + + /** + * Communication of an intention to shutdown. + */ + GOODBYE(10), + + /** + * Request for a peer status update. + * + * Expected Result is a Vector: [signed-belief-hash states-hash initial-state-hash peer-key consensus-state-hash] + */ + STATUS(11); + + private final byte messageCode; + + public byte getMessageCode() { + return messageCode; + } + + public static MessageType decode(int i) throws BadFormatException { + switch (i) { + case 1: + return CHALLENGE; + case 2: + return RESPONSE; + case 3: + return DATA; + case 4: + return COMMAND; + case 5: + return MISSING_DATA; + case 6: + return QUERY; + case 7: + return TRANSACT; + case 8: + return RESULT; + case 9: + return BELIEF; + case 10: + return GOODBYE; + case 11: + return STATUS; + } + throw new BadFormatException("Invalid message code: " + i); + } + + MessageType(int i) { + this.messageCode = (byte) i; + if (i != messageCode) throw new Error("Message format byte out of range: " + i); + } + +} diff --git a/convex-peer/src/main/java/convex/net/NIOServer.java b/convex-peer/src/main/java/convex/net/NIOServer.java new file mode 100644 index 000000000..43e50b0cd --- /dev/null +++ b/convex-peer/src/main/java/convex/net/NIOServer.java @@ -0,0 +1,269 @@ +package convex.net; + +import java.io.Closeable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.StandardSocketOptions; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.BlockingQueue; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import convex.core.Constants; +import convex.core.exceptions.BadFormatException; +import convex.core.store.Stores; +import convex.peer.Server; + +/** + * NIO Server implementation that handles incoming messages on a given port. + * + * Allocates a single thread for the selector. + * + * Incoming messages are associated with a Connection (which is created if required), then placed + * on the receive message queue. This will block if the receive queue is full (thereby applying + * back-pressure to clients) + * + */ +public class NIOServer implements Closeable { + public static final int DEFAULT_PORT = 18888; + + private static final Logger log=LoggerFactory.getLogger(NIOServer.class.getName()); + + private ServerSocketChannel ssc=null; + + private BlockingQueue receiveQueue; + + private Selector selector=null; + + private boolean running=false; + + private final Server server; + + private NIOServer(Server server, BlockingQueue receiveQueue) { + this.server=server; + this.receiveQueue=receiveQueue; + } + + /** + * Creates a new unlaunched NIO server + * @param server Peer Server instance for this NIOServer + * @param receiveQueue Queue for received messages + * @return New NIOServer instance + */ + public static NIOServer create(Server server, BlockingQueue receiveQueue) { + return new NIOServer(server,receiveQueue); + } + + public void launch(Integer port) { + launch(null, port); + } + + public void launch(String host, Integer port) { + if (port==null) port=0; + + try { + ssc=ServerSocketChannel.open(); + + // Set receive buffer size + ssc.socket().setReceiveBufferSize(Constants.SOCKET_SERVER_BUFFER_SIZE); + + host = (host == null)? "0.0.0.0" : host; + InetSocketAddress address=new InetSocketAddress(host, port); + ssc.bind(address); + address=(InetSocketAddress) ssc.getLocalAddress(); + ssc.configureBlocking(false); + port=ssc.socket().getLocalPort(); + + // Register for accept. Do this before selection loop starts and + // before we return from launch! + selector = Selector.open(); + ssc.register(selector, SelectionKey.OP_ACCEPT); + + // set running status now, so that loops don't terminate + running=true; + + Thread selectorThread=new Thread(selectorLoop,"NIO Server selector loop on port: "+port); + selectorThread.setDaemon(true); + selectorThread.start(); + log.info("NIO server started on port {}",port); + } catch (Exception e) { + throw new Error("Can't bind NIOServer to port: "+port,e); + } + } + + + /** + * Runnable class for accepting socket connections and incoming data, one per peer + * If this gets maxed out, rely on backpressure to throttle clients. + */ + private Runnable selectorLoop= new Runnable() { + @Override + public void run() { + // Use the store configured for the owning server. + Stores.setCurrent(server.getStore()); + try { + + while (running) { + selector.select(1000); + + Set keys = selector.selectedKeys(); + Iterator it = keys.iterator(); + while(it.hasNext()) { + SelectionKey key=it.next(); + it.remove(); + + try { + // Just do one op on each key + if (key.isAcceptable()) { + accept(selector); + } else if (key.isReadable()) { + selectRead(key); + } else if (key.isWritable()) { + selectWrite(key); + } + } catch (ClosedChannelException e) { + // channel was closed, just lose the key? + log.debug("Client closed channel"); + key.cancel(); + } catch (IOException e) { + log.warn("Unexpected IOException, canceling key: {}",e); + // e.printStackTrace(); + key.cancel(); + } + } + // keys.clear(); + } + } catch (IOException e) { + log.error("Unexpected IOException, terminating selector loop: {}",e); + // print error and terminate + e.printStackTrace(); + } finally { + try { + // close all client channels + for (SelectionKey key: selector.keys()) { + key.channel().close(); + } + selector.close(); + selector=null; + } catch (IOException e) { + log.error("IOException while closing NIO server"); + e.printStackTrace(); + } finally { + selector=null; + } + + + if (ssc!=null) { + try { + ssc.close(); + } catch (IOException e) { + log.error("IOException while closing NIO socket channel"); + e.printStackTrace(); + } finally { + ssc=null; + } + } + + log.info("Selector loop ended on port: "+getPort()); + } + } + }; + + /** + * Gets the port that this server instance is listening on. + * @return Port number, or 0 if a server socket is not bound. + */ + public int getPort() { + if (ssc==null) return 0; + ServerSocket socket = ssc.socket(); + if (socket==null) return 0; + return socket.getLocalPort(); + } + + protected void selectWrite(SelectionKey key) throws IOException { + // attach a PeerConnection if needed for this client + ensurePeerConnection(key); + + Connection.selectWrite(key); + } + + private Connection ensurePeerConnection(SelectionKey key) throws IOException { + Connection pc=(Connection) key.attachment(); + if (pc!=null) return pc; + SocketChannel sc=(SocketChannel) key.channel(); + assert(!sc.isBlocking()); + pc=createPC(sc,receiveQueue); + key.attach(pc); + return pc; + } + + private Connection createPC(SocketChannel sc, BlockingQueue queue) throws IOException { + return Connection.create(sc,server.getReceiveAction(),server.getStore(),null); + } + + protected void selectRead(SelectionKey key) throws IOException { + + // log.info("Connection read from: "+sc.getRemoteAddress()+" with key:"+key); + Connection conn=ensurePeerConnection(key); + if (conn==null) throw new Error("No PeerConnection specified"); + try { + int n=conn.handleChannelRecieve(); + if (n==0) { + log.debug("No bytes received for key: {}",key); + } + } + catch (ClosedChannelException e) { + log.debug("Channel closed from: {}",conn.getRemoteAddress()); + key.cancel(); + } + catch (BadFormatException e) { + log.warn("Cancelled connection: Bad data format from: {} message: {}",conn.getRemoteAddress(),e.getMessage()); + // TODO: blacklist peer? + key.cancel(); + } + } + + @Override public void finalize() { + close(); + } + + @Override + public void close() { + running=false; + if (selector!=null) { + selector.wakeup(); + } + + } + + private void accept(Selector selector) throws IOException, ClosedChannelException { + SocketChannel socketChannel=ssc.accept(); + if (socketChannel==null) return; // false alarm? Nobody there? + log.debug("New connection accepted: {}", socketChannel); + socketChannel.configureBlocking(false); + + // TODO: Confirm we don't want Nagle? + socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true); + socketChannel.register(selector, SelectionKey.OP_READ); + } + + /** + * Gets the host address for this server (including port), or null if closed + * @return Host address + */ + public InetSocketAddress getHostAddress() { + if (ssc == null) return null; + ServerSocket socket = ssc.socket(); + if (socket == null) return null; + return new InetSocketAddress(socket.getInetAddress(), socket.getLocalPort()); + } + +} diff --git a/convex-peer/src/main/java/convex/net/ResultConsumer.java b/convex-peer/src/main/java/convex/net/ResultConsumer.java new file mode 100644 index 000000000..c49d2b7aa --- /dev/null +++ b/convex-peer/src/main/java/convex/net/ResultConsumer.java @@ -0,0 +1,191 @@ +package convex.net; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import convex.core.Result; +import convex.core.data.ACell; +import convex.core.data.Hash; +import convex.core.data.Ref; +import convex.core.exceptions.MissingDataException; +import convex.core.lang.RT; +import convex.core.store.Stores; +import convex.core.util.Utils; + +/** + * Consumer abstract base class for awaiting results. + * + * Provides basic buffering of: + * - Missing data until all data is available. + */ +public abstract class ResultConsumer implements Consumer { + + private static final Logger log = LoggerFactory.getLogger(ResultConsumer.class.getName()); + + @Override + public void accept(Message m) { + try { + MessageType type = m.getType(); + switch (type) { + case DATA: { + handleDataProvided(m); + break; + } + case MISSING_DATA: { + handleMissingDataRequest(m); + break; + } + case RESULT: { + handleResultMessage(m); + break; + } + default: { + log.error("Message type ignored: ", type); + } + } + } catch (Throwable t) { + log.warn("Failed to accept message! {}",t); + t.printStackTrace(); + } + } + + private void handleDataProvided(Message m) { + // Just store the data, can't guarantee full persistence yet + try { + ACell o = m.getPayload(); + Ref r = Ref.get(o); + r.persistShallow(); + Hash h=r.getHash(); + log.trace("Recieved DATA for hash {}",h); + unbuffer(h); + } catch (MissingDataException e) { + // ignore? + } + } + + private void handleMissingDataRequest(Message m) { + // try to be helpful by returning sent data + Hash h = RT.ensureHash(m.getPayload()); + if (h==null) return; // not a valid payload so ignore + + Ref r = Stores.current().refForHash(h); + if (r != null) try { + m.getConnection().sendData(r.getValue()); + } catch (IOException e) { + log.debug("Error replying to MISSING DATA request",e); + } + } + + /** + * Map for messages delayed due to missing data + */ + private HashMap> bufferedMessages = new HashMap<>(); + + private synchronized void buffer(Hash hash, Message m) { + ArrayList msgs = bufferedMessages.get(hash); + if (msgs == null) { + msgs = new ArrayList(); + bufferedMessages.put(hash, msgs); + } + msgs.add(m); + } + + /** + * Unbuffer and replay messages for a given hash + * + * @param hash + */ + protected synchronized void unbuffer(Hash hash) { + ArrayList msgs = bufferedMessages.get(hash); + if (msgs != null) { + bufferedMessages.remove(hash); + for (Message m : msgs) { + accept(m); + } + } + } + + /** + * Method called when a result is received. + * + * By default, delegates to handleResult and handleError + */ + private final void handleResultMessage(Message m) { + Result result = m.getPayload(); + try { + ACell.createPersisted(result); + + // we now have the full result, so notify those interested + long id=m.getID().longValue(); + handleResult(id,result); + } catch (MissingDataException e) { + // If there is missing data, re-buffer the message + // And wait for it to arrive later + Hash hash = e.getMissingHash(); + try { + if (m.getConnection().sendMissingData(hash)) { + log.debug("Missing data {} requested by client for RESULT of type: {}",hash.toHexString(),Utils.getClassName(result)); + buffer(hash, m); + } else { + log.debug("Unable to request missing data"); + } + } catch (IOException e1) { + // Ignore. We probably lost this result? + log.warn("IO Exception handling result - {}",e1); + } + return; + } + } + + /** + * Handler for a fully received Result. May be overridden. + * + * @param id ID of message received + * @param result Result value + */ + protected void handleResult(long id, Result result) { + ACell rv = result.getValue(); + ACell err = result.getErrorCode(); + if (err!=null) { + handleError(id, err, rv); + } else { + handleNormalResult(id, rv); + } + } + + /** + * Method called when an error result is received. May be overriden. + * + * Default behaviour is simply to log the error. + * + * If this method throws a MissingDataException, missing data is requested and + * the result handling may be retried later. + * + * @param id The ID of the original message to which this result corresponds + * @param code The error code received. May not be null, and is usually a Keyword + * @param errorMessage The error message associated with the result (may be null) + */ + protected void handleError(long id, ACell code, ACell errorMessage) { + log.warn("UNHANDLED ERROR RECEIVED: {} : {}", code, errorMessage); + } + + /** + * Method called when a normal (non-error) result is received. + * + * If this method throws a MissingDataException, missing data is requested and + * the result handling may be retried later. + * + * @param id The ID of the original message to which this result corresponds + * @param value The result value + */ + protected void handleNormalResult(long id, ACell value) { + log.warn("UNHANDLED RESULT RECEIVED: id={}, value={}", id,value); + } + + +} diff --git a/convex-peer/src/main/java/convex/peer/API.java b/convex-peer/src/main/java/convex/peer/API.java new file mode 100644 index 000000000..62c313da5 --- /dev/null +++ b/convex-peer/src/main/java/convex/peer/API.java @@ -0,0 +1,205 @@ +package convex.peer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import convex.core.State; +import convex.core.crypto.AKeyPair; +import convex.core.data.Hash; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.store.AStore; +import convex.core.store.Stores; +import convex.core.util.Utils; + + +/** + * Class providing a simple API to a peer Server. + * + * Suitable for library usage, e.g. if a usr application wants to + * instantiate a local network peer. + * + * "If you don't believe it or don't get it , I don't have time to convince you" + * - Satoshi Nakamoto + */ +public class API { + + private static final Logger log = LoggerFactory.getLogger(API.class.getName()); + + /** + *

Launches a Peer Server with a supplied configuration.

+ * + *

Config keys are:

+ * + *
    + *
  • :keypair (required, AKeyPair) - AKeyPair instance. + *
  • :port (optional, Integer) - Integer port number to use for incoming connections. Defaults to random allocation. + *
  • :store (optional, AStore) - AStore instance. Defaults to the configured global store + *
  • :source (optional, String) - URL for Peer to replicate initial State/Belief from. + *
  • :state (optional, State) - Genesis state. Defaults to a fresh genesis state for the Peer if neither :source nor :state is specified + *
  • :restore (optional, Boolean) - Boolean Flag to restore from existing store. Default to true + *
  • :persist (optional, Boolean) - Boolean flag to determine if peer state should be persisted in store at server close. Default true. + *
  • :url (optional, String) - public URL for server. If provided, peer will set its public on-chain address based on this. + *
  • :auto-manage (optional Boolean) - set to true for peer to auto-manage own account. Defaults to true. + *
+ * + * @param peerConfig Config map for the new Peer + * + * @param event Optional event object that implements the IServerEvent interface + * + * @return New Server instance + */ + public static Server launchPeer(Map peerConfig) { + HashMap config=new HashMap<>(peerConfig); + + // State no8t strictly necessarry? Should be possible to restore a Peer from store + if (!(config.containsKey(Keywords.STATE) + ||config.containsKey(Keywords.STORE) + ||config.containsKey(Keywords.SOURCE) + )) { + throw new IllegalArgumentException("Peer launch requires a genesis :state, remote :source or existing :store in config"); + } + + if (!config.containsKey(Keywords.KEYPAIR)) throw new IllegalArgumentException("Peer launch requires a "+Keywords.KEYPAIR+" in config"); + + try { + if (!config.containsKey(Keywords.PORT)) config.put(Keywords.PORT, null); + if (!config.containsKey(Keywords.STORE)) config.put(Keywords.STORE, Stores.getGlobalStore()); + if (!config.containsKey(Keywords.RESTORE)) config.put(Keywords.RESTORE, true); + if (!config.containsKey(Keywords.PERSIST)) config.put(Keywords.PERSIST, true); + if (!config.containsKey(Keywords.AUTO_MANAGE)) config.put(Keywords.AUTO_MANAGE, true); + + Server server = Server.create(config); + server.launch(); + return server; + } catch (Throwable t) { + log.error("Error launching peer: ",t); + t.printStackTrace(); + throw Utils.sneakyThrow(t); + } + } + + /** + * Launch a local set of peers. Intended mainly for testing / development. + * + * The Peers will have a unique genesis State, i.e. an independent network + * + * @param keyPairs List of keypairs for peers + * @param genesisState genesis state for local network + * + * @return List of Servers launched + * + */ + public static List launchLocalPeers(List keyPairs, State genesisState) { + return launchLocalPeers(keyPairs, genesisState, null, null); + } + /** + * Launch a local set of peers. Intended mainly for testing / development. + * + * The Peers will have a unique genesis State, i.e. an independent network + * + * @param keyPairs List of keypairs for peers + * @param genesisState GEnesis state for local network + * @param peerPorts Array of ports to use for each peer, if == null then randomly assign port numbers + * @param event Server event handler + * + * @return List of Servers launched + * + */ + public static List launchLocalPeers(List keyPairs, State genesisState, int peerPorts[], IServerEvent event) { + int count=keyPairs.size(); + + List serverList = new ArrayList(); + + Map config = new HashMap<>(); + + // Peer should get a new allocated port + config.put(Keywords.PORT, null); + + // Peers should all have the same genesis state + config.put(Keywords.STATE, genesisState); + + // TODO maybe have this as an option in the calling parameters? + AStore store = Stores.current(); + config.put(Keywords.STORE, store); + + // Automatically manage Peer connections + config.put(Keywords.AUTO_MANAGE, true); + + if (event!=null) { + config.put(Keywords.EVENT_HOOK, event); + } + + for (int i = 0; i < count; i++) { + AKeyPair keyPair = keyPairs.get(i); + config.put(Keywords.KEYPAIR, keyPair); + if (peerPorts != null) { + config.put(Keywords.PORT, peerPorts[i]); + } + Server server = API.launchPeer(config); + serverList.add(server); + } + + Server genesisServer = serverList.get(0); + + // go through 1..count-1 peers and join them all to the genesis Peer + // do this twice to allow for all of the peers to get all of the address in the group of peers + + for (int i = 1; i < count; i++) { + Server server=serverList.get(i); + + // Join each additional Server to the Peer #0 + ConnectionManager cm=server.getConnectionManager(); + cm.connectToPeer(genesisServer.getHostAddress()); + + // Join server #0 to this server + genesisServer.getConnectionManager().connectToPeer(server.getHostAddress()); + server.setHostname("localhost:"+server.getPort()); + } + + // wait for the peers to sync upto 10 seconds + //API.waitForNetworkReady(serverList, 10); + return serverList; + } + + /** + * Returns a true value if the local network is ready and synced with the same consensus state hash. + * + * @param serverList List of local peer servers running on the local network. + * + * @param timeoutMillis Number of milliseconds to wait before exiting with a failure. + * + * @return Return true if all server peers have the same consensus hash, else false is a timeout. + * + */ + public static boolean isNetworkReady(List serverList, long timeoutMillis) { + boolean isReady = false; + long timeoutTime = Utils.getTimeMillis() + timeoutMillis; + while (timeoutTime > Utils.getTimeMillis()) { + isReady = true; + Hash consensusHash = null; + for (Server server: serverList) { + if (consensusHash == null) { + consensusHash = server.getPeer().getConsensusState().getHash(); + } + if (!consensusHash.equals(server.getPeer().getConsensusState().getHash())) { + isReady=false; + } + try { + Thread.sleep(100); + } catch ( InterruptedException e) { + return false; + } + } + if (isReady) { + break; + } + } + return isReady; + } +} diff --git a/convex-peer/src/main/java/convex/peer/ChallengeRequest.java b/convex-peer/src/main/java/convex/peer/ChallengeRequest.java new file mode 100644 index 000000000..72673b351 --- /dev/null +++ b/convex-peer/src/main/java/convex/peer/ChallengeRequest.java @@ -0,0 +1,89 @@ +package convex.peer; + +import java.io.IOException; +import java.security.SecureRandom; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import convex.core.Peer; +import convex.core.data.ACell; +import convex.core.data.AccountKey; +import convex.core.data.AVector; +import convex.core.data.Blob; +import convex.core.data.Vectors; +import convex.core.data.Hash; +import convex.core.data.SignedData; +import convex.net.Connection; + +class ChallengeRequest { + + private static final Logger log = LoggerFactory.getLogger(ChallengeRequest.class.getName()); + + private static final int TIMEOUT_SECONDS = 10; + + + protected AccountKey peerKey; + protected long timeout; + protected Hash token; + protected Hash sendHash; + + private ChallengeRequest(AccountKey peerKey, long timeout) { + this.peerKey = peerKey; + this.timeout = timeout; + } + + public static ChallengeRequest create(AccountKey peerKey) { + return ChallengeRequest.create(peerKey, TIMEOUT_SECONDS); + } + + public static ChallengeRequest create(AccountKey peerKey, int timeoutSeconds) { + long timeout = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(timeoutSeconds); + return new ChallengeRequest(peerKey, timeout); + } + + + /** + * Sends out a single challenge to the remote peer. + * @param connection Connection + * @param peer This Peer + * @return ID of message sent, or negative value if sending fails + */ + public long send(Connection connection, Peer peer) { + AVector values = null; + try { + SecureRandom random = new SecureRandom(); + + // Get 120 random bytes + byte bytes[] = new byte[120]; + random.nextBytes(bytes); + token = Blob.create(bytes).getHash(); + + values = Vectors.of(token, peer.getNetworkID(), peerKey); + SignedData challenge = peer.sign(values); + sendHash = challenge.getHash(); + return connection.sendChallenge(challenge); + } catch (IOException e) { + log.warn("Cannot send challenge to remote peer at {}", connection.getRemoteAddress()); + values = null; + } + return -1; + } + + public AccountKey getPeerKey() { + return peerKey; + } + + public Hash getToken() { + return token; + } + + public Hash getSendHash() { + return sendHash; + } + + public boolean isTimedout() { + return timeout < System.currentTimeMillis(); + } +} diff --git a/convex-peer/src/main/java/convex/peer/ConnectionManager.java b/convex-peer/src/main/java/convex/peer/ConnectionManager.java new file mode 100644 index 000000000..f3f04c6f8 --- /dev/null +++ b/convex-peer/src/main/java/convex/peer/ConnectionManager.java @@ -0,0 +1,639 @@ +package convex.peer; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.UnresolvedAddressException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import convex.api.Convex; +import convex.core.Belief; +import convex.core.Constants; +import convex.core.Peer; +import convex.core.State; +import convex.core.data.ACell; +import convex.core.data.AString; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.Hash; +import convex.core.data.Keywords; +import convex.core.data.PeerStatus; +import convex.core.data.SignedData; +import convex.core.data.Vectors; +import convex.core.lang.RT; +import convex.core.store.Stores; +import convex.core.util.Utils; +import convex.net.Connection; +import convex.net.Message; + +/** + * Class for managing the outbound connections from a Peer Server. + * + * Outbound connections need special handling: - Should be trusted connections + * to known peers - Should be targets for broadcast of belief updates - Should + * be limited in number + */ +public class ConnectionManager { + + private static final Logger log = LoggerFactory.getLogger(ConnectionManager.class.getName()); + + /** + * Pause for each iteration of Server connection loop. + */ + static final long SERVER_CONNECTION_PAUSE = 1000; + + /** + * Pause for each iteration of Server connection loop. + */ + static final long SERVER_POLL_DELAY = 2000; + + + protected final Server server; + private final HashMap connections = new HashMap<>(); + + /** + * Planned future connections for this Peer + */ + private final HashSet plannedConnections=new HashSet<>(); + + /** + * The list of outgoing challenges that are being made to remote peers + */ + private HashMap challengeList = new HashMap<>(); + + private Thread connectionThread = null; + + private SecureRandom random=new SecureRandom(); + + /** + * Timstamp for the last execution of the Connection Manager update loop. + */ + private long lastUpdate=Utils.getCurrentTimestamp(); + + /* + * Runnable loop for managing server connections + */ + private Runnable connectionLoop = new Runnable() { + @Override + public void run() { + Stores.setCurrent(server.getStore()); // ensure the loop uses this Server's store + try { + lastUpdate=Utils.getCurrentTimestamp(); + while (server.isLive()) { + Thread.sleep(ConnectionManager.SERVER_CONNECTION_PAUSE); + makePlannedConnections(); + maintainConnections(); + pollBelief(); + lastUpdate=Utils.getCurrentTimestamp(); + } + } catch (InterruptedException e) { + /* OK? Close the thread normally */ + } catch (Throwable e) { + log.error("Unexpected exception, Terminating Server connection loop"); + e.printStackTrace(); + } finally { + connectionThread = null; + closeAllConnections(); // shut down everything gracefully if we can + } + } + }; + + /** + * Celled by the connection manager to ensure we are tracking latest Beliefs on the network + */ + private void pollBelief() { + try { + // Poll if no recent consensus updates + long lastConsensus=server.getPeer().getConsensusState().getTimeStamp().longValue(); + if (lastConsensus+SERVER_POLL_DELAY>=lastUpdate) return; + + ArrayList conns=new ArrayList<>(connections.values()); + if (conns.size()==0) { + // Nothing to do + // log.debug("No connections available to poll!"); + return; + } + Connection c=conns.get(random.nextInt(conns.size())); + if (c.isClosed()) return; + Convex convex=Convex.connect(c.getRemoteAddress()); + try { + // use requestStatusSync to auto acquire hash of the status instead of the value + AVector status=convex.requestStatusSync(1000); + Hash h=RT.ensureHash(status.get(0)); + @SuppressWarnings("unchecked") + SignedData sb=(SignedData) convex.acquire(h).get(10000,TimeUnit.MILLISECONDS); + server.queueEvent(sb); + } finally { + convex.close(); + } + } catch (Throwable t) { + if (server.isLive()) log.warn("Polling failed: {}",t); + } + } + + private void makePlannedConnections() { + synchronized(plannedConnections) { + for (InetSocketAddress a: plannedConnections) { + Connection c=connectToPeer(a); + if (c==null) { + log.warn( "Planned Connection failed to {}",a); + } else { + log.info("Planned Connection made to {}",a); + } + } + plannedConnections.clear(); + } + } + + + protected void maintainConnections() { + State s=server.getPeer().getConsensusState(); + + long millisSinceLastUpdate=Math.max(0,Utils.getCurrentTimestamp()-lastUpdate); + + int targetPeerCount=getTargetPeerCount(); + int currentPeerCount=connections.size(); + double totalStake=s.computeStakes().get(null); + + AccountKey[] peers = connections.keySet().toArray(new AccountKey[currentPeerCount]); + for (AccountKey p: peers) { + Connection conn=connections.get(p); + + // Remove closed connections. No point keeping these + if ((conn==null)||(conn.isClosed())) { + closeConnection(p); + currentPeerCount--; + continue; + } + + /* + * Always remove Peers not staked in consensus. This should eliminate Peers that have + * withdrawn or are slashed from current consideration. + */ + PeerStatus ps=s.getPeer(p); + if ((ps==null)||(ps.getTotalStake()<=Constants.MINIMUM_EFFECTIVE_STAKE)) { + closeConnection(p); + currentPeerCount--; + continue; + } + + /* Drop Peers randomly if they have a small stake + * This ensure that new peers will get picked up occasionally and + * the distribution of peers tends towards the level of stake over time + */ + if ((millisSinceLastUpdate>0)&&(currentPeerCount>=targetPeerCount)) { + double prop=ps.getTotalStake()/totalStake; // proportion of stake represented by this Peer + // Very low chance of dropping a Peer with high stake (more than + double keepChance=Math.min(1.0, prop*targetPeerCount); + + if (keepChance<1.0) { + + double dropRate=millisSinceLastUpdate/(double)Constants.PEER_CONNECTION_DROP_TIME; + if (random.nextDouble()<(dropRate*(1.0-keepChance))) { + closeConnection(p); + currentPeerCount--; + continue; + } + } + } + + // send request for a trusted peer connection if necessary + // TODO: need to find out why the response message is not being received by the peers + requestChallenge(p, conn, server.getPeer()); + } + + // refresh peers list + currentPeerCount=connections.size(); + peers = connections.keySet().toArray(new AccountKey[currentPeerCount]); + if (peers.length potentialPeers=s.getPeers().keySet(); + InetSocketAddress target=null; + double accStake=0.0; + for (ACell c:potentialPeers) { + AccountKey peerKey=RT.ensureAccountKey(c); + if (connections.containsKey(peerKey)) continue; // skip if already connected + + if (server.getPeerKey().equals(peerKey)) continue; // don't connect to self!! + + PeerStatus ps=s.getPeers().get(peerKey); + if (ps==null) continue; // skip + AString hostName=ps.getHostname(); + if (hostName==null) continue; + InetSocketAddress maybeAddress=Utils.toInetSocketAddress(hostName.toString()); + if (maybeAddress==null) continue; + long peerStake=ps.getTotalStake(); + if (peerStake>0) { + double t=random.nextDouble()*(accStake+peerStake); + if (t>=accStake) { + target=maybeAddress; + } + accStake+=peerStake; + } + } + + if (target!=null) { + // Try to connect to Peer. If it fails, no worry, will retry another peer next time + connectToPeer(target); + } + } + } + + /** + * Gets the desired number of outgoing connections + * @return + */ + private int getTargetPeerCount() { + Integer target; + try { + target = Utils.toInt(server.getConfig().get(Keywords.OUTGOING_CONNECTIONS)); + } catch (Exception ex) { + target=null; + } + if (target==null) target=Constants.DEFAULT_OUTGOING_CONNECTION_COUNT; + return target; + } + + + public ConnectionManager(Server server) { + this.server = server; + } + + public synchronized void setConnection(AccountKey peerKey, Connection peerConnection) { + if (connections.containsKey(peerKey)) { + connections.get(peerKey).close(); + connections.replace(peerKey, peerConnection); + } + else { + connections.put(peerKey, peerConnection); + } + } + + /** + * Close and remove a connection + * + * @param peerKey Peer key linked to the connection to close and remove. + * + */ + public synchronized void closeConnection(AccountKey peerKey) { + if (connections.containsKey(peerKey)) { + Connection conn=connections.get(peerKey); + if (conn!=null) { + conn.close(); + } + connections.remove(peerKey); + server.raiseServerChange("connection"); + } + } + + /** + * Close all outgoing connections from this Peer + */ + public synchronized void closeAllConnections() { + for (Connection conn:connections.values()) { + if (conn!=null) conn.close(); + } + connections.clear(); + } + + /** + * Gets the current set of outbound peer connections from this server + * + * @return Set of connections + */ + public HashMap getConnections() { + return connections; + } + + /** + * Return true if a specified Peer is connected + * @param peerKey Public Key of Peer + * @return True if connected + * + */ + public boolean isConnected(AccountKey peerKey) { + return connections.containsKey(peerKey); + } + + + /** + * Gets a connection based on the peers public key + * @param peerKey Public key of Peer + * + * @return Connection instance, or null if not found + */ + public Connection getConnection(AccountKey peerKey) { + if (!connections.containsKey(peerKey)) return null; + return connections.get(peerKey); + } + + /** + * Returns the number of active connections + * @return Number of connections + */ + public int getConnectionCount() { + return connections.size(); + } + + /** + * Returns the number of trusted connections + * @return Number of trusted connections + * + */ + public int getTrustedConnectionCount() { + int result = 0; + for (Connection connection : connections.values()) { + if (connection.isTrusted()) { + result ++; + } + } + return result; + } + + public void processChallenge(Message m, Peer thisPeer) { + try { + SignedData> signedData = m.getPayload(); + if ( signedData == null) { + log.debug( "challenge bad message data sent"); + return; + } + AVector challengeValues = signedData.getValue(); + + if (challengeValues == null || challengeValues.size() != 3) { + log.debug("challenge data incorrect number of items should be 3 not ",RT.count(challengeValues)); + return; + } + Connection pc = m.getConnection(); + if ( pc == null) { + log.warn( "No remote peer connection from challenge"); + return; + } + // log.log(LEVEL_CHALLENGE_RESPONSE, "Processing challenge request from: " + pc.getRemoteAddress()); + + // get the token to respond with + Hash token = RT.ensureHash(challengeValues.get(0)); + if (token == null) { + log.warn( "no challenge token provided"); + return; + } + + // check to see if we are both want to connect to the same network + Hash networkId = RT.ensureHash(challengeValues.get(1)); + if (networkId == null) { + log.warn( "challenge data has no networkId"); + return; + } + if ( !networkId.equals(thisPeer.getNetworkID())) { + log.warn( "challenge data has incorrect networkId"); + return; + } + // check to see if the challenge is for this peer + AccountKey toPeer = RT.ensureAccountKey(challengeValues.get(2)); + if (toPeer == null) { + log.warn( "challenge data has no toPeer address"); + return; + } + if ( !toPeer.equals(thisPeer.getPeerKey())) { + log.warn( "challenge data has incorrect addressed peer"); + return; + } + + // get who sent this challenge + AccountKey fromPeer = signedData.getAccountKey(); + + // send the signed response back + AVector responseValues = Vectors.of(token, thisPeer.getNetworkID(), fromPeer, signedData.getHash()); + + SignedData response = thisPeer.sign(responseValues); + // log.log(LEVEL_CHALLENGE_RESPONSE, "Sending response to "+ pc.getRemoteAddress()); + if (pc.sendResponse(response) == -1 ){ + log.warn("Failed sending response from challenge to ", pc.getRemoteAddress()); + } + + } catch (Throwable t) { + log.error("Challenge Error: {}" ,t); + // t.printStackTrace(); + } + } + + AccountKey processResponse(Message m, Peer thisPeer) { + try { + SignedData signedData = m.getPayload(); + + log.debug( "Processing response request from: {}",m.getConnection().getRemoteAddress()); + + @SuppressWarnings("unchecked") + AVector responseValues = (AVector) signedData.getValue(); + + if (responseValues.size() != 4) { + log.warn( "response data incorrect number of items should be 4 not {}",responseValues.size()); + return null; + } + + + // get the signed token + Hash token = RT.ensureHash(responseValues.get(0)); + if (token == null) { + log.warn( "no response token provided"); + return null; + } + + // check to see if we are both want to connect to the same network + Hash networkId = RT.ensureHash(responseValues.get(1)); + if ( networkId == null || !networkId.equals(thisPeer.getNetworkID())) { + log.warn( "response data has incorrect networkId"); + return null; + } + // check to see if the challenge is for this peer + AccountKey toPeer = RT.ensureAccountKey(responseValues.get(2)); + if ( toPeer == null || !toPeer.equals(thisPeer.getPeerKey())) { + log.warn( "response data has incorrect addressed peer"); + return null; + } + + // hash sent by the response + Hash challengeHash = RT.ensureHash(responseValues.get(3)); + + // get who sent this challenge + AccountKey fromPeer = signedData.getAccountKey(); + + + if ( !challengeList.containsKey(fromPeer)) { + log.warn( "response from an unkown challenge"); + return null; + } + synchronized(challengeList) { + + // get the challenge data we sent out for this peer + ChallengeRequest challengeRequest = challengeList.get(fromPeer); + + Hash challengeToken = challengeRequest.getToken(); + if (!challengeToken.equals(token)) { + log.warn( "invalid response token sent"); + return null; + } + + AccountKey challengeFromPeer = challengeRequest.getPeerKey(); + if (!signedData.getAccountKey().equals(challengeFromPeer)) { + log.warn("response key does not match requested key, sent from a different peer"); + return null; + } + + // hash sent by this peer for the challenge + Hash challengeSourceHash = challengeRequest.getSendHash(); + if ( !challengeHash.equals(challengeSourceHash)) { + log.warn("response hash of the challenge does not match"); + return null; + } + // remove from list incase this fails, we can generate another challenge + challengeList.remove(fromPeer); + + Connection connection = getConnection(fromPeer); + if (connection != null) { + connection.setTrustedPeerKey(fromPeer); + server.raiseServerChange("trusted connection"); + } + + // return the trusted peer key + return fromPeer; + } + + } catch (Throwable t) { + log.error("Response Error: {}",t); + } + return null; + } + + + + /** + * Sends out a challenge to a connection that is not trusted. + * @param toPeerKey Peer key that we need to send the challenge too. + * @param connection untrusted connection + * @param thisPeer Source peer that the challenge is issued from + * + */ + public void requestChallenge(AccountKey toPeerKey, Connection connection, Peer thisPeer) { + synchronized(challengeList) { + if (connection.isTrusted()) { + return; + } + // skip if a challenge is already being sent + if (challengeList.containsKey(toPeerKey)) { + if (!challengeList.get(toPeerKey).isTimedout()) { + // not timed out, then continue to wait + return; + } + // remove the old timed out request + challengeList.remove(toPeerKey); + } + ChallengeRequest request = ChallengeRequest.create(toPeerKey); + if (request.send(connection, thisPeer)>=0) { + challengeList.put(toPeerKey, request); + } else { + // TODO: check OK to do nothing and send later? + } + } + } + + /** + * + * @param msg Message to broadcast + * + * @param requireTrusted If true, only broadcast to trusted peers + * + */ + public synchronized void broadcast(Message msg, boolean requireTrusted) { + synchronized(connections) { + for (Connection pc : connections.values()) { + try { + if ( (requireTrusted && pc.isTrusted()) || !requireTrusted) { + pc.sendMessage(msg); + } + } catch (IOException e) { + log.error("Error in broadcast: ", e); + } + } + } + } + + /** + * Connects explicitly to a Peer at the given host address + * @param hostAddress Address to connect to + * @return new Connection, or null if attempt fails + */ + public Connection connectToPeer(InetSocketAddress hostAddress) { + Connection newConn = null; + try { + // Temp client connection + Convex convex=Convex.connect(hostAddress); + + AVector status = convex.requestStatusSync(Constants.DEFAULT_CLIENT_TIMEOUT); + if (status == null || status.count()!=Constants.STATUS_COUNT) { + throw new Error("Bad status message from remote Peer"); + } + + AccountKey peerKey =RT.ensureAccountKey(status.get(3)); + if (peerKey==null) return null; + + Connection existing=connections.get(peerKey); + if ((existing!=null)&&!existing.isClosed()) return existing; + // close the current connecton to Convex API + convex.close(); + synchronized(connections) { + // reopen with connection to the peer and handle server messages + newConn = Connection.connect(hostAddress, server.peerReceiveAction, server.getStore(), null,Constants.SOCKET_PEER_BUFFER_SIZE,Constants.SOCKET_PEER_BUFFER_SIZE); + connections.put(peerKey, newConn); + } + server.raiseServerChange("connection"); + } catch (IOException | InterruptedException | ExecutionException | TimeoutException e) { + // ignore any errors from the peer connections + } catch (UnresolvedAddressException e) { + log.info("Unable to resolve host address: "+hostAddress); + } + return newConn; + } + + /** + * Schedules a request to connect to a Peer at the given host address + * @param hostAddress Address to connect to + */ + public void connectToPeerAsync(InetSocketAddress hostAddress) { + synchronized (plannedConnections) { + plannedConnections.add(hostAddress); + } + } + + public void start() { + // Set timestamp for connection updates + lastUpdate=Utils.getCurrentTimestamp(); + + // start connection thread + connectionThread = new Thread(connectionLoop, "Connection Manager thread at "+server.getPort()); + connectionThread.setDaemon(true); + connectionThread.start(); + + } + + public void close() { + if (connectionThread!=null) { + connectionThread.interrupt(); + } + } + + + + +} diff --git a/convex-peer/src/main/java/convex/peer/IServerEvent.java b/convex-peer/src/main/java/convex/peer/IServerEvent.java new file mode 100644 index 000000000..20be10a43 --- /dev/null +++ b/convex-peer/src/main/java/convex/peer/IServerEvent.java @@ -0,0 +1,12 @@ +package convex.peer; + +/** + * Server Event Interface. The server will post events to this the callback. + * + */ +public interface IServerEvent { + + void onServerChange(ServerEvent serverEvent); // sent on server change status, connections, consensus + + +} diff --git a/convex-peer/src/main/java/convex/peer/Server.java b/convex-peer/src/main/java/convex/peer/Server.java new file mode 100644 index 000000000..1a67ebce7 --- /dev/null +++ b/convex-peer/src/main/java/convex/peer/Server.java @@ -0,0 +1,1207 @@ +package convex.peer; + +import java.io.Closeable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import convex.api.Convex; +import convex.core.Belief; +import convex.core.Block; +import convex.core.BlockResult; +import convex.core.Constants; +import convex.core.ErrorCodes; +import convex.core.Peer; +import convex.core.Result; +import convex.core.State; +import convex.core.crypto.AKeyPair; +import convex.core.data.ACell; +import convex.core.data.AString; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.core.data.Format; +import convex.core.data.Hash; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.PeerStatus; +import convex.core.data.Ref; +import convex.core.data.SignedData; +import convex.core.data.Strings; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.BadSignatureException; +import convex.core.exceptions.InvalidDataException; +import convex.core.exceptions.MissingDataException; +import convex.core.init.Init; +import convex.core.lang.Context; +import convex.core.lang.RT; +import convex.core.lang.Reader; +import convex.core.store.AStore; +import convex.core.store.Stores; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Invoke; +import convex.core.util.Shutdown; +import convex.core.util.Utils; +import convex.net.Connection; +import convex.net.Message; +import convex.net.MessageType; +import convex.net.NIOServer; + + +/** + * A self contained server that can be launched with a config. + * + * Server creates the following threads: + * - A ReceiverThread that processes message from the Server's receive Queue + * - An UpdateThread that handles Belief updates and transaction processing + * - A ConnectionManager thread, via the ConnectionManager + * + * "Programming is a science dressed up as art, because most of us don't + * understand the physics of software and it's rarely, if ever, taught. The + * physics of software is not algorithms, data structures, languages, and + * abstractions. These are just tools we make, use, and throw away. The real + * physics of software is the physics of people. Specifically, it's about our + * limitations when it comes to complexity and our desire to work together to + * solve large problems in pieces. This is the science of programming: make + * building blocks that people can understand and use easily, and people will + * work together to solve the very largest problems." ― Pieter Hintjens + * + */ +public class Server implements Closeable { + public static final int DEFAULT_PORT = 18888; + + private static final int RECEIVE_QUEUE_SIZE = 10000; + + private static final int EVENT_QUEUE_SIZE = 1000; + + // Maximum Pause for each iteration of Server update loop. + private static final long SERVER_UPDATE_PAUSE = 5L; + + static final Logger log = LoggerFactory.getLogger(Server.class.getName()); + + // private static final Level LEVEL_MESSAGE = Level.FINER; + + /** + * Queue for received messages to be processed by this Peer Server + */ + private BlockingQueue receiveQueue = new ArrayBlockingQueue(RECEIVE_QUEUE_SIZE); + + /** + * Queue for received events (Beliefs, Transactions) to be processed + */ + private BlockingQueue> eventQueue = new ArrayBlockingQueue<>(EVENT_QUEUE_SIZE); + + + /** + * Message consumer that simply enqueues received messages received by this Server + */ + Consumer peerReceiveAction = new Consumer() { + @Override + public void accept(Message msg) { + try { + receiveQueue.put(msg); + } catch (InterruptedException e) { + log.warn("Interrupt on peer receive queue!"); + } + } + }; + + /** + * Connection manager instance. + */ + protected ConnectionManager manager; + + /** + * Store to use for all threads associated with this server instance + */ + private final AStore store; + + private final HashMap config; + + /** + * Flag for a running server. Setting to false will terminate server threads. + */ + private volatile boolean isRunning = false; + + private NIOServer nio; + private Thread receiverThread = null; + private Thread updateThread = null; + + /** + * The Peer instance current state for this server. Will be updated based on peer events. + */ + private Peer peer; + + /** + * The Peer Controller Address + */ + private Address controller; + + /** + * The list of new transactions to be added to the next Block. Accessed only in update loop + * + * Must all have been fully persisted. + */ + private ArrayList> newTransactions = new ArrayList<>(); + + /** + * The set of queued partial messages pending missing data. + * + * Delivery will be re-attempted when missing data is provided + */ + private HashMap partialMessages = new HashMap(); + + /** + * The list of new beliefs received from remote peers the block being created + * Should only modify with the lock for this Server held. + */ + private HashMap> newBeliefs = new HashMap<>(); + + + /** + * Hostname of the peer server. + */ + String hostname; + + private IServerEvent eventHook = null; + + private Server(HashMap config) throws TimeoutException, IOException { + AStore configStore = (AStore) config.get(Keywords.STORE); + this.store = (configStore == null) ? Stores.current() : configStore; + + // assign the event hook if set + if (config.containsKey(Keywords.EVENT_HOOK)) { + Object maybeHook=config.get(Keywords.EVENT_HOOK); + if (maybeHook instanceof IServerEvent) { + this.eventHook = (IServerEvent)maybeHook; + } + } + // Switch to use the configured store for setup, saving the caller store + final AStore savedStore=Stores.current(); + try { + Stores.setCurrent(store); + this.config = config; + // now setup the connection manager + this.manager = new ConnectionManager(this); + + this.peer = establishPeer(); + + establishController(); + + nio = NIOServer.create(this, receiveQueue); + + } finally { + Stores.setCurrent(savedStore); + } + } + + /** + * Establish the controller Account for this Peer. + */ + private void establishController() { + Address controlAddress=RT.castAddress(getConfig().get(Keywords.CONTROLLER)); + if (controlAddress==null) { + controlAddress=peer.getController(); + if (controlAddress==null) { + throw new IllegalStateException("Peer Controller account does not exist for Peer Key: "+peer.getPeerKey()); + } + } + AccountStatus as=peer.getConsensusState().getAccount(controlAddress); + if (as==null) { + throw new IllegalStateException("Peer Controller Account does not exist: "+controlAddress); + } + if (!as.getAccountKey().equals(getKeyPair().getAccountKey())) { + throw new IllegalStateException("Server keypair does not match keypair for control account: "+controlAddress); + } + this.setPeerController(controlAddress); + } + + @SuppressWarnings("unchecked") + private Peer establishPeer() throws TimeoutException, IOException { + log.info("Establishing Peer with store: {}",Stores.current()); + try { + AKeyPair keyPair = (AKeyPair) getConfig().get(Keywords.KEYPAIR); + if (keyPair==null) throw new IllegalArgumentException("No Peer Key Pair provided in config"); + + Object source=getConfig().get(Keywords.SOURCE); + if (Utils.bool(source)) { + // Peer sync case + InetSocketAddress sourceAddr=Utils.toInetSocketAddress(source); + Convex convex=Convex.connect(sourceAddr); + log.info("Attempting Peer Sync with: "+sourceAddr); + long timeout = establishTimeout(); + AVector status = convex.requestStatusSync(timeout); + if (status == null || status.count()!=Constants.STATUS_COUNT) { + throw new Error("Bad status message from remote Peer"); + } + Hash beliefHash=RT.ensureHash(status.get(0)); + Hash networkID=RT.ensureHash(status.get(2)); + State genF=(State) convex.acquire(networkID).get(timeout,TimeUnit.MILLISECONDS); + log.info("Retreived Genesis State: "+networkID); + SignedData belF=(SignedData) convex.acquire(beliefHash).get(timeout,TimeUnit.MILLISECONDS); + log.info("Retreived Peer Signed Belief: "+networkID); + + Peer peer=Peer.create(keyPair, genF, belF.getValue()); + return peer; + + } else if (Utils.bool(getConfig().get(Keywords.RESTORE))) { + // Restore from storage case + try { + + Peer peer = Peer.restorePeer(store, keyPair); + if (peer != null) { + log.info("Restored Peer with root data hash: {}",store.getRootHash()); + return peer; + } + } catch (Throwable e) { + log.error("Can't restore Peer from store: {}",e); + } + } + State genesisState = (State) config.get(Keywords.STATE); + if (genesisState!=null) { + log.info("Defaulting to standard Peer startup with genesis state: "+genesisState.getHash()); + } else { + AccountKey peerKey=keyPair.getAccountKey(); + genesisState=Init.createState(List.of(peerKey)); + log.info("Created new genesis state: "+genesisState.getHash()+ " with initial peer: "+peerKey); + } + return Peer.createGenesisPeer(keyPair,genesisState); + } catch (ExecutionException|InterruptedException e) { + throw Utils.sneakyThrow(e); + } + } + + private long establishTimeout() { + Object maybeTimeout=getConfig().get(Keywords.TIMEOUT); + if (maybeTimeout==null) return Constants.PEER_SYNC_TIMEOUT; + Utils.toInt(maybeTimeout); + return 0; + } + + /** + * Creates a new (unlaunched) Server with a given config. + * + * @param config Server configuration map. Will be defensively copied. + * + * @param event Event interface where the server will send information about the peer + * @return New Server instance + * @throws IOException If an IO Error occurred establishing the Peer + * @throws TimeoutException If Peer creation timed out + */ + public static Server create(HashMap config) throws TimeoutException, IOException { + return new Server(new HashMap<>(config)); + } + + /** + * Gets the current Belief held by this PeerServer + * + * @return Current Belief + */ + public Belief getBelief() { + return peer.getBelief(); + } + + /** + * Gets the current Peer data structure for this Server. + * + * @return Current Peer + */ + public Peer getPeer() { + return peer; + } + + /** + * Gets the desired host name for this Peer + * @return Hostname String + */ + public String getHostname() { + return hostname; + } + + /** + * Launch the Peer Server, including all main server threads + */ + public void launch() { + AStore savedStore=Stores.current(); + try { + Stores.setCurrent(store); + + HashMap config = getConfig(); + + Object p = config.get(Keywords.PORT); + Integer port = (p == null) ? null : Utils.toInt(p); + + nio.launch((String)config.get(Keywords.HOST), port); + port = nio.getPort(); // Get the actual port (may be auto-allocated) + + if (getConfig().containsKey(Keywords.URL)) { + hostname = (String) config.get(Keywords.URL); + log.debug("Setting desired peer URL to: " + hostname); + } else { + hostname = null; + } + + + + // set running status now, so that loops don't terminate + isRunning = true; + + // Start connection manager loop + manager.start(); + + receiverThread = new Thread(receiverLoop, "Receive Loop on port: " + port); + receiverThread.setDaemon(true); + receiverThread.start(); + + // Start Peer update thread + updateThread = new Thread(updateLoop, "Update Loop on port: " + port); + updateThread.setDaemon(true); + updateThread.start(); + + + // Close server on shutdown, should be before Etch stores in priority + Shutdown.addHook(Shutdown.SERVER, new Runnable() { + @Override + public void run() { + close(); + } + }); + + // Connect to source peer if specified + if (getConfig().containsKey(Keywords.SOURCE)) { + Object s=getConfig().get(Keywords.SOURCE); + InetSocketAddress sa=Utils.toInetSocketAddress(s); + if (sa!=null) { + if (manager.connectToPeer(sa)!=null) { + log.debug("Automatically connected to :source peer at: {}",sa); + } else { + log.warn("Failed to connect to :source peer at: {}",sa); + } + } else { + log.warn("Failed to parse :source peer address {}",s); + } + } + + log.info( "Peer Server started with Peer Address: {}",getPeerKey()); + } catch (Throwable e) { + close(); + throw new Error("Failed to launch Server", e); + } finally { + Stores.setCurrent(savedStore); + } + } + + /** + * Process a message received from a peer or client. We know at this point that the + * message parsed successfully, not much else..... + * + * If the message is partial, will be queued pending delivery of missing data. + * + * Runs on receiver thread + * + * @param m + */ + private void processMessage(Message m) { + MessageType type = m.getType(); + log.trace("Processing message {}",type); + try { + switch (type) { + case BELIEF: + processBelief(m); + break; + case CHALLENGE: + processChallenge(m); + break; + case RESPONSE: + processResponse(m); + break; + case COMMAND: + break; + case DATA: + processData(m); + break; + case MISSING_DATA: + processMissingData(m); + break; + case QUERY: + processQuery(m); + break; + case RESULT: + break; + case TRANSACT: + processTransact(m); + break; + case GOODBYE: + processClose(m); + break; + case STATUS: + processStatus(m); + break; + } + + } catch (MissingDataException e) { + Hash missingHash = e.getMissingHash(); + log.trace("Missing data: {} in message of type {}" , missingHash,type); + try { + registerPartialMessage(missingHash, m); + m.getConnection().sendMissingData(missingHash); + log.trace("Requested missing data {} for partial message",missingHash); + } catch (IOException ex) { + log.warn( "Exception while requesting missing data: {}" + ex); + } + } catch (BadFormatException | ClassCastException | NullPointerException e) { + log.warn("Error processing client message: {}", e); + } + } + + /** + * Respond to a request for missing data, on a best-efforts basis. Requests for + * missing data we do not hold are ignored. + * + * @param m + * @throws BadFormatException + */ + private void processMissingData(Message m) throws BadFormatException { + // payload for a missing data request should be a valid Hash + Hash h = RT.ensureHash(m.getPayload()); + if (h == null) throw new BadFormatException("Hash required for missing data message"); + + Ref r = store.refForHash(h); + if (r != null) { + try { + ACell data = r.getValue(); + boolean sent = m.getConnection().sendData(data); + // log.trace( "Sent missing data for hash: {} with type {}",Utils.getClassName(data)); + if (!sent) { + log.debug("Can't send missing data for hash {} due to full buffer",h); + } + } catch (IOException e) { + log.warn("Unable to deliver missing data for {} due to exception: {}", h, e); + } + } else { + log.debug("Unable to provide missing data for {} from store: {}", h,Stores.current()); + } + } + + @SuppressWarnings("unchecked") + private void processTransact(Message m) { + // query is a vector [id , signed-object] + AVector v = m.getPayload(); + SignedData sd = (SignedData) v.get(1); + + // System.out.println("transact: "+v); + + // Persist the signed transaction. Might throw MissingDataException? + // If we already have the transaction persisted, will get signature status + ACell.createPersisted(sd); + + if (!sd.checkSignature()) { + // terminate the connection, dishonest client? + try { + // TODO: throttle? + m.getConnection().sendResult(m.getID(), Strings.create("Bad Signature!"), ErrorCodes.SIGNATURE); + } catch (IOException e) { + // Ignore?? Connection probably gone anyway + } + log.info("Bad signature from Client! {}" , sd); + return; + } + + registerInterest(sd.getHash(), m); + try { + eventQueue.put(sd); + } catch (InterruptedException e) { + log.warn("Unexpected interruption adding transaction to event queue!"); + } + } + + /** + * Called by a remote peer to close connections to the remote peer. + * + */ + private void processClose(Message m) { + SignedData signedPeerKey = m.getPayload(); + AccountKey remotePeerKey = RT.ensureAccountKey(signedPeerKey.getValue()); + manager.closeConnection(remotePeerKey); + raiseServerChange("connection"); + } + + /** + * Checks if received data fulfils the requirement for a partial message If so, + * process the message again. + * + * @param hash + * @return true if the data request resulted in a re-queued message, false + * otherwise + */ + private boolean maybeProcessPartial(Hash hash) { + Message m; + synchronized (partialMessages) { + m = partialMessages.get(hash); + + if (m != null) { + log.trace( "Attempting to re-queue partial message due to received hash: ",hash); + if (receiveQueue.offer(m)) { + partialMessages.remove(hash); + return true; + } else { + log.warn( "Queue full for message with received hash: {}", hash); + } + } + } + return false; + } + + /** + * Stores a partial message for potential later handling. + * + * @param missingHash Hash of missing data dependency + * @param m Message to re-attempt later when missing data is received. + */ + private void registerPartialMessage(Hash missingHash, Message m) { + synchronized (partialMessages) { + log.trace( "Registering partial message with missing hash: " ,missingHash); + partialMessages.put(missingHash, m); + } + } + + /** + * Register of client interests in receiving transaction responses + */ + private HashMap interests = new HashMap<>(); + + private void registerInterest(Hash signedTransactionHash, Message m) { + interests.put(signedTransactionHash, m); + } + + /** + * Handle general Belief update, taking belief registered in newBeliefs + * + * @return true if Peer Belief changed, false otherwise + * @throws InterruptedException + */ + protected boolean maybeUpdateBelief() throws InterruptedException { + long oldConsensusPoint = peer.getConsensusPoint(); + + // possibly have own transactions to publish + maybePostOwnTransactions(); + + // publish new blocks if needed. Guaranteed to change belief if this happens + boolean published = maybePublishBlock(); + + // only do belief merge if needed: either after publishing a new block or with + // incoming beliefs + if ((!published) && newBeliefs.isEmpty()) return false; + + // Update Peer timestamp first. This determines what we might accept. + peer = peer.updateTimestamp(Utils.getCurrentTimestamp()); + + boolean updated = maybeMergeBeliefs(); + // Must skip broadcast if we haven't published a new Block or updated our own Order + if (!(updated||published)) return false; + + // At this point we know our Order should have changed + final Belief belief = peer.getBelief(); + + broadcastBelief(belief); + + // Report transaction results + long newConsensusPoint = peer.getConsensusPoint(); + if (newConsensusPoint > oldConsensusPoint) { + log.debug("Consensus point update from {} to {}" ,oldConsensusPoint , newConsensusPoint); + for (long i = oldConsensusPoint; i < newConsensusPoint; i++) { + Block block = peer.getPeerOrder().getBlock(i); + BlockResult br = peer.getBlockResult(i); + reportTransactions(block, br); + } + } + + return true; + } + + /** + * Time of last belief broadcast + */ + private long lastBroadcastBelief=0; + private long broadcastCount=0L; + + private void broadcastBelief(Belief belief) { + // At this point we know something updated our belief, so we want to rebroadcast + // belief to network + Consumer> noveltyHandler = r -> { + ACell o = r.getValue(); + if (o == belief) return; // skip sending data for belief cell itself, will be BELIEF payload + Message msg = Message.createData(o); + // broadcast to all peers trusted or not + manager.broadcast(msg, false); + }; + + // persist the state of the Peer, announcing the new Belief + // (ensure we can handle missing data requests etc.) + peer=peer.persistState(noveltyHandler); + + // Broadcast latest Belief to connected Peers + SignedData sb = peer.getSignedBelief(); + + Message msg = Message.createBelief(sb); + + // at the moment broadcast to all peers trusted or not TODO: recheck this + manager.broadcast(msg, false); + lastBroadcastBelief=Utils.getCurrentTimestamp(); + broadcastCount++; + } + + /** + * Gets the number of belief broadcasts made by this Peer + * @return Count of broadcasts from this Server instance + */ + public long getBroadcastCount() { + return broadcastCount; + } + + private long lastBlockPublishedTime=0L; + + /** + * Checks for pending transactions, and if found propose them as a new Block. + * + * @return True if a new block is published, false otherwise. + */ + protected boolean maybePublishBlock() { + long timestamp=Utils.getCurrentTimestamp(); + // skip if recently published a block + if ((lastBlockPublishedTime+Constants.MIN_BLOCK_TIME)>timestamp) return false; + + Block block=null; + int n = newTransactions.size(); + if (n == 0) return false; + // TODO: smaller block if too many transactions? + block = Block.create(timestamp, (List>) newTransactions, peer.getPeerKey()); + newTransactions.clear(); + + ACell.createPersisted(block); + + Peer newPeer = peer.proposeBlock(block); + log.info("New block proposed: {} transaction(s), hash={}", block.getTransactions().count(), block.getHash()); + + peer = newPeer; + lastBlockPublishedTime=timestamp; + return true; + } + + private long lastOwnTransactionTimestamp=0L; + + private static final long OWN_TRANSACTIONS_DELAY=300; + + /** + * Gets the Peer controller Address + * @return Peer controller Address + */ + public Address getPeerController() { + return controller; + } + + /** + * Sets the Peer controller Address + * @param a Peer Controller Address to set + */ + public void setPeerController(Address a) { + controller=a; + } + + /** + * Adds an event to the inbound server event queue. May block. + * @param event Signed event to add to inbound event queue + * @throws InterruptedException + */ + public void queueEvent(SignedData event) throws InterruptedException { + eventQueue.put(event); + } + + /** + * Check if the Peer want to send any of its own transactions + * @param transactionList List of transactions to add to. + */ + private void maybePostOwnTransactions() { + if (!Utils.bool(config.get(Keywords.AUTO_MANAGE))) return; + + State s=getPeer().getConsensusState(); + long ts=Utils.getCurrentTimestamp(); + + // If we already did this recently, don't try again + if (ts<(lastOwnTransactionTimestamp+OWN_TRANSACTIONS_DELAY)) return; + + lastOwnTransactionTimestamp=ts; // mark this timestamp + + String desiredHostname=getHostname(); // Intended hostname + AccountKey peerKey=getPeerKey(); + PeerStatus ps=s.getPeer(peerKey); + AString chn=ps.getHostname(); + String currentHostname=(chn==null)?null:chn.toString(); + + // Try to set hostname if not correctly set + trySetHostname: + if (!Utils.equals(desiredHostname, currentHostname)) { + log.info("Trying to update own hostname from: {} to {}",currentHostname,desiredHostname); + Address address=ps.getController(); + if (address==null) break trySetHostname; + AccountStatus as=s.getAccount(address); + if (as==null) break trySetHostname; + if (!Utils.equals(getPeerKey(), as.getAccountKey())) break trySetHostname; + + String code; + if (desiredHostname==null) { + code = String.format("(set-peer-data %s {:url nil})", peerKey); + } else { + code = String.format("(set-peer-data %s {:url \"%s\"})", peerKey, desiredHostname); + } + ACell message = Reader.read(code); + ATransaction transaction = Invoke.create(address, as.getSequence()+1, message); + newTransactions.add(getKeyPair().signData(transaction)); + } + } + + + /** + * Checks for mergeable remote beliefs, and if found merge and update own + * belief. + * + * @return True if Peer Belief Order was changed, false otherwise. + */ + protected boolean maybeMergeBeliefs() { + try { + // First get the set of new beliefs for merging + Belief[] beliefs; + synchronized (newBeliefs) { + int n = newBeliefs.size(); + beliefs = new Belief[n]; + int i = 0; + for (AccountKey addr : newBeliefs.keySet()) { + beliefs[i++] = newBeliefs.get(addr).getValue(); + } + newBeliefs.clear(); + } + Peer newPeer = peer.mergeBeliefs(beliefs); + + // Check for substantive change (i.e. Orders updated, can ignore timestamp) + if (newPeer.getBelief().getOrders().equals(peer.getBelief().getOrders())) return false; + + log.debug( "New merged Belief update: {}" ,newPeer.getBelief().getHash()); + // we merged successfully, so clear pending beliefs and update Peer + peer = newPeer; + return true; + } catch (MissingDataException e) { + // Shouldn't happen if beliefs are persisted + // e.printStackTrace(); + throw new Error("Missing data in belief update: " + e.getMissingHash().toHexString(), e); + } catch (BadSignatureException e) { + // Shouldn't happen if Beliefs are already validated + // e.printStackTrace(); + throw new Error("Bad Signature in belief update!", e); + } catch (InvalidDataException e) { + // Shouldn't happen if Beliefs are already validated + // e.printStackTrace(); + throw new Error("Invalid data in belief update!", e); + } + } + + private void processStatus(Message m) { + try { + // We can ignore payload + + Connection pc = m.getConnection(); + log.debug( "Processing status request from: {}" ,pc.getRemoteAddress()); + // log.log(LEVEL_MESSAGE, "Processing query: " + form + " with address: " + + // address); + + Peer peer=this.getPeer(); + Hash beliefHash=peer.getSignedBelief().getHash(); + Hash stateHash=peer.getStates().getHash(); + Hash initialStateHash=peer.getStates().get(0).getHash(); + AccountKey peerKey=getPeerKey(); + Hash consensusHash=peer.getConsensusState().getHash(); + + AVector reply=Vectors.of(beliefHash,stateHash,initialStateHash,peerKey,consensusHash); + + pc.sendResult(m.getID(), reply); + } catch (Throwable t) { + log.warn("Status Request Error: {}", t); + } + } + + private void processChallenge(Message m) { + manager.processChallenge(m, peer); + } + + private void processResponse(Message m) { + manager.processResponse(m, peer); + } + + private void processQuery(Message m) { + try { + // query is a vector [id , form, address?] + AVector v = m.getPayload(); + CVMLong id = (CVMLong) v.get(0); + ACell form = v.get(1); + + // extract the Address, or use HERO if not available. + Address address = (Address) v.get(2); + + Connection pc = m.getConnection(); + log.debug( "Processing query: {} with address: {}" , form, address); + // log.log(LEVEL_MESSAGE, "Processing query: " + form + " with address: " + + // address); + Context resultContext = peer.executeQuery(form, address); + boolean resultReturned; + + if (resultContext.isExceptional()) { + resultReturned = pc.sendResult(Result.fromContext(id, resultContext)); + } else { + resultReturned = pc.sendResult(id, resultContext.getResult()); + } + + if (!resultReturned) { + log.warn("Failed to send query result back to client with ID: {}", id); + } + + } catch (Throwable t) { + log.warn("Query Error: {}", t); + } + } + + private void processData(Message m) { + ACell payload = m.getPayload(); + + // TODO: be smarter about this? hold a per-client queue for a while? + Ref r = Ref.get(payload); + r = r.persistShallow(); + Hash payloadHash = r.getHash(); + + if (log.isTraceEnabled()) { + log.trace( "Processing DATA of type: " + Utils.getClassName(payload) + " with hash: " + + payloadHash.toHexString() + " and encoding: " + Format.encodedBlob(payload).toHexString()); + } + // if our data satisfies a missing data object, need to process it + maybeProcessPartial(r.getHash()); + } + + /** + * Process an incoming message that represents a Belief + * + * @param m + */ + private void processBelief(Message m) { + Connection pc = m.getConnection(); + if (pc.isClosed()) return; // skip messages from closed peer + + ACell o = m.getPayload(); + + Ref ref = Ref.get(o); + try { + // check we can persist the new belief + // May also pick up cached signature verification if already held + ref = ref.persist(); + + @SuppressWarnings("unchecked") + SignedData receivedBelief = (SignedData) o; + receivedBelief.validateSignature(); + + // TODO: validate trusted connection? + // TODO: can drop Beliefs if under pressure? + + eventQueue.put(receivedBelief); + } catch (ClassCastException e) { + // bad message? + log.warn("Exception due to bad message from peer? {}" ,e); + } catch (BadSignatureException e) { + // we got sent a bad signature. + // TODO: Probably need to slash peer? but ignore for now + log.warn("Bad signed belief from peer: " + Utils.print(o)); + } catch (InterruptedException e) { + throw Utils.sneakyThrow(e); + } + } + + /* + * Loop to process messages from the receive queue + */ + private Runnable receiverLoop = new Runnable() { + @Override + public void run() { + Stores.setCurrent(getStore()); // ensure the loop uses this Server's store + + try { + log.debug("Reciever thread started for peer at {}", getHostAddress()); + + while (isRunning) { // loop until server terminated + Message m = receiveQueue.poll(100, TimeUnit.MILLISECONDS); + if (m != null) { + processMessage(m); + } + } + + log.debug("Reciever thread terminated normally for peer {}", this); + } catch (InterruptedException e) { + log.debug("Receiver thread interrupted "); + } catch (Throwable e) { + log.warn("Receiver thread terminated abnormally! "); + log.error("Server FAILED: " + e.getMessage()); + e.printStackTrace(); + } + } + }; + + /* + * Runnable loop for managing Server state updates + */ + private final Runnable updateLoop = new Runnable() { + @Override + public void run() { + Stores.setCurrent(getStore()); // ensure the loop uses this Server's store + try { + // loop while the server is running + while (isRunning) { + long timestamp=Utils.getCurrentTimestamp(); + + // Try belief update + if (maybeUpdateBelief() ) { + raiseServerChange("consensus"); + } + + // Maybe rebroadcast Belief if not done recently + if ((lastBroadcastBelief+Constants.REBROADCAST_DELAY) firstEvent=eventQueue.poll(SERVER_UPDATE_PAUSE, TimeUnit.MILLISECONDS); + if (firstEvent==null) return; + ArrayList> allEvents=new ArrayList<>(); + allEvents.add(firstEvent); + eventQueue.drainTo(allEvents); + for (SignedData signedEvent: allEvents) { + ACell event=signedEvent.getValue(); + if (event instanceof ATransaction) { + SignedData receivedTrans=(SignedData)signedEvent; + newTransactions.add(receivedTrans); + } else if (event instanceof Belief) { + SignedData receivedBelief=(SignedData)signedEvent; + AccountKey addr = receivedBelief.getAccountKey(); + SignedData current = newBeliefs.get(addr); + // Make sure the Belief is the latest from a Peer + if ((current == null) || (current.getValue().getTimestamp() <= receivedBelief.getValue() + .getTimestamp())) { + // Add to map of new Beliefs received for each Peer + newBeliefs.put(addr, receivedBelief); + + // Notify the update thread that there is something new to handle + log.debug("Valid belief received by peer at {}: {}" + ,getHostAddress(),receivedBelief.getValue().getHash()); + } + } else { + throw new Error("Unexpected type in event queue!"+Utils.getClassName(event)); + } + } + } + + private void reportTransactions(Block block, BlockResult br) { + // TODO: consider culling old interests after some time period + int nTrans = block.length(); + for (long j = 0; j < nTrans; j++) { + try { + SignedData t = block.getTransactions().get(j); + Hash h = t.getHash(); + Message m = interests.get(h); + if (m != null) { + log.trace("Returning transaction result to ", m.getConnection().getRemoteAddress()); + + Connection pc = m.getConnection(); + if ((pc == null) || pc.isClosed()) continue; + ACell id = m.getID(); + Result res = br.getResults().get(j).withID(id); + + pc.sendResult(res); + interests.remove(h); + } + } catch (Throwable e) { + log.warn("Exception while sending Result: ",e); + // ignore + } + } + } + + /** + * Gets the port that this Server is currently accepting connections on + * @return Port number + */ + public int getPort() { + return nio.getPort(); + } + + @Override + public void finalize() { + close(); + } + + /** + * Writes the Peer data to the configured store. + * + * This will overwrite any previously persisted peer data. + */ + public void persistPeerData() { + AStore tempStore = Stores.current(); + try { + Stores.setCurrent(store); + ACell peerData = peer.toData(); + Ref peerRef = ACell.createPersisted(peerData); + Hash peerHash = peerRef.getHash(); + store.setRootHash(peerHash); + log.info( "Stored peer data for Server with hash: {}", peerHash.toHexString()); + } catch (Throwable e) { + log.warn("Failed to persist peer state when closing server: {}" ,e); + } finally { + Stores.setCurrent(tempStore); + } + } + + @Override + public void close() { + // persist peer state if necessary + if ((peer != null) && Utils.bool(getConfig().get(Keywords.PERSIST))) { + persistPeerData(); + } + + // TODO: not much point signing this? + SignedData signedPeerKey = peer.sign(peer.getPeerKey()); + Message msg = Message.createGoodBye(signedPeerKey); + + // broadcast GOODBYE message to all outgoing remote peers + manager.broadcast(msg, false); + + isRunning = false; + if (updateThread != null) { + updateThread.interrupt(); + try { + updateThread.join(100); + } catch (InterruptedException e) { + // Ignore + } + } + if (receiverThread != null) { + receiverThread.interrupt(); + try { + receiverThread.join(100); + } catch (InterruptedException e) { + // Ignore + } + } + manager.close(); + nio.close(); + // Note we don't do store.close(); because we don't own the store. + } + + /** + * Gets the host address for this Server (including port), or null if closed + * + * @return Host Address + */ + public InetSocketAddress getHostAddress() { + return nio.getHostAddress(); + } + + /** + * Returns the Keypair for this peer server + * + * SECURITY: Be careful with this! + * @return Key pair for Peer + */ + public AKeyPair getKeyPair() { + return getPeer().getKeyPair(); + } + + /** + * Gets the public key of the peer account + * + * @return AccountKey of this Peer + */ + public AccountKey getPeerKey() { + AKeyPair kp = getKeyPair(); + if (kp == null) return null; + return kp.getAccountKey(); + } + + /** + * Gets the Store configured for this Server. A server must consistently use the + * same store instance for all Server threads. + * + * @return Store instance + */ + public AStore getStore() { + return store; + } + + /** + * Reports a server change event to the registered hook, if any + * @param reason Message for server change + */ + public void raiseServerChange(String reason) { + if (eventHook != null) { + ServerEvent serverEvent = ServerEvent.create(this, reason); + eventHook.onServerChange(serverEvent); + } + } + + public ConnectionManager getConnectionManager() { + return manager; + } + + public HashMap getConfig() { + return config; + } + + public Consumer getReceiveAction() { + return peerReceiveAction; + } + + /** + * Sets the desired host name for this Server + * @param string Desired host name String, e.g. "my-domain.com:12345" + */ + public void setHostname(String string) { + hostname=string; + } + + public boolean isLive() { + return isRunning; + } +} diff --git a/convex-peer/src/main/java/convex/peer/ServerEvent.java b/convex-peer/src/main/java/convex/peer/ServerEvent.java new file mode 100644 index 000000000..c186816c9 --- /dev/null +++ b/convex-peer/src/main/java/convex/peer/ServerEvent.java @@ -0,0 +1,30 @@ +package convex.peer; + +/** + * Lightweight wrapper for server events + */ +public class ServerEvent { + + protected ServerInformation information=null; + protected Server server; + protected String reason; + + private ServerEvent(Server server, String reason) { + this.server = server; + this.reason = reason; + } + + public static ServerEvent create(Server server, String reason) { + return new ServerEvent(server, reason); + } + + public ServerInformation getInformation() { + if (information==null) { + information=ServerInformation.create(server); + } + return information; + } + public String getReason() { + return reason; + } +} diff --git a/convex-peer/src/main/java/convex/peer/ServerInformation.java b/convex-peer/src/main/java/convex/peer/ServerInformation.java new file mode 100644 index 000000000..e03df6a95 --- /dev/null +++ b/convex-peer/src/main/java/convex/peer/ServerInformation.java @@ -0,0 +1,88 @@ +package convex.peer; + + +import convex.core.Order; +import convex.core.Peer; +import convex.core.data.AccountKey; +import convex.core.data.Hash; + +/** + * Utility class to extract and store server information samples + */ +public class ServerInformation { + + private AccountKey peerKey; + private String hostname; + private int connectionCount; + private int trustedConnectionCount; + private boolean isSynced; + private boolean isJoined; + private Hash networkID; + private long consensusPoint; + private Hash stateHash; + private Hash beliefHash; + private long blockCount; + + + private ServerInformation(Server server, ConnectionManager manager) { + load(server, manager); + } + + public static ServerInformation create(Server server) { + return new ServerInformation(server, server.getConnectionManager()); + } + + protected void load(Server server, ConnectionManager manager) { + Peer peer = server.getPeer(); + Order order = peer.getPeerOrder(); + + peerKey = peer.getPeerKey(); + hostname = server.getHostname(); + connectionCount = manager.getConnectionCount(); + trustedConnectionCount = manager.getTrustedConnectionCount(); + isSynced = order != null && peer.getConsensusPoint() > 0; + networkID = peer.getNetworkID(); + consensusPoint = peer.getConsensusPoint(); + isJoined = connectionCount > 0; + stateHash = peer.getConsensusState().getHash(); + beliefHash = peer.getBelief().getHash(); + blockCount = 0; + if (order != null ) { + blockCount = order.getBlockCount(); + } + } + + public AccountKey getPeerKey() { + return peerKey; + } + public String getHostname() { + return hostname; + } + public int getConnectionCount() { + return connectionCount; + } + public int getTrustedConnectionCount() { + return trustedConnectionCount; + } + public boolean isSynced() { + return isSynced; + } + public boolean isJoined() { + return isJoined; + } + public Hash getNetworkID() { + return networkID; + } + public long getConsensusPoint() { + return consensusPoint; + } + public Hash getStateHash() { + return stateHash; + } + public Hash getBeliefHash() { + return beliefHash; + } + public long getBlockCount() { + return blockCount; + } +} diff --git a/convex-peer/src/test/java/convex/api/ConvexTest.java b/convex-peer/src/test/java/convex/api/ConvexTest.java new file mode 100644 index 000000000..b86118b7b --- /dev/null +++ b/convex-peer/src/test/java/convex/api/ConvexTest.java @@ -0,0 +1,103 @@ +package convex.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import convex.core.ErrorCodes; +import convex.core.Result; +import convex.core.crypto.AKeyPair; +import convex.core.crypto.Ed25519Signature; +import convex.core.data.Address; +import convex.core.data.Ref; +import convex.core.data.SignedData; +import convex.core.lang.Reader; +import convex.core.lang.ops.Constant; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Invoke; +import convex.core.util.Utils; +import convex.peer.TestNetwork; + +/** + * Tests for a Convex Client connection + */ +public class ConvexTest { + + static Address ADDRESS; + static final AKeyPair KEYPAIR = AKeyPair.generate(); + + private static TestNetwork network; + + @BeforeAll + public static void init() { + network = TestNetwork.getInstance(); + synchronized(network.SERVER) { + try { + ADDRESS=network.CONVEX.createAccountSync(KEYPAIR.getAccountKey()); + network.CONVEX.transfer(ADDRESS, 1000000000L).get(1000,TimeUnit.MILLISECONDS); + } catch (Throwable e) { + e.printStackTrace(); + throw Utils.sneakyThrow(e); + } + } + } + + @Test + public void testConnection() throws IOException, TimeoutException { + synchronized (network.SERVER) { + Convex convex = Convex.connect(network.SERVER); + assertTrue(convex.isConnected()); + convex.close(); + assertFalse(convex.isConnected()); + } + } + + @Test + public void testConvex() throws IOException, TimeoutException { + synchronized (network.SERVER) { + Convex convex = Convex.connect(network.SERVER.getHostAddress(), ADDRESS, KEYPAIR); + Result r = convex.transactSync(Invoke.create(ADDRESS, 0, Reader.read("*address*")), 1000); + assertNull(r.getErrorCode(), "Error:" + r.toString()); + assertEquals(ADDRESS, r.getValue()); + } + } + + @Test + public void testBadSignature() throws IOException, TimeoutException, InterruptedException, ExecutionException { + synchronized (network.SERVER) { + Convex convex = Convex.connect(network.SERVER.getHostAddress(), ADDRESS, KEYPAIR); + Ref tr = Invoke.create(ADDRESS, 0, Reader.read("*address*")).getRef(); + Result r = convex.transact(SignedData.create(KEYPAIR, Ed25519Signature.ZERO, tr)).get(); + assertEquals(ErrorCodes.SIGNATURE, r.getErrorCode()); + } + } + + @SuppressWarnings("unchecked") + @Test + public void testManyTransactions() throws IOException, TimeoutException, InterruptedException, ExecutionException { + synchronized (network.SERVER) { + Convex convex = Convex.connect(network.SERVER.getHostAddress(), ADDRESS, KEYPAIR); + int n = 100; + Future[] rs = new Future[n]; + for (int i = 0; i < n; i++) { + Future f = convex.transact(Invoke.create(ADDRESS, 0, Constant.of(i))); + rs[i] = f; + } + for (int i = 0; i < n; i++) { + Result r = rs[i].get(6000, TimeUnit.MILLISECONDS); + assertNull(r.getErrorCode(), "Error:" + r.toString()); + } + } + } + +} diff --git a/convex-peer/src/test/java/convex/api/TestApplications.java b/convex-peer/src/test/java/convex/api/TestApplications.java new file mode 100644 index 000000000..d57366b29 --- /dev/null +++ b/convex-peer/src/test/java/convex/api/TestApplications.java @@ -0,0 +1,30 @@ +package convex.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class TestApplications { + + @Test + public void testProcess() throws Exception { + Process p=Applications.launchApp(TestApplications.class); + p.waitFor(); + assertEquals(0,p.exitValue()); + } + + @Test + public void testProcessWithArgs() throws Exception { + Process p=Applications.launchApp(TestApplications.class,"foo","bar"); + p.waitFor(); + assertEquals(2,p.exitValue()); + } + + /** + * Test main class for launch + * @param args + */ + public static void main(String[] args) { + System.exit(args.length); + } +} diff --git a/convex-peer/src/test/java/convex/examples/AcquireState.java b/convex-peer/src/test/java/convex/examples/AcquireState.java new file mode 100644 index 000000000..3325eb77f --- /dev/null +++ b/convex-peer/src/test/java/convex/examples/AcquireState.java @@ -0,0 +1,27 @@ +package convex.examples; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import convex.api.Convex; +import convex.core.State; + +public class AcquireState { + + public static void main(String[] args) throws IOException, InterruptedException, ExecutionException, TimeoutException { + // Use a fresh store + //EtchStore etch=EtchStore.createTemp("acquire-testing"); + //Stores.setCurrent(etch); + + InetSocketAddress hostAddress = new InetSocketAddress("convex.world", 43579); + + Convex convex = Convex.connect(hostAddress, null,null); + + State state=convex.acquireState().get(5000, TimeUnit.MILLISECONDS); + + System.out.println(state); + } +} diff --git a/convex-peer/src/test/java/convex/examples/ClientApp.java b/convex-peer/src/test/java/convex/examples/ClientApp.java new file mode 100644 index 000000000..7e859549b --- /dev/null +++ b/convex-peer/src/test/java/convex/examples/ClientApp.java @@ -0,0 +1,27 @@ +package convex.examples; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import convex.api.Convex; +import convex.core.lang.RT; +import convex.peer.Server; + +public class ClientApp { + + public static void main(String... args) throws IOException, InterruptedException, TimeoutException, ExecutionException { + InetSocketAddress hostAddress = new InetSocketAddress("localhost", Server.DEFAULT_PORT + 1); + + Convex convex = Convex.connect(hostAddress, null,null); + + // send a couple of queries, wait for results + convex.querySync(RT.cvm("A beautiful life - something special - a magic moment")); + convex.querySync(RT.cvm(1L)); + convex.close(); + + System.exit(0); + } + +} diff --git a/convex-peer/src/test/java/convex/examples/Ed25519Sign.java b/convex-peer/src/test/java/convex/examples/Ed25519Sign.java new file mode 100644 index 000000000..3cf074814 --- /dev/null +++ b/convex-peer/src/test/java/convex/examples/Ed25519Sign.java @@ -0,0 +1,54 @@ +package convex.examples; + +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; + +import convex.core.data.Blob; +import convex.core.util.Utils; + +/** + * Test class for Ed25519 functionality + */ +public class Ed25519Sign { + + public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, SignatureException { + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519", "BC"); + KeyPair kp = kpg.generateKeyPair(); + + { + byte[] enc=kp.getPrivate().getEncoded(); + System.out.println(enc.length + " bytes in private key encoding:"); + System.out.println(" => "+Blob.wrap(enc).toHexString()); + } + + { + byte[] enc=kp.getPublic().getEncoded(); + System.out.println(enc.length + " bytes in public key encoding:"); + System.out.println(" => "+Blob.wrap(enc).toHexString()); + } + + Signature sig = Signature.getInstance("Ed25519"); + sig.initSign(kp.getPrivate()); + + byte[] msg=Utils.hexToBytes("cafebabe"); + + sig.update(msg); + byte[] sbs = sig.sign(); + + System.out.println("Sig: "+Utils.toHexString(sbs)+" ("+sbs.length+" bytes)"); + + PublicKey pubKey=kp.getPublic(); + sig.initVerify(pubKey); + sig.update(msg); + System.out.println("Verify: "+sig.verify(sbs)); + + } + +} diff --git a/convex-peer/src/test/java/convex/examples/JoinTestNetwork.java b/convex-peer/src/test/java/convex/examples/JoinTestNetwork.java new file mode 100644 index 000000000..a5b985be8 --- /dev/null +++ b/convex-peer/src/test/java/convex/examples/JoinTestNetwork.java @@ -0,0 +1,51 @@ +package convex.examples; + +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import convex.core.Peer; +import convex.core.crypto.AKeyPair; +import convex.core.data.AccountKey; +import convex.core.data.Address; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.exceptions.BadSignatureException; +import convex.core.util.Utils; +import convex.peer.API; +import convex.peer.Server; +import etch.EtchStore; + +public class JoinTestNetwork { + InetSocketAddress hostAddress=Utils.toInetSocketAddress("convex.world:18888"); + AKeyPair kp=AKeyPair.createSeeded(578578); // for user + Address acct=Address.create(47); + AccountKey peerKey=kp.getAccountKey(); + + public void testJoinNetwork() throws IOException, InterruptedException, ExecutionException, TimeoutException, BadSignatureException { + + System.out.println("PublicKey: "+kp.getAccountKey()); + + HashMap config=new HashMap<>(); + config.put(Keywords.KEYPAIR,kp); + config.put(Keywords.STORE,EtchStore.create(new File("temp-join-db.etch"))); + config.put(Keywords.CONTROLLER,acct); + config.put(Keywords.SOURCE,"convex.world:18888"); + + Server newServer=API.launchPeer(config); + + // make peer connections directly + newServer.getConnectionManager().connectToPeer(hostAddress); + + Thread.sleep(10000); + Peer peer=newServer.getPeer(); + System.out.println("State count:"+peer.getStates().count()); + } + + public static void main(String[] args) throws BadSignatureException, IOException, InterruptedException, ExecutionException, TimeoutException { + new JoinTestNetwork().testJoinNetwork(); + } +} diff --git a/convex-peer/src/test/java/convex/examples/PeerCluster.java b/convex-peer/src/test/java/convex/examples/PeerCluster.java new file mode 100644 index 000000000..407323c9a --- /dev/null +++ b/convex-peer/src/test/java/convex/examples/PeerCluster.java @@ -0,0 +1,110 @@ +package convex.examples; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import convex.core.Constants; +import convex.core.State; +import convex.core.crypto.AKeyPair; +import convex.core.crypto.Ed25519KeyPair; +import convex.core.data.AString; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.AccountStatus; +import convex.core.data.Address; +import convex.core.data.BlobMap; +import convex.core.data.BlobMaps; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.Maps; +import convex.core.data.PeerStatus; +import convex.core.data.Strings; +import convex.core.data.Vectors; +import convex.core.util.Utils; +import convex.peer.API; +import convex.peer.Server; + +public class PeerCluster { + + private static final Logger log = LoggerFactory.getLogger(PeerCluster.class.getName()); + + public static final int NUM_PEERS = 5; + public static final ArrayList> PEER_CONFIGS = new ArrayList<>(NUM_PEERS); + public static final ArrayList PEER_KEYPAIRS = new ArrayList<>(NUM_PEERS); + + static { + // create a key pair for each peer + for (int i = 0; i < NUM_PEERS; i++) { + PEER_KEYPAIRS.add(Ed25519KeyPair.createSeeded(1000+i)); + } + + // create configuration maps for each peer + for (int i = 0; i < NUM_PEERS; i++) { + int port = Server.DEFAULT_PORT + i; + Map config = new HashMap<>(); + config.put(Keywords.PORT, port); + config.put(Keywords.KEYPAIR, PEER_KEYPAIRS.get(i)); + PEER_CONFIGS.add(config); + } + + State initialState = createInitialState(); + + for (int i = 0; i < NUM_PEERS; i++) { + Map config = PEER_CONFIGS.get(i); + config.put(Keywords.STATE, initialState); + } + } + + private static State createInitialState() { + // setting up NUM_PEERS peers with accounts 0..NUM_PEERS-1 + AVector accts = Vectors.empty(); + BlobMap peers = BlobMaps.empty(); + for (int i = 0; i < NUM_PEERS; i++) { + AccountKey peerKey = PEER_KEYPAIRS.get(i).getAccountKey(); + Map config = PEER_CONFIGS.get(i); + int port = Utils.toInt(config.get(Keywords.PORT)); + AString urlString = Strings.create("http://localhost"+ port); + Address address = Address.create(i); + PeerStatus ps = PeerStatus.create(address, 1000000000, Maps.create(Keywords.URL,urlString)); + peers = peers.assoc(peerKey, ps); + + AccountStatus as = AccountStatus.create(1000000000,peerKey); + accts = accts.conj(as); + } + + return State.create(accts, peers, Constants.INITIAL_GLOBALS, BlobMaps.empty()); + } + + public static void main(String... args) { + ArrayList peers = new ArrayList<>(NUM_PEERS); + + log.info("Creating peer configurations"); + for (Map config : PEER_CONFIGS) { + peers.add(API.launchPeer(config)); + } + + try { + log.info("Peers launched"); + + while (true) { + try { + Thread.sleep(1000); + // Log.info("Waiting..."); + } catch (InterruptedException e) { + log.warn("Sleep interrupted?"); + return; + } + } + } finally { + log.info("Server stopping...."); + for (Server peer : peers) { + peer.close(); + } + log.info("Server stopped successfully"); + } + } + +} diff --git a/convex-peer/src/test/java/convex/examples/SigSamples.java b/convex-peer/src/test/java/convex/examples/SigSamples.java new file mode 100644 index 000000000..7f5c14d8c --- /dev/null +++ b/convex-peer/src/test/java/convex/examples/SigSamples.java @@ -0,0 +1,27 @@ +package convex.examples; + +import convex.core.crypto.AKeyPair; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.SignedData; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMLong; + +/** + * Test class for Ed25519 functionality + */ +public class SigSamples { + + public static void main(String[] args) { + + AKeyPair kp=AKeyPair.generate(); + AccountKey a=kp.getAccountKey(); + + AVector v=Vectors.of(1L,2L); + SignedData> sd=kp.signData(v); + System.out.println("Address: "+a); + System.out.println("Hash: "+v.getHash()); + System.out.println("Signature: "+sd.getSignature().toString()); + } + +} diff --git a/convex-peer/src/test/java/convex/net/ConnectionTest.java b/convex-peer/src/test/java/convex/net/ConnectionTest.java new file mode 100644 index 000000000..33daa7eec --- /dev/null +++ b/convex-peer/src/test/java/convex/net/ConnectionTest.java @@ -0,0 +1,88 @@ +package convex.net; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.util.ArrayList; + +import org.junit.Test; + +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadFormatException; +import convex.core.store.Stores; +import convex.core.util.Utils; + +/** + * Tests for the low level Connection class + */ +public class ConnectionTest { + + @Test + public void testMessageFlood() throws IOException, BadFormatException, InterruptedException { + final ArrayList received = new ArrayList<>(); + + MemoryByteChannel chan = MemoryByteChannel.create(100); + Connection conn=Connection.create(chan, null, Stores.current(), null); + + // create a custom PeerConnection and MessageReceiver for testing + // null Queue OK, we aren't queueing with our custom receive action + MessageReceiver mr = new MessageReceiver(a -> { + synchronized (received) { + received.add(a); + } + }, conn); + + Thread receiveThread=new Thread(()-> { + while (true) { + try { + mr.receiveFromChannel(chan); + if(Thread.interrupted()) return; + } catch (BadFormatException | IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + throw Utils.sneakyThrow(e); + } + } + }); + receiveThread.start(); + + int NUM=10000; + int sentCount = 0; + int resendCount = 0; + + for (int i=0; i received = new ArrayList<>(); + + MemoryByteChannel chan = MemoryByteChannel.create(10000); + Connection pc = Connection.create(chan, null, Stores.current(), null); + + // create a custom PeerConnection and MessageReceiver for testing + // null Queue OK, we aren't queueing with our custom receive action + MessageReceiver mr = new MessageReceiver(a -> received.add(a), pc); + + ACell msg1 = RT.cvm("Hello World!"); + assertTrue(pc.sendData(msg1)); + ACell msg2 = RT.cvm(13L); + assertTrue(pc.sendData(msg2)); + + // need to call sendBytes to flush send buffer to channel + // since we aren't using a Selector / SocketChannel here + assertTrue(pc.flushBytes()); + + // receive first message + mr.receiveFromChannel(chan); + assertEquals(1, received.size()); + assertEquals(msg1, received.get(0).getPayload()); + + // receive second message + mr.receiveFromChannel(chan); + assertEquals(2, received.size()); + assertEquals(msg2, received.get(1).getPayload()); + + Message m1 = received.get(0); + assertEquals(MessageType.DATA, m1.getType()); + } +} diff --git a/convex-peer/src/test/java/convex/peer/MessageTest.java b/convex-peer/src/test/java/convex/peer/MessageTest.java new file mode 100644 index 000000000..27d8db469 --- /dev/null +++ b/convex-peer/src/test/java/convex/peer/MessageTest.java @@ -0,0 +1,28 @@ +package convex.peer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import convex.core.exceptions.BadFormatException; +import convex.net.MessageType; + +public class MessageTest { + + @Test + public void testTypes() throws BadFormatException { + MessageType[] types = MessageType.values(); + assertEquals(11, types.length); + + for (MessageType t : types) { + assertSame(t, MessageType.decode(t.getMessageCode())); + } + } + + @Test + public void testBadCode() { + assertThrows(BadFormatException.class, () -> MessageType.decode(-1)); + } +} diff --git a/convex-peer/src/test/java/convex/peer/RestoreTest.java b/convex-peer/src/test/java/convex/peer/RestoreTest.java new file mode 100644 index 000000000..bde9bc68c --- /dev/null +++ b/convex-peer/src/test/java/convex/peer/RestoreTest.java @@ -0,0 +1,96 @@ +package convex.peer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.junit.jupiter.api.Test; + +import convex.api.Convex; +import convex.core.Result; +import convex.core.State; +import convex.core.crypto.AKeyPair; +import convex.core.data.AccountKey; +import convex.core.data.Address; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.Lists; +import convex.core.data.Maps; +import convex.core.init.Init; +import convex.core.lang.Symbols; +import convex.core.store.AStore; +import convex.core.transactions.Invoke; +import etch.EtchStore; + +public class RestoreTest { + AKeyPair KP=AKeyPair.createSeeded(123456781); + List keys=Lists.of(KP.getAccountKey()); + + State GENESIS=Init.createState(keys); + Address HERO=Init.GENESIS_ADDRESS; + + @Test + public void restoreTest() throws IOException, InterruptedException, ExecutionException, TimeoutException { +// { +// System.out.println("Test store = "+Stores.current()); +// +// State s=Init.STATE; +// System.out.println("Init Ref = "+s.getRef()); +// +// Ref ref=Ref.forHash(s.getHash()); +// if (ref.isMissing()) { +// System.out.println("State not stored"); +// } else { +// State s2=ref.getValue(); +// System.out.println("Store ref: "+s2.getRef()); +// } +// } + + AStore store=EtchStore.createTemp(); + Map config = Maps.hashMapOf( + Keywords.KEYPAIR,KP, + Keywords.STATE,GENESIS, + Keywords.STORE,store, + Keywords.URL,null, + Keywords.PERSIST,true + ); + Server s1=API.launchPeer(config); + + // Connect with HERO Account + Convex cvx1=Convex.connect(s1); + + Result tx1=cvx1.transactSync(Invoke.create(HERO,1, Symbols.STAR_ADDRESS)); + assertEquals(HERO,tx1.getValue()); + Long balance1=cvx1.getBalance(HERO); + assertTrue(balance1>0); + s1.close(); + + // TODO: testing that server is definitely down. This is a bit slow.... + // assertThrows(Throwable.class,()->cvx1.getBalance(HERO)); + + // Launch peer and connect + Server s2=API.launchPeer(config); + + assertNull(s2.getHostname()); + + Convex cvx2=Convex.connect(s2.getHostAddress(), HERO,KP); + + // TODO: check this? + // Long balance2=cvx2.getBalance(HERO); + // assertEquals(balance1,balance2); + + Result tx2=cvx2.transactSync(Invoke.create(HERO,2, Symbols.BALANCE)); + assertFalse(tx2.isError()); + + State state=s2.getPeer().getConsensusState(); + assertNotNull(state); + } +} \ No newline at end of file diff --git a/convex-peer/src/test/java/convex/peer/ServerTest.java b/convex-peer/src/test/java/convex/peer/ServerTest.java new file mode 100644 index 000000000..6d43c55a6 --- /dev/null +++ b/convex-peer/src/test/java/convex/peer/ServerTest.java @@ -0,0 +1,289 @@ +package convex.peer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import convex.api.Convex; +import convex.core.Belief; +import convex.core.Coin; +import convex.core.ErrorCodes; +import convex.core.Result; +import convex.core.State; +import convex.core.crypto.AKeyPair; +import convex.core.data.ACell; +import convex.core.data.AVector; +import convex.core.data.AccountKey; +import convex.core.data.Address; +import convex.core.data.Hash; +import convex.core.data.Keyword; +import convex.core.data.Keywords; +import convex.core.data.Maps; +import convex.core.data.Ref; +import convex.core.data.SignedData; +import convex.core.data.Vectors; +import convex.core.data.prim.CVMLong; +import convex.core.exceptions.BadSignatureException; +import convex.core.init.Init; +import convex.core.lang.RT; +import convex.core.lang.Reader; +import convex.core.lang.Symbols; +import convex.core.store.AStore; +import convex.core.store.Stores; +import convex.core.transactions.ATransaction; +import convex.core.transactions.Call; +import convex.core.transactions.Invoke; +import convex.core.transactions.Transfer; +import convex.core.util.Utils; +import convex.net.Connection; +import convex.net.Message; +import convex.net.ResultConsumer; +import etch.EtchStore; + +/** + * Tests for a fresh standalone server cluster instance + */ +public class ServerTest { + + private static final Logger log = LoggerFactory.getLogger(ServerTest.class.getName()); + + private HashMap results = new HashMap<>(); + + private static TestNetwork network; + + private Consumer handler = new ResultConsumer() { + @Override + protected synchronized void handleNormalResult(long id, ACell value) { + String msg=id+ " : "+Utils.toString(value); + //System.err.println(msg); + log.debug(msg); + results.put(id, value); + } + + @Override + protected synchronized void handleError(long id, ACell code, ACell message) { + String msg=id+ " ERR: "+Utils.toString(code)+ " : "+message; + //System.err.println(msg); + log.debug(msg); + + results.put(id, code); + } + }; + + @BeforeAll + public static void init() { + network = TestNetwork.getInstance(); + } + + @Test + public void testServerConnect() throws IOException, InterruptedException, TimeoutException { + InetSocketAddress hostAddress=network.SERVER.getHostAddress(); + + // Connect to Peer Server using the current store for the client + Connection pc = Connection.connect(hostAddress, handler, Stores.current()); + AVector v = Vectors.of(1l, 2l, 3l); + long id1 = pc.sendQuery(v,network.HERO); + Utils.timeout(5000, () -> results.get(id1) != null); + assertEquals(v, results.get(id1)); + } + +// Commented out because it's slow.... +// @Test +// public void testServerFlood() throws IOException, InterruptedException { +// InetSocketAddress hostAddress=server.getHostAddress(); +// // This is a test of flooding a client connection with async messages. Should eventually throw an IOExcepion +// // from backpressure and *not* bring down the server. +// Convex convex=Convex.connect(hostAddress, VILLAIN_ADDRESS,Init.VILLAIN_KEYPAIR); +// +// Object cmd=Reader.read("(def tmp (inc tmp))"); +// assertThrows(IOException.class, ()-> { +// for (int i=0; i<1000000; i++) { +// convex.transact(Invoke.create(VILLAIN_ADDRESS, 0, cmd)); +// } +// }); +// } + + @Test + public void testBalanceQuery() throws IOException, TimeoutException { + Convex convex=Convex.connect(network.SERVER.getHostAddress(),network.VILLAIN,network.VILLAIN_KEYPAIR); + + // test the connection is still working + assertNotNull(convex.getBalance(network.VILLAIN)); + } + + @Test + public void testConvexAPI() throws IOException, InterruptedException, ExecutionException, TimeoutException { + Convex convex=Convex.connect(network.SERVER.getHostAddress(),network.VILLAIN,network.VILLAIN_KEYPAIR); + + Future f=convex.query(Symbols.STAR_BALANCE); + convex.core.Result f2=convex.querySync(Symbols.STAR_ADDRESS); + + assertEquals(network.VILLAIN,f2.getValue()); + assertTrue(f.get().getValue() instanceof CVMLong); + + + convex.core.Result r3=convex.querySync(Reader.read("(fail :foo)")); + assertTrue(r3.isError()); + assertEquals(ErrorCodes.ASSERT,r3.getErrorCode()); + assertEquals(Keywords.FOO,r3.getValue()); + assertNotNull(r3.getTrace()); + } + + @Test + public void testMissingData() throws IOException, InterruptedException, TimeoutException { + + InetSocketAddress hostAddress=network.SERVER.getHostAddress(); + + // Connect to Peer Server using the current store for the client + AStore store=Stores.current(); + Connection pc = Connection.connect(hostAddress, handler, store); + State s=network.SERVER.getPeer().getConsensusState(); + Hash h=s.getHash(); + + boolean sent=pc.sendMissingData(h); + assertTrue(sent); + + Thread.sleep(200); + Ref ref=Ref.forHash(h); + assertNotNull(ref); + } + + @Test + public void testJoinNetwork() throws IOException, InterruptedException, ExecutionException, TimeoutException, BadSignatureException { + AKeyPair kp=AKeyPair.generate(); + AccountKey peerKey=kp.getAccountKey(); + + long STAKE=1000000000; + synchronized(network.SERVER) { + Convex heroConvex=network.CONVEX; + + // Create new peer controller account + Address controller=heroConvex.createAccountSync(kp.getAccountKey()); + Result trans=heroConvex.transferSync(controller,Coin.DIAMOND); + assertFalse(trans.isError()); + + // create test user account + Address user=heroConvex.createAccountSync(kp.getAccountKey()); + trans=heroConvex.transferSync(user,STAKE); + assertFalse(trans.isError()); + + Convex convex=Convex.connect(network.SERVER.getHostAddress(), controller, kp); + trans=convex.transactSync(Invoke.create(controller, 0, "(create-peer "+peerKey+" "+STAKE+")")); + assertEquals(RT.cvm(STAKE),trans.getValue()); + //Thread.sleep(1000); // sleep a bit to allow background stuff + + HashMap config=new HashMap<>(); + config.put(Keywords.KEYPAIR,kp); + config.put(Keywords.STORE,EtchStore.createTemp()); + config.put(Keywords.SOURCE,network.SERVER.getHostAddress()); + + Server newServer=API.launchPeer(config); + + // make peer connections directly + newServer.getConnectionManager().connectToPeer(network.SERVER.getHostAddress()); + network.SERVER.getConnectionManager().connectToPeer(newServer.getHostAddress()); + + // should be in consensus at this point since just synced + // note: shouldn't matter which is the current store + assertEquals(newServer.getPeer().getConsensusState(),network.SERVER.getPeer().getConsensusState()); + + Convex client=Convex.connect(newServer.getHostAddress(), user, kp); + assertEquals(user,client.transactSync(Invoke.create(user, 0, "*address*")).getValue()); + + Result r=client.requestStatus().get(1000,TimeUnit.MILLISECONDS); + assertFalse(r.isError()); + } + } + + @Test + public void testAcquireBelief() throws IOException, InterruptedException, ExecutionException, TimeoutException, BadSignatureException { + synchronized(network.SERVER) { + + Convex convex=network.CONVEX; + + Future statusFuture=convex.requestStatus(); + Result status=statusFuture.get(10000,TimeUnit.MILLISECONDS); + assertFalse(status.isError()); + AVector v=status.getValue(); + Hash h=RT.ensureHash(v.get(0)); + + Future> acquiror=convex.acquire(h); + SignedData ab=acquiror.get(10000,TimeUnit.MILLISECONDS); + assertTrue(ab.getValue() instanceof Belief); + assertEquals(h,ab.getHash()); + } + } + + @Test + public void testAcquireState() throws IOException, InterruptedException, ExecutionException, TimeoutException, BadSignatureException { + synchronized(network.SERVER) { + + Convex convex=network.CONVEX; + + State s=convex.acquireState().get(60000,TimeUnit.MILLISECONDS); + assertTrue(s instanceof State); + } + } + + public long checkSent(Connection pc,SignedData st) throws IOException { + long x=pc.sendTransaction(st); + assertTrue(x>=0); + return x; + } + + @Test + public void testServerTransactions() throws IOException, InterruptedException, TimeoutException { + synchronized(network.SERVER) { + InetSocketAddress hostAddress=network.SERVER.getHostAddress(); + + // Connect to Peer Server using the current store for the client + Connection pc = Connection.connect(hostAddress, handler, Stores.current()); + Address addr=network.SERVER.getPeerController(); + long s=network.SERVER.getPeer().getConsensusState().getAccount(addr).getSequence(); + AKeyPair kp=network.SERVER.getKeyPair(); + long id1 = checkSent(pc,kp.signData(Invoke.create(addr, s+1, Reader.read("[1 2 3]")))); + long id2 = checkSent(pc,kp.signData(Invoke.create(addr, s+2, Reader.read("(return 2)")))); + long id2a = checkSent(pc,kp.signData(Invoke.create(addr, s+2, Reader.read("22")))); + long id3 = checkSent(pc,kp.signData(Invoke.create(addr, s+3, Reader.read("(do (def foo :bar) (rollback 3))")))); + long id4 = checkSent(pc,kp.signData(Transfer.create(addr, s+4, addr, 1000))); + long id5 = checkSent(pc,kp.signData(Call.create(addr, s+5, Init.REGISTRY_ADDRESS, Symbols.FOO, Vectors.of(Maps.empty())))); + long id6bad = checkSent(pc,kp.signData(Invoke.create(addr.offset(2), s+6, Reader.read("(def a 1)")))); + long id6 = checkSent(pc,kp.signData(Invoke.create(addr, s+6, Reader.read("foo")))); + + long last=id6; + + assertTrue(last>=0); + assertTrue(!pc.isClosed()); + + // wait for results to come back + assertFalse(Utils.timeout(10000, () -> results.containsKey(last))); + Thread.sleep(100); // bit more time in case something out of order? + + AVector v = Vectors.of(1l, 2l, 3l); + assertEquals(v, results.get(id1)); + assertEquals(RT.cvm(2L), results.get(id2)); + assertEquals(ErrorCodes.SEQUENCE, results.get(id2a)); + assertEquals(RT.cvm(3L), results.get(id3)); + assertEquals(RT.cvm(1000L), results.get(id4)); + assertTrue( results.containsKey(id5)); + assertEquals(ErrorCodes.SIGNATURE, results.get(id6bad)); + assertEquals(ErrorCodes.UNDECLARED, results.get(id6)); + } + } + +} diff --git a/convex-peer/src/test/java/convex/peer/TestNetwork.java b/convex-peer/src/test/java/convex/peer/TestNetwork.java new file mode 100644 index 000000000..07ebf95f8 --- /dev/null +++ b/convex-peer/src/test/java/convex/peer/TestNetwork.java @@ -0,0 +1,85 @@ +package convex.peer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import convex.api.Convex; +import convex.core.State; +import convex.core.crypto.AKeyPair; +import convex.core.data.AccountKey; +import convex.core.data.Address; +import convex.core.init.Init; +import convex.core.util.Utils; + +/** + * Singleton server cluster instance + */ +public class TestNetwork { + + public Server SERVER = null; + + private List SERVERS; + + public Convex CONVEX; + + // Deterministic keypairs + public AKeyPair[] KEYPAIRS = new AKeyPair[] { + AKeyPair.createSeeded(2), + AKeyPair.createSeeded(3), + AKeyPair.createSeeded(5), + AKeyPair.createSeeded(7), + AKeyPair.createSeeded(11), + AKeyPair.createSeeded(13), + AKeyPair.createSeeded(17), + AKeyPair.createSeeded(19), + }; + + public ArrayList PEER_KEYPAIRS=(ArrayList) Arrays.asList(KEYPAIRS).stream().collect(Collectors.toList()); + public ArrayList PEER_KEYS=(ArrayList) Arrays.asList(KEYPAIRS).stream().map(kp->kp.getAccountKey()).collect(Collectors.toList()); + + public AKeyPair FIRST_PEER_KEYPAIR = KEYPAIRS[0]; + public AccountKey FIRST_PEER_KEY = FIRST_PEER_KEYPAIR.getAccountKey(); + + public AKeyPair HERO_KEYPAIR = KEYPAIRS[0]; + public AKeyPair VILLAIN_KEYPAIR = KEYPAIRS[1]; + + public AccountKey HERO_KEY = HERO_KEYPAIR.getAccountKey(); + + public Address HERO; + public Address VILLAIN; + + private static TestNetwork instance = null; + + private TestNetwork() { + // Use fresh State + State s=Init.createState(PEER_KEYS); + HERO=Address.create(Init.GENESIS_ADDRESS); + VILLAIN=HERO.offset(1); + + SERVERS=API.launchLocalPeers(PEER_KEYPAIRS, s); + } + + private void waitForLaunch() { + if (SERVER == null) { + SERVER = SERVERS.get(0); + try { + // Thread.sleep(1000); + API.isNetworkReady(SERVERS, 10000); + CONVEX=Convex.connect(SERVER.getHostAddress(), HERO, HERO_KEYPAIR); + } catch (Throwable t) { + throw Utils.sneakyThrow(t); + } + } + API.isNetworkReady(SERVERS, 10000); + } + + public static TestNetwork getInstance() { + if (instance == null) { + instance = new TestNetwork(); + } + instance.waitForLaunch(); + return instance; + } +} diff --git a/convex.bat b/convex.bat new file mode 100644 index 000000000..22920a97c --- /dev/null +++ b/convex.bat @@ -0,0 +1,3 @@ +@echo off +java -jar convex-cli/target/convex-cli.jar %* +exit /b %errorlevel% diff --git a/docs/coding-principles.md b/docs/coding-principles.md new file mode 100644 index 000000000..93fc5150b --- /dev/null +++ b/docs/coding-principles.md @@ -0,0 +1,74 @@ +## Coding principles + +### Immutable first + +Everything is an immutable data structure, with the exception of necessary +mutable values for either: +1. Locally managed state +2. Lazy computation / caching + +### Trust the JVM + +We unashamedly exploit the JVM as an excellent runtime platform for decentralised sysytems. +It's very good at what it does, in particular the following attributes are very useful: + +- Efficient GC of short-lived objects. Much cheaper than C++ heap allocations, in fact. +- Fast JIT compiler. Close enough to C++ that we don't care. +- Rich runtime library. We don't need many external dependencies, which add complexity +and present security risks. +- Memory safety. No buffer overflows to worry about. +- Portability. This makes it easy to deploy pretty much anywhere. + +The use of a memory-managed runtime is of particular importance to this project. +We absolutely require top class garbage collection to clear unnecessary data from memory +while also ensuring that we can exploit structural sharing of persistent data structures. +We also need the exploit soft references for lazy loading of data structures +that can be evicted when no longer required. Alternative means of managing this +(reference counting etc.) were judged infeasible for performance and complexity reasons. + +### Canonical format + +Our data representations make use of a single, canonical data format. Advantages: + +- Sorting order guaranteed and stable +- Better caching / de-duplication with hashes +- Identity comparison == hash + +### Defensive coding + +Assume that you are being passed bad / malicious inputs. Check everything, +especially if there is any chance that it may have come from an external system. + +### Fail Fast + +Stop the current operation as soon as any unexpected error occurs. Throw an exception so +that a higher level operation can determine what step to take. + +### Common sense + +Any of the the above principles can be overridden by reason, evidence and common sense. + +"The three great essentials to achieve anything worthwhile are, first, hard work; second, stick-to-itiveness; third, common sense." +― Thomas Edison + + +## Some Inspirations + +- *Haskell* - for its functional purity, and attribute which is extremely valuable for +decentralised systems. + +- *Lisp* - for demonstrationg the power of homoiconicity, and the ability to bootstrap a +languge ecosystem with just a few core primatives closely linked to the Lambda Calculus. + +- *Clojure* - primarily for its syntax and functional styole, an elegant evolution of Lisp +for the modern age. + +- *Persistent data structures* - functional data structures that enable efficient operations +such as update while preserving previous copies of data in an immutable fashion. + +- *Java* - for giving us the JVM, an unusually robust and high-performance platform +for implementing systems of this nature. + +- *Ethereum* - for demonstrating a working decentralised execution engine. + +- *Bitcoin* - for demonstrating decentralised consensus, albeit in a very inefficient fashion \ No newline at end of file diff --git a/docs/wip/ethos.md b/docs/wip/ethos.md new file mode 100644 index 000000000..8d5c49eb8 --- /dev/null +++ b/docs/wip/ethos.md @@ -0,0 +1,23 @@ +## Convex Ethos + +### Privacy + +We support the principle that individuals should have a right to privacy, and the Convex Organisation will not disclose any personal information without the individual's consent. + +This principle is also built into the Convex protocol at a technical level, as participants may choose to operate under pseudonymous accounts. + +### Strong Opinions, Weakly Held + +Producing a complex product requires hard decisions. Making decisions well is a difficult art. Nobody has all the answers, and the best solutions come from incorporating the strongest ideas, regardless of source. + +In order to have a realistic plan, it is necessary to form an initial hypothesis on the solution. This is not necessarily the final answer, but a starting assumption on which to iterate and improve. As new information becomes available, the team should be willing to amend this hypothesis in light of new information and experience. + +### Obligation to Dissent + +It is our belief that the best ideas are those that are developed under an environment of honest, constructive criticism. It is therefore our expectation that all team members should speak up if they feel that something is being done the wrong way, and have a better solution to propose. + +### Backwards Compatibility + +We commit to maintaining backwards compatibility as a core feature of the Convex system. It is unacceptable for breaking changes to occur in underlying infrastructure once working systems are in production. + +Special exceptions may be made in extreme circumstances (e.g. fixing fundamental security flaws). It is our goal that this never occurs, but we must be prepared for such an eventuality. \ No newline at end of file diff --git a/docs/wip/governance.md b/docs/wip/governance.md new file mode 100644 index 000000000..a7be2d442 --- /dev/null +++ b/docs/wip/governance.md @@ -0,0 +1,19 @@ + + +## Non-technical Challenges + +As well as technical challenges, the Blockchain sector has faced some significant non-technical challenges. + +* **Illegal Activity** - The anonymous nature of blockchain technology has unfortunately proved valuable for criminals wishing to transfer assets without being identified or tracked. It has facilitated a number of scams and schemes to "get rich quick" of dubious legality. It has also proved rich picking for "black hat" hackers exploiting other users of the network. +* **Over promising** - Many projects launched on a wave of "Blockchain Hype" promised the world, but in fact were unable to deliver. This tendency to over-hype technology is not new (witness the "dotcom bubble") but certainly harmed the credibility of the sector. +* **Inappropriate Application** - In many cases, applications being built with blockchain technology (particularly in the "Enterprise" space) would be better served by traditional database / server technology. While blockchains offer some significant benefits, they require significant trade-offs. In general, applications that are operated by a controlled set of trusted participants do not require decentralisation. +* **Education** - Typical users often do not have the skills to interact with decentralised applications directly, such as management of private keys. While 3rd parties may innovate in providing key management services or more pleasant user experiences, this re-introduces the problem of trusting a centralised gatekeeper, and opens up more potential security risks. +* **Forking** - major blockchain networks have seen "forks" where ecosystems have broken into two or more incompatible networks, perhaps over political differences on whether or not to make a specific technical change on the network. While forking may allow for some interesting technical differentiation, it is an unhelpful property for economic systems - where the ability for economic actors to transact freely with all others is important. + +### Fundamental Principles + +* **Decentralisation** : Economic value exchange must be a public good, open to all, and not controlled by any centralised entity. Without decentralisation, the digital economy will be unfair (not accessible to all) and inefficient (monopoly rents and barriers to entry imposed by centralised gatekeepers). +* **Governance**: The Convex network itself, should serve the interests of the whole community of users, and network governance must be neutral in all matters other than technical operation and improvements to the network. +* **Transparency**: As a public service, the state of the network and all transactions should be publicly visible and verifiable to all. +* **Privacy**: Convex may be used anonymously to protect individual identities. +* **Trust**: In economic transactions, it is often necessary for participants to trust the legal entity they are transacting with. It is important to provide safeguards against illegal activity, and recourse to real-world legal and regulatory solutions. \ No newline at end of file diff --git a/docs/wip/misc-faq.md b/docs/wip/misc-faq.md new file mode 100644 index 000000000..baee4ed52 --- /dev/null +++ b/docs/wip/misc-faq.md @@ -0,0 +1,16 @@ +## Why should I use Convex? + +That's up to you! But we hope you'll find it compelling for these sort of reasons: + +- A fun and empowering experience as a developer +- The ability to easily build powerful decentralised applications +- Probably the best overall performance of any decentralised platform + + +## But XXX is faster! It can do YY million transactions per second! + +Maybe. We're not really in the game of competing on the basis of meaningless benchmarks. + +It's important to remember that performance isn't about a single number. It's about the being able to get what you want done quickly and efficiently, as a user of the system. + +We've seen plenty of big performance claims for decentralised platforms. The headlines may be impressive, but a lot of these don't really stack up when you examine more closely. Usually there are some significant compromises made to achieve these numbers. You can look out for: Unrealistic testing setups. Relaxed security requirements (e.g. using PoA networks). Networks that can't handle general purpose smart contracts. Issues with transactions that span across shards. Long time delays to confirm final consensus. \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..84906e05d --- /dev/null +++ b/pom.xml @@ -0,0 +1,207 @@ + + 4.0.0 + world.convex + convex + 0.7.0-rc3 + pom + + Convex Parent + Parent POM used to build core Convex modules. + https://convex.world + + + convex-core + convex-cli + convex-gui + convex-peer + convex-benchmarks + + + + 11 + 11 + UTF-8 + 1.32 + 5.7.1 + 1.7.31 + ${project.version} + + 2020-02-02T00:00:00Z + + --illegal-access=permit + --add-opens=java.base/java.util=ALL-UNNAMED + + + + + + Convex Public License + https://github.com/Convex-Dev/convex/blob/master/LICENSE.md + + + + + + Mike Anderson + mike@convex.world + Convex Foundation + https://convex.world + + + + + scm:git:git://github.com/Convex-Dev/convex.git + scm:git:ssh://github.com:Convex-Dev/convex.git + https://github.com/Convex-Dev/convex.git + + + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + + + + only-eclipse + + + m2e.version + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + + + + + + + + + release + + + performRelease + true + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.0.1 + + + sign-artifacts + verify + + sign + + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.2.0 + + syntax + + + + + attach-javadocs + + jar + + + + + + + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + enforce-maven + + enforce + + + + + 3.6 + + + + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + ossrh + https://s01.oss.sonatype.org/ + true + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M4 + + + + + From 4dfe9ed6cf975340f9777ef5ab51c24439ec74c9 Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 3 Sep 2021 09:58:52 +0800 Subject: [PATCH 0002/1041] Delete all files --- .gitattributes | 1 - .github/workflows/deploy_docs.yml | 26 - .github/workflows/tests.yml | 20 - .gitignore | 25 - .mvn/maven.config | 1 - BUILD.md | 25 - CHANGELOG.md | 13 - Dockerfile | 11 - LICENSE.md | 132 - README.md | 82 - convex | 8 - convex-benchmarks/.gitignore | 4 - convex-benchmarks/README.md | 41 - convex-benchmarks/pom.xml | 84 - .../java/convex/benchmarks/Benchmarks.java | 55 - .../convex/benchmarks/BigBlockBenchmark.java | 65 - .../java/convex/benchmarks/CVMBenchmark.java | 84 - .../java/convex/benchmarks/EtchBenchmark.java | 53 - .../java/convex/benchmarks/EvalBenchmark.java | 49 - .../java/convex/benchmarks/HashBenchmark.java | 32 - .../convex/benchmarks/LatencyBenchmark.java | 113 - .../convex/benchmarks/ListDataBenchmark.java | 25 - .../java/convex/benchmarks/MapBenchmark.java | 26 - .../java/convex/benchmarks/OpBenchmark.java | 55 - .../convex/benchmarks/SignatureBenchmark.java | 61 - .../ThreadCoordinationBenchmark.java | 48 - convex-cli/.gitignore | 5 - convex-cli/pom.xml | 213 - .../src/docs/asciidoc/images/convex_logo.svg | 1 - convex-cli/src/docs/asciidoc/index.adoc | 660 --- .../src/main/java/convex/cli/Account.java | 38 - .../main/java/convex/cli/AccountBalance.java | 75 - .../main/java/convex/cli/AccountCreate.java | 117 - .../src/main/java/convex/cli/AccountFund.java | 96 - .../java/convex/cli/AccountInformation.java | 77 - .../src/main/java/convex/cli/Constants.java | 27 - .../src/main/java/convex/cli/Helpers.java | 114 - convex-cli/src/main/java/convex/cli/Key.java | 36 - .../src/main/java/convex/cli/KeyExport.java | 98 - .../src/main/java/convex/cli/KeyGenerate.java | 62 - .../src/main/java/convex/cli/KeyImport.java | 83 - .../src/main/java/convex/cli/KeyList.java | 63 - .../src/main/java/convex/cli/Local.java | 35 - .../src/main/java/convex/cli/LocalGUI.java | 41 - .../src/main/java/convex/cli/LocalStart.java | 129 - convex-cli/src/main/java/convex/cli/Main.java | 389 -- convex-cli/src/main/java/convex/cli/Peer.java | 37 - .../src/main/java/convex/cli/PeerCreate.java | 146 - .../src/main/java/convex/cli/PeerStart.java | 142 - .../src/main/java/convex/cli/Query.java | 82 - .../src/main/java/convex/cli/Status.java | 108 - .../src/main/java/convex/cli/Transaction.java | 107 - .../main/java/convex/cli/output/Output.java | 70 - .../java/convex/cli/output/OutputField.java | 25 - .../java/convex/cli/peer/PeerManager.java | 349 -- .../main/java/convex/cli/peer/Session.java | 147 - .../java/convex/cli/peer/SessionItem.java | 106 - .../convex/cli/CLICommandKeyExportTest.java | 68 - .../convex/cli/CLICommandKeyImportTest.java | 34 - .../java/convex/cli/CLICommandKeyTest.java | 28 - .../src/test/java/convex/cli/CLIHelpTest.java | 33 - .../src/test/java/convex/cli/CLIMainTest.java | 51 - .../java/convex/cli/CommandLineTester.java | 69 - .../src/test/java/convex/cli/Helper.java | 19 - .../java/convex/cli/output/OutputTest.java | 68 - .../java/convex/cli/peer/SessionTest.java | 107 - convex-core/.gitignore | 5 - convex-core/pom.xml | 125 - .../convex/core/lang/reader/antlr/Convex.g4 | 189 - convex-core/src/main/assembly/full.xml | 34 - convex-core/src/main/assembly/testing.xml | 30 - convex-core/src/main/cvx/asset/box.cvx | 100 - convex-core/src/main/cvx/asset/box/actor.cvx | 301 -- convex-core/src/main/cvx/asset/nft/simple.cvx | 208 - convex-core/src/main/cvx/asset/nft/tokens.cvx | 747 ---- convex-core/src/main/cvx/convex/asset.cvx | 392 -- convex-core/src/main/cvx/convex/core.cvx | 692 --- .../src/main/cvx/convex/core/metadata.cvx | 1158 ----- convex-core/src/main/cvx/convex/fungible.cvx | 380 -- convex-core/src/main/cvx/convex/play.cvx | 80 - convex-core/src/main/cvx/convex/registry.cvx | 140 - convex-core/src/main/cvx/convex/trust.cvx | 260 -- .../src/main/cvx/convex/trusted-oracle.cvx | 141 - .../main/cvx/convex/trusted-oracle/actor.cvx | 107 - convex-core/src/main/cvx/lab/convex/xform.cvx | 175 - convex-core/src/main/cvx/lab/messenger.cvx | 9 - .../src/main/cvx/lab/prediction-market.cvx | 102 - convex-core/src/main/cvx/lab/secured-loan.con | 18 - convex-core/src/main/cvx/torus/currencies.cvx | 12 - convex-core/src/main/cvx/torus/exchange.cvx | 632 --- .../src/main/java/convex/core/Belief.java | 723 ---- .../src/main/java/convex/core/Block.java | 243 -- .../main/java/convex/core/BlockResult.java | 211 - .../src/main/java/convex/core/Coin.java | 43 - .../src/main/java/convex/core/Constants.java | 195 - .../src/main/java/convex/core/ErrorCodes.java | 190 - .../main/java/convex/core/MergeContext.java | 87 - .../src/main/java/convex/core/Order.java | 309 -- .../src/main/java/convex/core/Peer.java | 564 --- .../src/main/java/convex/core/Result.java | 199 - .../src/main/java/convex/core/State.java | 808 ---- .../java/convex/core/crypto/AKeyPair.java | 117 - .../java/convex/core/crypto/ASignature.java | 90 - .../convex/core/crypto/Ed25519KeyPair.java | 336 -- .../convex/core/crypto/Ed25519Signature.java | 141 - .../java/convex/core/crypto/Encoding.java | 12 - .../main/java/convex/core/crypto/Hashing.java | 143 - .../convex/core/crypto/InsecureRandom.java | 83 - .../java/convex/core/crypto/Mnemonic.java | 257 -- .../src/main/java/convex/core/crypto/PBE.java | 34 - .../java/convex/core/crypto/PEMTools.java | 163 - .../java/convex/core/crypto/PFXTools.java | 170 - .../java/convex/core/crypto/Providers.java | 27 - .../java/convex/core/crypto/Symmetric.java | 160 - .../main/java/convex/core/crypto/Wallet.java | 69 - .../java/convex/core/crypto/WalletEntry.java | 86 - .../java/convex/core/data/AArrayBlob.java | 299 -- .../src/main/java/convex/core/data/ABlob.java | 393 -- .../main/java/convex/core/data/ABlobMap.java | 115 - .../src/main/java/convex/core/data/ACell.java | 495 --- .../java/convex/core/data/ACollection.java | 211 - .../java/convex/core/data/ACountable.java | 56 - .../java/convex/core/data/ADataStructure.java | 121 - .../main/java/convex/core/data/AHashMap.java | 171 - .../main/java/convex/core/data/AHashSet.java | 160 - .../src/main/java/convex/core/data/AList.java | 74 - .../main/java/convex/core/data/ALongBlob.java | 150 - .../src/main/java/convex/core/data/AMap.java | 323 -- .../main/java/convex/core/data/AMapEntry.java | 156 - .../java/convex/core/data/ANumericBlob.java | 65 - .../main/java/convex/core/data/AObject.java | 56 - .../main/java/convex/core/data/ARecord.java | 340 -- .../java/convex/core/data/ARecordGeneric.java | 102 - .../main/java/convex/core/data/ASequence.java | 262 -- .../src/main/java/convex/core/data/ASet.java | 216 - .../main/java/convex/core/data/AString.java | 84 - .../main/java/convex/core/data/ASymbolic.java | 70 - .../main/java/convex/core/data/AVector.java | 268 -- .../java/convex/core/data/AccountKey.java | 281 -- .../java/convex/core/data/AccountStatus.java | 500 --- .../main/java/convex/core/data/Address.java | 227 - .../src/main/java/convex/core/data/Blob.java | 269 -- .../main/java/convex/core/data/BlobMap.java | 744 ---- .../main/java/convex/core/data/BlobMaps.java | 38 - .../main/java/convex/core/data/BlobTree.java | 503 --- .../src/main/java/convex/core/data/Blobs.java | 96 - .../main/java/convex/core/data/Format.java | 981 ----- .../src/main/java/convex/core/data/Hash.java | 223 - .../java/convex/core/data/IAssociative.java | 12 - .../main/java/convex/core/data/INumeric.java | 33 - .../java/convex/core/data/IRefFunction.java | 16 - .../java/convex/core/data/IValidated.java | 26 - .../java/convex/core/data/IWriteable.java | 31 - .../main/java/convex/core/data/Keyword.java | 150 - .../main/java/convex/core/data/Keywords.java | 89 - .../src/main/java/convex/core/data/List.java | 425 -- .../src/main/java/convex/core/data/Lists.java | 19 - .../main/java/convex/core/data/LongBlob.java | 146 - .../main/java/convex/core/data/MapEntry.java | 345 -- .../main/java/convex/core/data/MapLeaf.java | 757 ---- .../main/java/convex/core/data/MapTree.java | 880 ---- .../src/main/java/convex/core/data/Maps.java | 156 - .../java/convex/core/data/PeerStatus.java | 283 -- .../src/main/java/convex/core/data/Ref.java | 687 --- .../main/java/convex/core/data/RefDirect.java | 130 - .../main/java/convex/core/data/RefSoft.java | 151 - .../main/java/convex/core/data/SetLeaf.java | 535 --- .../main/java/convex/core/data/SetTree.java | 655 --- .../src/main/java/convex/core/data/Sets.java | 106 - .../java/convex/core/data/SignedData.java | 306 -- .../java/convex/core/data/StringShort.java | 163 - .../java/convex/core/data/StringSlice.java | 99 - .../java/convex/core/data/StringTree.java | 190 - .../main/java/convex/core/data/Strings.java | 42 - .../main/java/convex/core/data/Symbol.java | 147 - .../main/java/convex/core/data/Syntax.java | 336 -- .../src/main/java/convex/core/data/Tag.java | 87 - .../java/convex/core/data/VectorArray.java | 216 - .../java/convex/core/data/VectorLeaf.java | 763 ---- .../java/convex/core/data/VectorTree.java | 719 ---- .../main/java/convex/core/data/Vectors.java | 134 - .../java/convex/core/data/package-info.java | 5 - .../convex/core/data/prim/APrimitive.java | 66 - .../java/convex/core/data/prim/CVMBool.java | 101 - .../java/convex/core/data/prim/CVMByte.java | 117 - .../java/convex/core/data/prim/CVMChar.java | 132 - .../java/convex/core/data/prim/CVMDouble.java | 130 - .../java/convex/core/data/prim/CVMLong.java | 129 - .../convex/core/data/type/ANumericType.java | 11 - .../convex/core/data/type/AStandardType.java | 42 - .../java/convex/core/data/type/AType.java | 46 - .../convex/core/data/type/AddressType.java | 39 - .../main/java/convex/core/data/type/Any.java | 46 - .../main/java/convex/core/data/type/Blob.java | 39 - .../convex/core/data/type/BlobMapType.java | 41 - .../java/convex/core/data/type/Boolean.java | 40 - .../main/java/convex/core/data/type/Byte.java | 40 - .../convex/core/data/type/CharacterType.java | 40 - .../convex/core/data/type/Collection.java | 39 - .../convex/core/data/type/DataStructure.java | 40 - .../java/convex/core/data/type/Double.java | 40 - .../java/convex/core/data/type/Function.java | 40 - .../convex/core/data/type/KeywordType.java | 41 - .../main/java/convex/core/data/type/List.java | 39 - .../main/java/convex/core/data/type/Long.java | 40 - .../main/java/convex/core/data/type/Map.java | 40 - .../main/java/convex/core/data/type/Nil.java | 47 - .../java/convex/core/data/type/Number.java | 48 - .../java/convex/core/data/type/OpCode.java | 42 - .../java/convex/core/data/type/Record.java | 38 - .../java/convex/core/data/type/Sequence.java | 40 - .../main/java/convex/core/data/type/Set.java | 39 - .../convex/core/data/type/StringType.java | 41 - .../convex/core/data/type/SymbolType.java | 41 - .../convex/core/data/type/SyntaxType.java | 40 - .../convex/core/data/type/Transaction.java | 29 - .../java/convex/core/data/type/Types.java | 87 - .../java/convex/core/data/type/Vector.java | 40 - .../core/exceptions/BadFormatException.java | 21 - .../exceptions/BadSignatureException.java | 21 - .../convex/core/exceptions/BaseException.java | 27 - .../core/exceptions/InvalidDataException.java | 21 - .../core/exceptions/MissingDataException.java | 37 - .../core/exceptions/ParseException.java | 17 - .../convex/core/exceptions/TODOException.java | 18 - .../core/exceptions/ValidationException.java | 16 - .../java/convex/core/init/AInitConfig.java | 79 - .../src/main/java/convex/core/init/Init.java | 350 -- .../src/main/java/convex/core/lang/AFn.java | 60 - .../src/main/java/convex/core/lang/AOp.java | 91 - .../main/java/convex/core/lang/Compiler.java | 868 ---- .../main/java/convex/core/lang/Context.java | 2262 ---------- .../src/main/java/convex/core/lang/Core.java | 2469 ----------- .../src/main/java/convex/core/lang/IFn.java | 25 - .../src/main/java/convex/core/lang/Juice.java | 332 -- .../src/main/java/convex/core/lang/Ops.java | 94 - .../src/main/java/convex/core/lang/RT.java | 1381 ------ .../main/java/convex/core/lang/Reader.java | 78 - .../main/java/convex/core/lang/Symbols.java | 297 -- .../java/convex/core/lang/impl/AClosure.java | 35 - .../java/convex/core/lang/impl/ADataFn.java | 77 - .../convex/core/lang/impl/AExceptional.java | 32 - .../java/convex/core/lang/impl/AReturn.java | 8 - .../convex/core/lang/impl/ATrampoline.java | 28 - .../java/convex/core/lang/impl/CoreFn.java | 128 - .../java/convex/core/lang/impl/CorePred.java | 29 - .../convex/core/lang/impl/ErrorValue.java | 137 - .../main/java/convex/core/lang/impl/Fn.java | 216 - .../java/convex/core/lang/impl/HaltValue.java | 43 - .../java/convex/core/lang/impl/ICoreDef.java | 20 - .../java/convex/core/lang/impl/KeywordFn.java | 52 - .../java/convex/core/lang/impl/MapFn.java | 41 - .../java/convex/core/lang/impl/MultiFn.java | 149 - .../convex/core/lang/impl/RecordFormat.java | 59 - .../convex/core/lang/impl/RecurValue.java | 45 - .../java/convex/core/lang/impl/Reduced.java | 32 - .../convex/core/lang/impl/ReturnValue.java | 43 - .../convex/core/lang/impl/RollbackValue.java | 43 - .../java/convex/core/lang/impl/SeqFn.java | 56 - .../java/convex/core/lang/impl/SetFn.java | 38 - .../convex/core/lang/impl/TailcallValue.java | 49 - .../java/convex/core/lang/ops/AMultiOp.java | 67 - .../main/java/convex/core/lang/ops/Cond.java | 112 - .../java/convex/core/lang/ops/Constant.java | 131 - .../main/java/convex/core/lang/ops/Def.java | 155 - .../main/java/convex/core/lang/ops/Do.java | 88 - .../java/convex/core/lang/ops/Invoke.java | 108 - .../java/convex/core/lang/ops/Lambda.java | 111 - .../main/java/convex/core/lang/ops/Let.java | 179 - .../main/java/convex/core/lang/ops/Local.java | 104 - .../java/convex/core/lang/ops/Lookup.java | 136 - .../main/java/convex/core/lang/ops/Query.java | 94 - .../main/java/convex/core/lang/ops/Set.java | 128 - .../java/convex/core/lang/ops/Special.java | 154 - .../convex/core/lang/reader/AntlrReader.java | 477 --- .../convex/core/lang/reader/ReaderUtils.java | 79 - .../main/java/convex/core/store/AStore.java | 115 - .../java/convex/core/store/BlobCache.java | 66 - .../java/convex/core/store/MemoryStore.java | 108 - .../main/java/convex/core/store/Stores.java | 70 - .../core/transactions/ATransaction.java | 118 - .../java/convex/core/transactions/Call.java | 144 - .../java/convex/core/transactions/Invoke.java | 173 - .../convex/core/transactions/Transfer.java | 147 - .../src/main/java/convex/core/util/Bits.java | 106 - .../main/java/convex/core/util/Counters.java | 25 - .../main/java/convex/core/util/Economics.java | 53 - .../main/java/convex/core/util/Errors.java | 54 - .../src/main/java/convex/core/util/Huge.java | 137 - .../java/convex/core/util/MergeFunction.java | 16 - .../main/java/convex/core/util/Shutdown.java | 77 - .../src/main/java/convex/core/util/Text.java | 71 - .../src/main/java/convex/core/util/UMath.java | 35 - .../src/main/java/convex/core/util/Utils.java | 1339 ------ convex-core/src/main/java/etch/Etch.java | 909 ---- convex-core/src/main/java/etch/EtchStore.java | 218 - convex-core/src/test/cvx/test/asset/box.cvx | 301 -- .../src/test/cvx/test/asset/nft/simple.cvx | 278 -- .../src/test/cvx/test/asset/nft/tokens.cvx | 1042 ----- .../src/test/cvx/test/convex/asset.cvx | 167 - .../test/convex/asset/quantity/set-long.cvx | 59 - .../src/test/cvx/test/convex/fungible.cvx | 509 --- .../test/cvx/test/convex/fungible/generic.cvx | 46 - convex-core/src/test/cvx/test/convex/play.cvx | 97 - .../src/test/cvx/test/convex/registry.cvx | 153 - .../src/test/cvx/test/convex/trust.cvx | 390 -- .../test/cvx/test/convex/trusted-oracle.cvx | 109 - .../src/test/cvx/test/torus/exchange.cvx | 499 --- .../test/java/convex/actors/ActorsTest.java | 256 -- .../convex/actors/PredictionMarketTest.java | 168 - .../test/java/convex/actors/RegistryTest.java | 111 - .../test/java/convex/actors/TorusTest.java | 251 -- .../test/java/convex/comms/GenTestFormat.java | 59 - .../java/convex/comms/VLCEncodingTest.java | 142 - .../test/java/convex/comms/VLCParamTest.java | 56 - .../java/convex/core/BeliefMergeTest.java | 497 --- .../java/convex/core/BeliefVotingTest.java | 16 - .../java/convex/core/MessageSizeTest.java | 32 - .../src/test/java/convex/core/PeerTest.java | 118 - .../src/test/java/convex/core/ResultTest.java | 31 - .../test/java/convex/core/StakingTest.java | 71 - .../src/test/java/convex/core/StateTest.java | 72 - .../convex/core/StateTransitionsTest.java | 317 -- .../java/convex/core/TransactionTest.java | 49 - .../convex/core/crypto/AccountKeyTest.java | 20 - .../java/convex/core/crypto/Ed25519Test.java | 201 - .../java/convex/core/crypto/HashTest.java | 102 - .../java/convex/core/crypto/MnemonicTest.java | 43 - .../test/java/convex/core/crypto/PBETest.java | 16 - .../java/convex/core/crypto/PEMToolsTest.java | 56 - .../test/java/convex/core/crypto/PFXTest.java | 41 - .../convex/core/crypto/ParamTestHash.java | 47 - .../convex/core/crypto/SignKeyPairTest.java | 20 - .../convex/core/crypto/SymmetricTest.java | 41 - .../java/convex/core/crypto/WalletTest.java | 17 - .../java/convex/core/data/AccountKeyTest.java | 46 - .../java/convex/core/data/AddressTest.java | 33 - .../java/convex/core/data/BlobMapsTest.java | 297 -- .../test/java/convex/core/data/BlobsTest.java | 239 -- .../java/convex/core/data/BlocksTest.java | 49 - .../convex/core/data/CollectionsTest.java | 138 - .../java/convex/core/data/EncodingTest.java | 223 - .../java/convex/core/data/FuzzTestFormat.java | 97 - .../convex/core/data/GenTestAnyValue.java | 143 - .../java/convex/core/data/GenTestBlobs.java | 25 - .../core/data/GenTestDataStructures.java | 72 - .../java/convex/core/data/GenTestMap.java | 72 - .../convex/core/data/GenTestMessages.java | 25 - .../java/convex/core/data/GenTestStrings.java | 19 - .../java/convex/core/data/GenTestVectors.java | 44 - .../java/convex/core/data/KeywordTest.java | 67 - .../test/java/convex/core/data/ListsTest.java | 90 - .../test/java/convex/core/data/MapsTest.java | 361 -- .../java/convex/core/data/ObjectsTest.java | 183 - .../java/convex/core/data/ParamTestBlobs.java | 72 - .../java/convex/core/data/ParamTestOps.java | 79 - .../java/convex/core/data/ParamTestRefs.java | 81 - .../convex/core/data/ParamTestValues.java | 69 - .../convex/core/data/ParamTestVector.java | 77 - .../java/convex/core/data/RecordTest.java | 90 - .../test/java/convex/core/data/RefTest.java | 182 - .../test/java/convex/core/data/SetsTest.java | 209 - .../java/convex/core/data/SignedDataTest.java | 110 - .../java/convex/core/data/StreamsTest.java | 33 - .../java/convex/core/data/StringsTest.java | 31 - .../java/convex/core/data/SymbolTest.java | 55 - .../java/convex/core/data/SyntaxTest.java | 59 - .../java/convex/core/data/TreeVectorTest.java | 81 - .../java/convex/core/data/VectorsTest.java | 337 -- .../java/convex/core/data/prim/ByteTest.java | 39 - .../java/convex/core/data/prim/LongTest.java | 14 - .../java/convex/core/data/type/TypesTest.java | 183 - .../test/java/convex/core/init/InitTest.java | 124 - .../test/java/convex/core/lang/ACVMTest.java | 240 -- .../test/java/convex/core/lang/AliasTest.java | 120 - .../java/convex/core/lang/CompilerTest.java | 595 --- .../java/convex/core/lang/ContextTest.java | 201 - .../test/java/convex/core/lang/CoreTest.java | 3793 ----------------- .../convex/core/lang/DataStructuresTest.java | 63 - .../test/java/convex/core/lang/DocsTest.java | 58 - .../java/convex/core/lang/GenTestCode.java | 63 - .../java/convex/core/lang/GenTestCore.java | 220 - .../test/java/convex/core/lang/GenTestRT.java | 44 - .../test/java/convex/core/lang/JuiceTest.java | 173 - .../java/convex/core/lang/NumericsTest.java | 307 -- .../test/java/convex/core/lang/OpsTest.java | 302 -- .../java/convex/core/lang/ParamTestCasts.java | 64 - .../java/convex/core/lang/ParamTestEvals.java | 122 - .../java/convex/core/lang/ParamTestJuice.java | 126 - .../core/lang/ParamTestRTSequences.java | 82 - .../test/java/convex/core/lang/RTTest.java | 88 - .../java/convex/core/lang/ReaderTest.java | 273 -- .../java/convex/core/lang/SyntaxTest.java | 19 - .../test/java/convex/core/lang/TestState.java | 252 -- .../convex/core/lang/reader/ANTLRTest.java | 144 - .../java/convex/core/util/EconomicsTest.java | 47 - .../convex/core/util/GenTestEconomics.java | 31 - .../src/test/java/convex/lib/AssetTest.java | 106 - .../test/java/convex/lib/FungibleTest.java | 244 -- .../test/java/convex/lib/SimpleNFTTest.java | 69 - .../src/test/java/convex/lib/TrustTest.java | 237 - .../test/java/convex/store/EtchInitTest.java | 35 - .../test/java/convex/store/EtchStoreTest.java | 240 -- .../java/convex/store/MemoryStoreTest.java | 114 - .../java/convex/store/ParamTestStores.java | 5 - .../src/test/java/convex/test/Assertions.java | 115 - .../src/test/java/convex/test/Samples.java | 282 -- .../src/test/java/convex/test/Testing.java | 39 - .../convex/test/generators/AddressGen.java | 19 - .../convex/test/generators/AnyMapGen.java | 60 - .../java/convex/test/generators/BlobGen.java | 48 - .../convex/test/generators/CollectionGen.java | 38 - .../test/generators/DataStructureGen.java | 46 - .../java/convex/test/generators/FormGen.java | 72 - .../convex/test/generators/KeywordGen.java | 33 - .../java/convex/test/generators/ListGen.java | 26 - .../java/convex/test/generators/MapGen.java | 48 - .../convex/test/generators/NumericGen.java | 39 - .../convex/test/generators/PrimitiveGen.java | 46 - .../convex/test/generators/RecordGen.java | 36 - .../java/convex/test/generators/SetGen.java | 46 - .../convex/test/generators/StringGen.java | 32 - .../test/generators/TransactionGen.java | 39 - .../java/convex/test/generators/ValueGen.java | 59 - .../convex/test/generators/VectorGen.java | 70 - .../java/convex/util/BigIntegerParamTest.java | 46 - .../src/test/java/convex/util/BitsTest.java | 18 - .../test/java/convex/util/GenTestHuge.java | 64 - .../test/java/convex/util/GenTestUMath.java | 24 - .../src/test/java/convex/util/HugeTest.java | 46 - .../src/test/java/convex/util/TextTest.java | 28 - .../src/test/java/convex/util/UMathTest.java | 15 - .../src/test/java/convex/util/UtilsTest.java | 375 -- .../src/test/java/etch/api/TestEtch.java | 73 - .../test/resources/contracts/box/test1.con | 33 - .../test/resources/contracts/deposit-box.con | 19 - .../test/resources/contracts/exceptional.con | 30 - .../src/test/resources/contracts/funding.con | 54 - .../src/test/resources/contracts/hello.con | 16 - .../contracts/nft/simple-nft-test.con | 21 - .../src/test/resources/contracts/token.con | 42 - .../src/test/resources/examples/adventure.cvx | 21 - .../test/resources/junit-platform.properties | 4 - .../src/test/resources/testsource/min.con | 8 - convex-gui/.gitignore | 5 - convex-gui/pom.xml | 103 - convex-gui/src/main/assembly/full.xml | 34 - convex-gui/src/main/assembly/testing.xml | 30 - .../java/convex/gui/client/ConvexClient.java | 116 - .../convex/gui/client/panels/HomePanel.java | 41 - .../gui/components/AccountChooserPanel.java | 102 - .../convex/gui/components/ActionPanel.java | 23 - .../gui/components/BaseListComponent.java | 16 - .../gui/components/BlockViewComponent.java | 82 - .../java/convex/gui/components/CodeLabel.java | 16 - .../gui/components/DefaultReceiveAction.java | 51 - .../convex/gui/components/DropdownMenu.java | 30 - .../java/convex/gui/components/Identicon.java | 42 - .../convex/gui/components/PeerComponent.java | 139 - .../java/convex/gui/components/PeerView.java | 92 - .../convex/gui/components/ScrollyList.java | 96 - .../java/convex/gui/components/Toast.java | 93 - .../gui/components/UnlockWalletDialog.java | 93 - .../gui/components/WalletComponent.java | 100 - .../convex/gui/components/WorldPanel.java | 54 - .../components/models/AccountsTableModel.java | 90 - .../components/models/OracleTableModel.java | 109 - .../gui/components/models/StateModel.java | 68 - .../java/convex/gui/etch/EtchExplorer.java | 112 - .../convex/gui/etch/panels/DatabasePanel.java | 98 - .../main/java/convex/gui/manager/PeerGUI.java | 317 -- .../gui/manager/mainpanels/AboutPanel.java | 81 - .../gui/manager/mainpanels/AccountsPanel.java | 150 - .../gui/manager/mainpanels/ActorsPanel.java | 38 - .../gui/manager/mainpanels/DeployPanel.java | 44 - .../gui/manager/mainpanels/HomePanel.java | 36 - .../gui/manager/mainpanels/KeyGenPanel.java | 221 - .../mainpanels/MessageFormatPanel.java | 134 - .../manager/mainpanels/PeersListPanel.java | 157 - .../gui/manager/mainpanels/WalletPanel.java | 60 - .../mainpanels/actors/DeployPanel.java | 43 - .../mainpanels/actors/MarketComponent.java | 214 - .../mainpanels/actors/MarketsPanel.java | 53 - .../mainpanels/actors/OraclePanel.java | 193 - .../gui/manager/windows/BaseWindow.java | 32 - .../manager/windows/actor/ActorInfoPanel.java | 51 - .../windows/actor/ActorInvokePanel.java | 44 - .../manager/windows/actor/ActorWindow.java | 44 - .../gui/manager/windows/actor/ArgBox.java | 13 - .../gui/manager/windows/actor/ParamLabel.java | 20 - .../windows/actor/SmartOpComponent.java | 177 - .../gui/manager/windows/etch/EtchWindow.java | 47 - .../gui/manager/windows/peer/PeerWindow.java | 44 - .../gui/manager/windows/peer/REPLPanel.java | 296 -- .../gui/manager/windows/peer/StressPanel.java | 249 -- .../manager/windows/state/StateTreeNode.java | 79 - .../manager/windows/state/StateTreePanel.java | 129 - .../manager/windows/state/StateWindow.java | 30 - .../convex/gui/utils/RobinsonProjection.java | 112 - .../main/java/convex/gui/utils/Toolkit.java | 139 - .../src/main/java/convex/wallet/Wallet.java | 73 - .../src/main/resources/images/Convex.png | Bin 10210 -> 0 bytes convex-gui/src/main/resources/images/cog.png | Bin 5007 -> 0 bytes .../resources/images/ic_cake_black_36dp.png | Bin 657 -> 0 bytes .../images/ic_lock_open_black_36dp.png | Bin 502 -> 0 bytes .../images/ic_lock_outline_black_36dp.png | Bin 504 -> 0 bytes .../images/ic_priority_high_black_36dp.png | Bin 185 -> 0 bytes .../resources/images/ic_stars_black_36dp.png | Bin 951 -> 0 bytes .../main/resources/images/padlock-open.png | Bin 1946 -> 0 bytes .../src/main/resources/images/padlock.png | Bin 1948 -> 0 bytes .../src/main/resources/images/world.png | Bin 468500 -> 0 bytes .../src/test/java/convex/gui/GUITest.java | 25 - convex-peer/.gitignore | 5 - convex-peer/pom.xml | 59 - .../main/java/convex/api/Applications.java | 37 - .../src/main/java/convex/api/Convex.java | 898 ---- .../src/main/java/convex/net/Connection.java | 770 ---- .../java/convex/net/MemoryByteChannel.java | 70 - .../src/main/java/convex/net/Message.java | 111 - .../main/java/convex/net/MessageReceiver.java | 174 - .../main/java/convex/net/MessageSender.java | 81 - .../src/main/java/convex/net/MessageType.java | 143 - .../src/main/java/convex/net/NIOServer.java | 269 -- .../main/java/convex/net/ResultConsumer.java | 191 - .../src/main/java/convex/peer/API.java | 205 - .../java/convex/peer/ChallengeRequest.java | 89 - .../java/convex/peer/ConnectionManager.java | 639 --- .../main/java/convex/peer/IServerEvent.java | 12 - .../src/main/java/convex/peer/Server.java | 1207 ------ .../main/java/convex/peer/ServerEvent.java | 30 - .../java/convex/peer/ServerInformation.java | 88 - .../src/test/java/convex/api/ConvexTest.java | 103 - .../java/convex/api/TestApplications.java | 30 - .../java/convex/examples/AcquireState.java | 27 - .../test/java/convex/examples/ClientApp.java | 27 - .../java/convex/examples/Ed25519Sign.java | 54 - .../java/convex/examples/JoinTestNetwork.java | 51 - .../java/convex/examples/PeerCluster.java | 110 - .../test/java/convex/examples/SigSamples.java | 27 - .../test/java/convex/net/ConnectionTest.java | 88 - .../convex/net/MemoryByteChannelTest.java | 23 - .../java/convex/peer/MessageReceiverTest.java | 56 - .../test/java/convex/peer/MessageTest.java | 28 - .../test/java/convex/peer/RestoreTest.java | 96 - .../src/test/java/convex/peer/ServerTest.java | 289 -- .../test/java/convex/peer/TestNetwork.java | 85 - convex.bat | 3 - docs/coding-principles.md | 74 - docs/wip/ethos.md | 23 - docs/wip/governance.md | 19 - docs/wip/misc-faq.md | 16 - pom.xml | 207 - 552 files changed, 84566 deletions(-) delete mode 100644 .gitattributes delete mode 100644 .github/workflows/deploy_docs.yml delete mode 100644 .github/workflows/tests.yml delete mode 100644 .gitignore delete mode 100644 .mvn/maven.config delete mode 100644 BUILD.md delete mode 100644 CHANGELOG.md delete mode 100644 Dockerfile delete mode 100644 LICENSE.md delete mode 100644 README.md delete mode 100755 convex delete mode 100644 convex-benchmarks/.gitignore delete mode 100644 convex-benchmarks/README.md delete mode 100644 convex-benchmarks/pom.xml delete mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/Benchmarks.java delete mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/BigBlockBenchmark.java delete mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/CVMBenchmark.java delete mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/EtchBenchmark.java delete mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/EvalBenchmark.java delete mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/HashBenchmark.java delete mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/LatencyBenchmark.java delete mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/ListDataBenchmark.java delete mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/MapBenchmark.java delete mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/OpBenchmark.java delete mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/SignatureBenchmark.java delete mode 100644 convex-benchmarks/src/main/java/convex/benchmarks/ThreadCoordinationBenchmark.java delete mode 100644 convex-cli/.gitignore delete mode 100644 convex-cli/pom.xml delete mode 100644 convex-cli/src/docs/asciidoc/images/convex_logo.svg delete mode 100644 convex-cli/src/docs/asciidoc/index.adoc delete mode 100644 convex-cli/src/main/java/convex/cli/Account.java delete mode 100644 convex-cli/src/main/java/convex/cli/AccountBalance.java delete mode 100644 convex-cli/src/main/java/convex/cli/AccountCreate.java delete mode 100644 convex-cli/src/main/java/convex/cli/AccountFund.java delete mode 100644 convex-cli/src/main/java/convex/cli/AccountInformation.java delete mode 100644 convex-cli/src/main/java/convex/cli/Constants.java delete mode 100644 convex-cli/src/main/java/convex/cli/Helpers.java delete mode 100644 convex-cli/src/main/java/convex/cli/Key.java delete mode 100644 convex-cli/src/main/java/convex/cli/KeyExport.java delete mode 100644 convex-cli/src/main/java/convex/cli/KeyGenerate.java delete mode 100644 convex-cli/src/main/java/convex/cli/KeyImport.java delete mode 100644 convex-cli/src/main/java/convex/cli/KeyList.java delete mode 100644 convex-cli/src/main/java/convex/cli/Local.java delete mode 100644 convex-cli/src/main/java/convex/cli/LocalGUI.java delete mode 100644 convex-cli/src/main/java/convex/cli/LocalStart.java delete mode 100644 convex-cli/src/main/java/convex/cli/Main.java delete mode 100644 convex-cli/src/main/java/convex/cli/Peer.java delete mode 100644 convex-cli/src/main/java/convex/cli/PeerCreate.java delete mode 100644 convex-cli/src/main/java/convex/cli/PeerStart.java delete mode 100644 convex-cli/src/main/java/convex/cli/Query.java delete mode 100644 convex-cli/src/main/java/convex/cli/Status.java delete mode 100644 convex-cli/src/main/java/convex/cli/Transaction.java delete mode 100644 convex-cli/src/main/java/convex/cli/output/Output.java delete mode 100644 convex-cli/src/main/java/convex/cli/output/OutputField.java delete mode 100644 convex-cli/src/main/java/convex/cli/peer/PeerManager.java delete mode 100644 convex-cli/src/main/java/convex/cli/peer/Session.java delete mode 100644 convex-cli/src/main/java/convex/cli/peer/SessionItem.java delete mode 100644 convex-cli/src/test/java/convex/cli/CLICommandKeyExportTest.java delete mode 100644 convex-cli/src/test/java/convex/cli/CLICommandKeyImportTest.java delete mode 100644 convex-cli/src/test/java/convex/cli/CLICommandKeyTest.java delete mode 100644 convex-cli/src/test/java/convex/cli/CLIHelpTest.java delete mode 100644 convex-cli/src/test/java/convex/cli/CLIMainTest.java delete mode 100644 convex-cli/src/test/java/convex/cli/CommandLineTester.java delete mode 100644 convex-cli/src/test/java/convex/cli/Helper.java delete mode 100644 convex-cli/src/test/java/convex/cli/output/OutputTest.java delete mode 100644 convex-cli/src/test/java/convex/cli/peer/SessionTest.java delete mode 100644 convex-core/.gitignore delete mode 100644 convex-core/pom.xml delete mode 100644 convex-core/src/main/antlr4/convex/core/lang/reader/antlr/Convex.g4 delete mode 100644 convex-core/src/main/assembly/full.xml delete mode 100644 convex-core/src/main/assembly/testing.xml delete mode 100644 convex-core/src/main/cvx/asset/box.cvx delete mode 100644 convex-core/src/main/cvx/asset/box/actor.cvx delete mode 100644 convex-core/src/main/cvx/asset/nft/simple.cvx delete mode 100644 convex-core/src/main/cvx/asset/nft/tokens.cvx delete mode 100644 convex-core/src/main/cvx/convex/asset.cvx delete mode 100644 convex-core/src/main/cvx/convex/core.cvx delete mode 100644 convex-core/src/main/cvx/convex/core/metadata.cvx delete mode 100644 convex-core/src/main/cvx/convex/fungible.cvx delete mode 100644 convex-core/src/main/cvx/convex/play.cvx delete mode 100644 convex-core/src/main/cvx/convex/registry.cvx delete mode 100644 convex-core/src/main/cvx/convex/trust.cvx delete mode 100644 convex-core/src/main/cvx/convex/trusted-oracle.cvx delete mode 100644 convex-core/src/main/cvx/convex/trusted-oracle/actor.cvx delete mode 100644 convex-core/src/main/cvx/lab/convex/xform.cvx delete mode 100644 convex-core/src/main/cvx/lab/messenger.cvx delete mode 100644 convex-core/src/main/cvx/lab/prediction-market.cvx delete mode 100644 convex-core/src/main/cvx/lab/secured-loan.con delete mode 100644 convex-core/src/main/cvx/torus/currencies.cvx delete mode 100644 convex-core/src/main/cvx/torus/exchange.cvx delete mode 100644 convex-core/src/main/java/convex/core/Belief.java delete mode 100644 convex-core/src/main/java/convex/core/Block.java delete mode 100644 convex-core/src/main/java/convex/core/BlockResult.java delete mode 100644 convex-core/src/main/java/convex/core/Coin.java delete mode 100644 convex-core/src/main/java/convex/core/Constants.java delete mode 100644 convex-core/src/main/java/convex/core/ErrorCodes.java delete mode 100644 convex-core/src/main/java/convex/core/MergeContext.java delete mode 100644 convex-core/src/main/java/convex/core/Order.java delete mode 100644 convex-core/src/main/java/convex/core/Peer.java delete mode 100644 convex-core/src/main/java/convex/core/Result.java delete mode 100644 convex-core/src/main/java/convex/core/State.java delete mode 100644 convex-core/src/main/java/convex/core/crypto/AKeyPair.java delete mode 100644 convex-core/src/main/java/convex/core/crypto/ASignature.java delete mode 100644 convex-core/src/main/java/convex/core/crypto/Ed25519KeyPair.java delete mode 100644 convex-core/src/main/java/convex/core/crypto/Ed25519Signature.java delete mode 100644 convex-core/src/main/java/convex/core/crypto/Encoding.java delete mode 100644 convex-core/src/main/java/convex/core/crypto/Hashing.java delete mode 100644 convex-core/src/main/java/convex/core/crypto/InsecureRandom.java delete mode 100644 convex-core/src/main/java/convex/core/crypto/Mnemonic.java delete mode 100644 convex-core/src/main/java/convex/core/crypto/PBE.java delete mode 100644 convex-core/src/main/java/convex/core/crypto/PEMTools.java delete mode 100644 convex-core/src/main/java/convex/core/crypto/PFXTools.java delete mode 100644 convex-core/src/main/java/convex/core/crypto/Providers.java delete mode 100644 convex-core/src/main/java/convex/core/crypto/Symmetric.java delete mode 100644 convex-core/src/main/java/convex/core/crypto/Wallet.java delete mode 100644 convex-core/src/main/java/convex/core/crypto/WalletEntry.java delete mode 100644 convex-core/src/main/java/convex/core/data/AArrayBlob.java delete mode 100644 convex-core/src/main/java/convex/core/data/ABlob.java delete mode 100644 convex-core/src/main/java/convex/core/data/ABlobMap.java delete mode 100644 convex-core/src/main/java/convex/core/data/ACell.java delete mode 100644 convex-core/src/main/java/convex/core/data/ACollection.java delete mode 100644 convex-core/src/main/java/convex/core/data/ACountable.java delete mode 100644 convex-core/src/main/java/convex/core/data/ADataStructure.java delete mode 100644 convex-core/src/main/java/convex/core/data/AHashMap.java delete mode 100644 convex-core/src/main/java/convex/core/data/AHashSet.java delete mode 100644 convex-core/src/main/java/convex/core/data/AList.java delete mode 100644 convex-core/src/main/java/convex/core/data/ALongBlob.java delete mode 100644 convex-core/src/main/java/convex/core/data/AMap.java delete mode 100644 convex-core/src/main/java/convex/core/data/AMapEntry.java delete mode 100644 convex-core/src/main/java/convex/core/data/ANumericBlob.java delete mode 100644 convex-core/src/main/java/convex/core/data/AObject.java delete mode 100644 convex-core/src/main/java/convex/core/data/ARecord.java delete mode 100644 convex-core/src/main/java/convex/core/data/ARecordGeneric.java delete mode 100644 convex-core/src/main/java/convex/core/data/ASequence.java delete mode 100644 convex-core/src/main/java/convex/core/data/ASet.java delete mode 100644 convex-core/src/main/java/convex/core/data/AString.java delete mode 100644 convex-core/src/main/java/convex/core/data/ASymbolic.java delete mode 100644 convex-core/src/main/java/convex/core/data/AVector.java delete mode 100644 convex-core/src/main/java/convex/core/data/AccountKey.java delete mode 100644 convex-core/src/main/java/convex/core/data/AccountStatus.java delete mode 100644 convex-core/src/main/java/convex/core/data/Address.java delete mode 100644 convex-core/src/main/java/convex/core/data/Blob.java delete mode 100644 convex-core/src/main/java/convex/core/data/BlobMap.java delete mode 100644 convex-core/src/main/java/convex/core/data/BlobMaps.java delete mode 100644 convex-core/src/main/java/convex/core/data/BlobTree.java delete mode 100644 convex-core/src/main/java/convex/core/data/Blobs.java delete mode 100644 convex-core/src/main/java/convex/core/data/Format.java delete mode 100644 convex-core/src/main/java/convex/core/data/Hash.java delete mode 100644 convex-core/src/main/java/convex/core/data/IAssociative.java delete mode 100644 convex-core/src/main/java/convex/core/data/INumeric.java delete mode 100644 convex-core/src/main/java/convex/core/data/IRefFunction.java delete mode 100644 convex-core/src/main/java/convex/core/data/IValidated.java delete mode 100644 convex-core/src/main/java/convex/core/data/IWriteable.java delete mode 100644 convex-core/src/main/java/convex/core/data/Keyword.java delete mode 100644 convex-core/src/main/java/convex/core/data/Keywords.java delete mode 100644 convex-core/src/main/java/convex/core/data/List.java delete mode 100644 convex-core/src/main/java/convex/core/data/Lists.java delete mode 100644 convex-core/src/main/java/convex/core/data/LongBlob.java delete mode 100644 convex-core/src/main/java/convex/core/data/MapEntry.java delete mode 100644 convex-core/src/main/java/convex/core/data/MapLeaf.java delete mode 100644 convex-core/src/main/java/convex/core/data/MapTree.java delete mode 100644 convex-core/src/main/java/convex/core/data/Maps.java delete mode 100644 convex-core/src/main/java/convex/core/data/PeerStatus.java delete mode 100644 convex-core/src/main/java/convex/core/data/Ref.java delete mode 100644 convex-core/src/main/java/convex/core/data/RefDirect.java delete mode 100644 convex-core/src/main/java/convex/core/data/RefSoft.java delete mode 100644 convex-core/src/main/java/convex/core/data/SetLeaf.java delete mode 100644 convex-core/src/main/java/convex/core/data/SetTree.java delete mode 100644 convex-core/src/main/java/convex/core/data/Sets.java delete mode 100644 convex-core/src/main/java/convex/core/data/SignedData.java delete mode 100644 convex-core/src/main/java/convex/core/data/StringShort.java delete mode 100644 convex-core/src/main/java/convex/core/data/StringSlice.java delete mode 100644 convex-core/src/main/java/convex/core/data/StringTree.java delete mode 100644 convex-core/src/main/java/convex/core/data/Strings.java delete mode 100644 convex-core/src/main/java/convex/core/data/Symbol.java delete mode 100644 convex-core/src/main/java/convex/core/data/Syntax.java delete mode 100644 convex-core/src/main/java/convex/core/data/Tag.java delete mode 100644 convex-core/src/main/java/convex/core/data/VectorArray.java delete mode 100644 convex-core/src/main/java/convex/core/data/VectorLeaf.java delete mode 100644 convex-core/src/main/java/convex/core/data/VectorTree.java delete mode 100644 convex-core/src/main/java/convex/core/data/Vectors.java delete mode 100644 convex-core/src/main/java/convex/core/data/package-info.java delete mode 100644 convex-core/src/main/java/convex/core/data/prim/APrimitive.java delete mode 100644 convex-core/src/main/java/convex/core/data/prim/CVMBool.java delete mode 100644 convex-core/src/main/java/convex/core/data/prim/CVMByte.java delete mode 100644 convex-core/src/main/java/convex/core/data/prim/CVMChar.java delete mode 100644 convex-core/src/main/java/convex/core/data/prim/CVMDouble.java delete mode 100644 convex-core/src/main/java/convex/core/data/prim/CVMLong.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/ANumericType.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/AStandardType.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/AType.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/AddressType.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Any.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Blob.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/BlobMapType.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Boolean.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Byte.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/CharacterType.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Collection.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/DataStructure.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Double.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Function.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/KeywordType.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/List.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Long.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Map.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Nil.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Number.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/OpCode.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Record.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Sequence.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Set.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/StringType.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/SymbolType.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/SyntaxType.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Transaction.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Types.java delete mode 100644 convex-core/src/main/java/convex/core/data/type/Vector.java delete mode 100644 convex-core/src/main/java/convex/core/exceptions/BadFormatException.java delete mode 100644 convex-core/src/main/java/convex/core/exceptions/BadSignatureException.java delete mode 100644 convex-core/src/main/java/convex/core/exceptions/BaseException.java delete mode 100644 convex-core/src/main/java/convex/core/exceptions/InvalidDataException.java delete mode 100644 convex-core/src/main/java/convex/core/exceptions/MissingDataException.java delete mode 100644 convex-core/src/main/java/convex/core/exceptions/ParseException.java delete mode 100644 convex-core/src/main/java/convex/core/exceptions/TODOException.java delete mode 100644 convex-core/src/main/java/convex/core/exceptions/ValidationException.java delete mode 100644 convex-core/src/main/java/convex/core/init/AInitConfig.java delete mode 100644 convex-core/src/main/java/convex/core/init/Init.java delete mode 100644 convex-core/src/main/java/convex/core/lang/AFn.java delete mode 100644 convex-core/src/main/java/convex/core/lang/AOp.java delete mode 100644 convex-core/src/main/java/convex/core/lang/Compiler.java delete mode 100644 convex-core/src/main/java/convex/core/lang/Context.java delete mode 100644 convex-core/src/main/java/convex/core/lang/Core.java delete mode 100644 convex-core/src/main/java/convex/core/lang/IFn.java delete mode 100644 convex-core/src/main/java/convex/core/lang/Juice.java delete mode 100644 convex-core/src/main/java/convex/core/lang/Ops.java delete mode 100644 convex-core/src/main/java/convex/core/lang/RT.java delete mode 100644 convex-core/src/main/java/convex/core/lang/Reader.java delete mode 100644 convex-core/src/main/java/convex/core/lang/Symbols.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/AClosure.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/ADataFn.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/AExceptional.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/AReturn.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/ATrampoline.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/CoreFn.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/CorePred.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/ErrorValue.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/Fn.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/HaltValue.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/ICoreDef.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/KeywordFn.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/MapFn.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/MultiFn.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/RecordFormat.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/RecurValue.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/Reduced.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/ReturnValue.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/RollbackValue.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/SeqFn.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/SetFn.java delete mode 100644 convex-core/src/main/java/convex/core/lang/impl/TailcallValue.java delete mode 100644 convex-core/src/main/java/convex/core/lang/ops/AMultiOp.java delete mode 100644 convex-core/src/main/java/convex/core/lang/ops/Cond.java delete mode 100644 convex-core/src/main/java/convex/core/lang/ops/Constant.java delete mode 100644 convex-core/src/main/java/convex/core/lang/ops/Def.java delete mode 100644 convex-core/src/main/java/convex/core/lang/ops/Do.java delete mode 100644 convex-core/src/main/java/convex/core/lang/ops/Invoke.java delete mode 100644 convex-core/src/main/java/convex/core/lang/ops/Lambda.java delete mode 100644 convex-core/src/main/java/convex/core/lang/ops/Let.java delete mode 100644 convex-core/src/main/java/convex/core/lang/ops/Local.java delete mode 100644 convex-core/src/main/java/convex/core/lang/ops/Lookup.java delete mode 100644 convex-core/src/main/java/convex/core/lang/ops/Query.java delete mode 100644 convex-core/src/main/java/convex/core/lang/ops/Set.java delete mode 100644 convex-core/src/main/java/convex/core/lang/ops/Special.java delete mode 100644 convex-core/src/main/java/convex/core/lang/reader/AntlrReader.java delete mode 100644 convex-core/src/main/java/convex/core/lang/reader/ReaderUtils.java delete mode 100644 convex-core/src/main/java/convex/core/store/AStore.java delete mode 100644 convex-core/src/main/java/convex/core/store/BlobCache.java delete mode 100644 convex-core/src/main/java/convex/core/store/MemoryStore.java delete mode 100644 convex-core/src/main/java/convex/core/store/Stores.java delete mode 100644 convex-core/src/main/java/convex/core/transactions/ATransaction.java delete mode 100644 convex-core/src/main/java/convex/core/transactions/Call.java delete mode 100644 convex-core/src/main/java/convex/core/transactions/Invoke.java delete mode 100644 convex-core/src/main/java/convex/core/transactions/Transfer.java delete mode 100644 convex-core/src/main/java/convex/core/util/Bits.java delete mode 100644 convex-core/src/main/java/convex/core/util/Counters.java delete mode 100644 convex-core/src/main/java/convex/core/util/Economics.java delete mode 100644 convex-core/src/main/java/convex/core/util/Errors.java delete mode 100644 convex-core/src/main/java/convex/core/util/Huge.java delete mode 100644 convex-core/src/main/java/convex/core/util/MergeFunction.java delete mode 100644 convex-core/src/main/java/convex/core/util/Shutdown.java delete mode 100644 convex-core/src/main/java/convex/core/util/Text.java delete mode 100644 convex-core/src/main/java/convex/core/util/UMath.java delete mode 100644 convex-core/src/main/java/convex/core/util/Utils.java delete mode 100644 convex-core/src/main/java/etch/Etch.java delete mode 100644 convex-core/src/main/java/etch/EtchStore.java delete mode 100644 convex-core/src/test/cvx/test/asset/box.cvx delete mode 100644 convex-core/src/test/cvx/test/asset/nft/simple.cvx delete mode 100644 convex-core/src/test/cvx/test/asset/nft/tokens.cvx delete mode 100644 convex-core/src/test/cvx/test/convex/asset.cvx delete mode 100644 convex-core/src/test/cvx/test/convex/asset/quantity/set-long.cvx delete mode 100644 convex-core/src/test/cvx/test/convex/fungible.cvx delete mode 100644 convex-core/src/test/cvx/test/convex/fungible/generic.cvx delete mode 100644 convex-core/src/test/cvx/test/convex/play.cvx delete mode 100644 convex-core/src/test/cvx/test/convex/registry.cvx delete mode 100644 convex-core/src/test/cvx/test/convex/trust.cvx delete mode 100644 convex-core/src/test/cvx/test/convex/trusted-oracle.cvx delete mode 100644 convex-core/src/test/cvx/test/torus/exchange.cvx delete mode 100644 convex-core/src/test/java/convex/actors/ActorsTest.java delete mode 100644 convex-core/src/test/java/convex/actors/PredictionMarketTest.java delete mode 100644 convex-core/src/test/java/convex/actors/RegistryTest.java delete mode 100644 convex-core/src/test/java/convex/actors/TorusTest.java delete mode 100644 convex-core/src/test/java/convex/comms/GenTestFormat.java delete mode 100644 convex-core/src/test/java/convex/comms/VLCEncodingTest.java delete mode 100644 convex-core/src/test/java/convex/comms/VLCParamTest.java delete mode 100644 convex-core/src/test/java/convex/core/BeliefMergeTest.java delete mode 100644 convex-core/src/test/java/convex/core/BeliefVotingTest.java delete mode 100644 convex-core/src/test/java/convex/core/MessageSizeTest.java delete mode 100644 convex-core/src/test/java/convex/core/PeerTest.java delete mode 100644 convex-core/src/test/java/convex/core/ResultTest.java delete mode 100644 convex-core/src/test/java/convex/core/StakingTest.java delete mode 100644 convex-core/src/test/java/convex/core/StateTest.java delete mode 100644 convex-core/src/test/java/convex/core/StateTransitionsTest.java delete mode 100644 convex-core/src/test/java/convex/core/TransactionTest.java delete mode 100644 convex-core/src/test/java/convex/core/crypto/AccountKeyTest.java delete mode 100644 convex-core/src/test/java/convex/core/crypto/Ed25519Test.java delete mode 100644 convex-core/src/test/java/convex/core/crypto/HashTest.java delete mode 100644 convex-core/src/test/java/convex/core/crypto/MnemonicTest.java delete mode 100644 convex-core/src/test/java/convex/core/crypto/PBETest.java delete mode 100644 convex-core/src/test/java/convex/core/crypto/PEMToolsTest.java delete mode 100644 convex-core/src/test/java/convex/core/crypto/PFXTest.java delete mode 100644 convex-core/src/test/java/convex/core/crypto/ParamTestHash.java delete mode 100644 convex-core/src/test/java/convex/core/crypto/SignKeyPairTest.java delete mode 100644 convex-core/src/test/java/convex/core/crypto/SymmetricTest.java delete mode 100644 convex-core/src/test/java/convex/core/crypto/WalletTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/AccountKeyTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/AddressTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/BlobMapsTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/BlobsTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/BlocksTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/CollectionsTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/EncodingTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/FuzzTestFormat.java delete mode 100644 convex-core/src/test/java/convex/core/data/GenTestAnyValue.java delete mode 100644 convex-core/src/test/java/convex/core/data/GenTestBlobs.java delete mode 100644 convex-core/src/test/java/convex/core/data/GenTestDataStructures.java delete mode 100644 convex-core/src/test/java/convex/core/data/GenTestMap.java delete mode 100644 convex-core/src/test/java/convex/core/data/GenTestMessages.java delete mode 100644 convex-core/src/test/java/convex/core/data/GenTestStrings.java delete mode 100644 convex-core/src/test/java/convex/core/data/GenTestVectors.java delete mode 100644 convex-core/src/test/java/convex/core/data/KeywordTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/ListsTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/MapsTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/ObjectsTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/ParamTestBlobs.java delete mode 100644 convex-core/src/test/java/convex/core/data/ParamTestOps.java delete mode 100644 convex-core/src/test/java/convex/core/data/ParamTestRefs.java delete mode 100644 convex-core/src/test/java/convex/core/data/ParamTestValues.java delete mode 100644 convex-core/src/test/java/convex/core/data/ParamTestVector.java delete mode 100644 convex-core/src/test/java/convex/core/data/RecordTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/RefTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/SetsTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/SignedDataTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/StreamsTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/StringsTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/SymbolTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/SyntaxTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/TreeVectorTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/VectorsTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/prim/ByteTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/prim/LongTest.java delete mode 100644 convex-core/src/test/java/convex/core/data/type/TypesTest.java delete mode 100644 convex-core/src/test/java/convex/core/init/InitTest.java delete mode 100644 convex-core/src/test/java/convex/core/lang/ACVMTest.java delete mode 100644 convex-core/src/test/java/convex/core/lang/AliasTest.java delete mode 100644 convex-core/src/test/java/convex/core/lang/CompilerTest.java delete mode 100644 convex-core/src/test/java/convex/core/lang/ContextTest.java delete mode 100644 convex-core/src/test/java/convex/core/lang/CoreTest.java delete mode 100644 convex-core/src/test/java/convex/core/lang/DataStructuresTest.java delete mode 100644 convex-core/src/test/java/convex/core/lang/DocsTest.java delete mode 100644 convex-core/src/test/java/convex/core/lang/GenTestCode.java delete mode 100644 convex-core/src/test/java/convex/core/lang/GenTestCore.java delete mode 100644 convex-core/src/test/java/convex/core/lang/GenTestRT.java delete mode 100644 convex-core/src/test/java/convex/core/lang/JuiceTest.java delete mode 100644 convex-core/src/test/java/convex/core/lang/NumericsTest.java delete mode 100644 convex-core/src/test/java/convex/core/lang/OpsTest.java delete mode 100644 convex-core/src/test/java/convex/core/lang/ParamTestCasts.java delete mode 100644 convex-core/src/test/java/convex/core/lang/ParamTestEvals.java delete mode 100644 convex-core/src/test/java/convex/core/lang/ParamTestJuice.java delete mode 100644 convex-core/src/test/java/convex/core/lang/ParamTestRTSequences.java delete mode 100644 convex-core/src/test/java/convex/core/lang/RTTest.java delete mode 100644 convex-core/src/test/java/convex/core/lang/ReaderTest.java delete mode 100644 convex-core/src/test/java/convex/core/lang/SyntaxTest.java delete mode 100644 convex-core/src/test/java/convex/core/lang/TestState.java delete mode 100644 convex-core/src/test/java/convex/core/lang/reader/ANTLRTest.java delete mode 100644 convex-core/src/test/java/convex/core/util/EconomicsTest.java delete mode 100644 convex-core/src/test/java/convex/core/util/GenTestEconomics.java delete mode 100644 convex-core/src/test/java/convex/lib/AssetTest.java delete mode 100644 convex-core/src/test/java/convex/lib/FungibleTest.java delete mode 100644 convex-core/src/test/java/convex/lib/SimpleNFTTest.java delete mode 100644 convex-core/src/test/java/convex/lib/TrustTest.java delete mode 100644 convex-core/src/test/java/convex/store/EtchInitTest.java delete mode 100644 convex-core/src/test/java/convex/store/EtchStoreTest.java delete mode 100644 convex-core/src/test/java/convex/store/MemoryStoreTest.java delete mode 100644 convex-core/src/test/java/convex/store/ParamTestStores.java delete mode 100644 convex-core/src/test/java/convex/test/Assertions.java delete mode 100644 convex-core/src/test/java/convex/test/Samples.java delete mode 100644 convex-core/src/test/java/convex/test/Testing.java delete mode 100644 convex-core/src/test/java/convex/test/generators/AddressGen.java delete mode 100644 convex-core/src/test/java/convex/test/generators/AnyMapGen.java delete mode 100644 convex-core/src/test/java/convex/test/generators/BlobGen.java delete mode 100644 convex-core/src/test/java/convex/test/generators/CollectionGen.java delete mode 100644 convex-core/src/test/java/convex/test/generators/DataStructureGen.java delete mode 100644 convex-core/src/test/java/convex/test/generators/FormGen.java delete mode 100644 convex-core/src/test/java/convex/test/generators/KeywordGen.java delete mode 100644 convex-core/src/test/java/convex/test/generators/ListGen.java delete mode 100644 convex-core/src/test/java/convex/test/generators/MapGen.java delete mode 100644 convex-core/src/test/java/convex/test/generators/NumericGen.java delete mode 100644 convex-core/src/test/java/convex/test/generators/PrimitiveGen.java delete mode 100644 convex-core/src/test/java/convex/test/generators/RecordGen.java delete mode 100644 convex-core/src/test/java/convex/test/generators/SetGen.java delete mode 100644 convex-core/src/test/java/convex/test/generators/StringGen.java delete mode 100644 convex-core/src/test/java/convex/test/generators/TransactionGen.java delete mode 100644 convex-core/src/test/java/convex/test/generators/ValueGen.java delete mode 100644 convex-core/src/test/java/convex/test/generators/VectorGen.java delete mode 100644 convex-core/src/test/java/convex/util/BigIntegerParamTest.java delete mode 100644 convex-core/src/test/java/convex/util/BitsTest.java delete mode 100644 convex-core/src/test/java/convex/util/GenTestHuge.java delete mode 100644 convex-core/src/test/java/convex/util/GenTestUMath.java delete mode 100644 convex-core/src/test/java/convex/util/HugeTest.java delete mode 100644 convex-core/src/test/java/convex/util/TextTest.java delete mode 100644 convex-core/src/test/java/convex/util/UMathTest.java delete mode 100644 convex-core/src/test/java/convex/util/UtilsTest.java delete mode 100644 convex-core/src/test/java/etch/api/TestEtch.java delete mode 100644 convex-core/src/test/resources/contracts/box/test1.con delete mode 100644 convex-core/src/test/resources/contracts/deposit-box.con delete mode 100644 convex-core/src/test/resources/contracts/exceptional.con delete mode 100644 convex-core/src/test/resources/contracts/funding.con delete mode 100644 convex-core/src/test/resources/contracts/hello.con delete mode 100644 convex-core/src/test/resources/contracts/nft/simple-nft-test.con delete mode 100644 convex-core/src/test/resources/contracts/token.con delete mode 100644 convex-core/src/test/resources/examples/adventure.cvx delete mode 100644 convex-core/src/test/resources/junit-platform.properties delete mode 100644 convex-core/src/test/resources/testsource/min.con delete mode 100644 convex-gui/.gitignore delete mode 100644 convex-gui/pom.xml delete mode 100644 convex-gui/src/main/assembly/full.xml delete mode 100644 convex-gui/src/main/assembly/testing.xml delete mode 100644 convex-gui/src/main/java/convex/gui/client/ConvexClient.java delete mode 100644 convex-gui/src/main/java/convex/gui/client/panels/HomePanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/AccountChooserPanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/ActionPanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/BaseListComponent.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/BlockViewComponent.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/CodeLabel.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/DefaultReceiveAction.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/DropdownMenu.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/Identicon.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/PeerComponent.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/PeerView.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/ScrollyList.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/Toast.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/UnlockWalletDialog.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/WalletComponent.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/WorldPanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/models/AccountsTableModel.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/models/OracleTableModel.java delete mode 100644 convex-gui/src/main/java/convex/gui/components/models/StateModel.java delete mode 100644 convex-gui/src/main/java/convex/gui/etch/EtchExplorer.java delete mode 100644 convex-gui/src/main/java/convex/gui/etch/panels/DatabasePanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/PeerGUI.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/AboutPanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/AccountsPanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/ActorsPanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/DeployPanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/HomePanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/KeyGenPanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/MessageFormatPanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/PeersListPanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/WalletPanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/DeployPanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/MarketComponent.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/MarketsPanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/OraclePanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/BaseWindow.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorInfoPanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorInvokePanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorWindow.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/actor/ArgBox.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/actor/ParamLabel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/actor/SmartOpComponent.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/etch/EtchWindow.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/peer/PeerWindow.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/peer/REPLPanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/peer/StressPanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/state/StateTreeNode.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/state/StateTreePanel.java delete mode 100644 convex-gui/src/main/java/convex/gui/manager/windows/state/StateWindow.java delete mode 100644 convex-gui/src/main/java/convex/gui/utils/RobinsonProjection.java delete mode 100644 convex-gui/src/main/java/convex/gui/utils/Toolkit.java delete mode 100644 convex-gui/src/main/java/convex/wallet/Wallet.java delete mode 100644 convex-gui/src/main/resources/images/Convex.png delete mode 100644 convex-gui/src/main/resources/images/cog.png delete mode 100644 convex-gui/src/main/resources/images/ic_cake_black_36dp.png delete mode 100644 convex-gui/src/main/resources/images/ic_lock_open_black_36dp.png delete mode 100644 convex-gui/src/main/resources/images/ic_lock_outline_black_36dp.png delete mode 100644 convex-gui/src/main/resources/images/ic_priority_high_black_36dp.png delete mode 100644 convex-gui/src/main/resources/images/ic_stars_black_36dp.png delete mode 100644 convex-gui/src/main/resources/images/padlock-open.png delete mode 100644 convex-gui/src/main/resources/images/padlock.png delete mode 100644 convex-gui/src/main/resources/images/world.png delete mode 100644 convex-gui/src/test/java/convex/gui/GUITest.java delete mode 100644 convex-peer/.gitignore delete mode 100644 convex-peer/pom.xml delete mode 100644 convex-peer/src/main/java/convex/api/Applications.java delete mode 100644 convex-peer/src/main/java/convex/api/Convex.java delete mode 100644 convex-peer/src/main/java/convex/net/Connection.java delete mode 100644 convex-peer/src/main/java/convex/net/MemoryByteChannel.java delete mode 100644 convex-peer/src/main/java/convex/net/Message.java delete mode 100644 convex-peer/src/main/java/convex/net/MessageReceiver.java delete mode 100644 convex-peer/src/main/java/convex/net/MessageSender.java delete mode 100644 convex-peer/src/main/java/convex/net/MessageType.java delete mode 100644 convex-peer/src/main/java/convex/net/NIOServer.java delete mode 100644 convex-peer/src/main/java/convex/net/ResultConsumer.java delete mode 100644 convex-peer/src/main/java/convex/peer/API.java delete mode 100644 convex-peer/src/main/java/convex/peer/ChallengeRequest.java delete mode 100644 convex-peer/src/main/java/convex/peer/ConnectionManager.java delete mode 100644 convex-peer/src/main/java/convex/peer/IServerEvent.java delete mode 100644 convex-peer/src/main/java/convex/peer/Server.java delete mode 100644 convex-peer/src/main/java/convex/peer/ServerEvent.java delete mode 100644 convex-peer/src/main/java/convex/peer/ServerInformation.java delete mode 100644 convex-peer/src/test/java/convex/api/ConvexTest.java delete mode 100644 convex-peer/src/test/java/convex/api/TestApplications.java delete mode 100644 convex-peer/src/test/java/convex/examples/AcquireState.java delete mode 100644 convex-peer/src/test/java/convex/examples/ClientApp.java delete mode 100644 convex-peer/src/test/java/convex/examples/Ed25519Sign.java delete mode 100644 convex-peer/src/test/java/convex/examples/JoinTestNetwork.java delete mode 100644 convex-peer/src/test/java/convex/examples/PeerCluster.java delete mode 100644 convex-peer/src/test/java/convex/examples/SigSamples.java delete mode 100644 convex-peer/src/test/java/convex/net/ConnectionTest.java delete mode 100644 convex-peer/src/test/java/convex/net/MemoryByteChannelTest.java delete mode 100644 convex-peer/src/test/java/convex/peer/MessageReceiverTest.java delete mode 100644 convex-peer/src/test/java/convex/peer/MessageTest.java delete mode 100644 convex-peer/src/test/java/convex/peer/RestoreTest.java delete mode 100644 convex-peer/src/test/java/convex/peer/ServerTest.java delete mode 100644 convex-peer/src/test/java/convex/peer/TestNetwork.java delete mode 100644 convex.bat delete mode 100644 docs/coding-principles.md delete mode 100644 docs/wip/ethos.md delete mode 100644 docs/wip/governance.md delete mode 100644 docs/wip/misc-faq.md delete mode 100644 pom.xml diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 27d765118..000000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.cvx linguist-language=clojure diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml deleted file mode 100644 index 399638f14..000000000 --- a/.github/workflows/deploy_docs.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Deploy Docs -on: [push] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 - with: - java-version: '11.0.5' - - name: Cache Maven packages - uses: actions/cache@v2 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Run tests - run: mvn clean generate-resources - - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@4.1.4 - with: - branch: gh-pages - folder: convex-cli/target/html - target-folder: convex-cli/ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 77cbad02e..000000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: tests -on: [push] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 - with: - java-version: '15.0.2' - - name: Cache Maven packages - uses: actions/cache@v2 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Run tests - run: mvn clean test diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 267e6647a..000000000 --- a/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -*.jar -*.class -*.jfr -*.iml -/lib/ -/classes/ -target/ -/checkouts/ -.nrepl-port -.cpcache/ -/.project -/.classpath -/.settings/ -/.apt_generated/ -/myrecording.jfr -/.apt_generated_tests/ -**/.factorypath -/etch-db -/bin/ -.idea/ -.vscode/ -.DS_Store -/peers-shared-db -.metadata -/pom.xml.versionsBackup diff --git a/.mvn/maven.config b/.mvn/maven.config deleted file mode 100644 index 714163dc7..000000000 --- a/.mvn/maven.config +++ /dev/null @@ -1 +0,0 @@ --Drevision=0.7.0-SNAPSHOT \ No newline at end of file diff --git a/BUILD.md b/BUILD.md deleted file mode 100644 index 188dc8010..000000000 --- a/BUILD.md +++ /dev/null @@ -1,25 +0,0 @@ -# Build instructions - -## Overview - -Convex min repository is structured as a multi-module Maven project. - -## Build and test - -``` -mvn clean install -``` - -## Release preparation - -Set version information - -``` -mvn versions:set -DnewVersion=0.7.0-rc3 -``` - -Build and deploy - -``` -mvn clean deploy -DperformRelease -``` \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index b24b2ff04..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,13 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -## [0.7.0] - 2020-06-30 -### Added -- Initial Public Alpha release - - diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index c380358e7..000000000 --- a/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -# Docker for Convex CLI - -FROM maven:3-openjdk-15 - -ENV HOME=/home/convex - -WORKDIR $HOME - -ADD . $HOME - -RUN mvn clean package diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index e3e50106d..000000000 --- a/LICENSE.md +++ /dev/null @@ -1,132 +0,0 @@ -LICENSE DRAFT - -Convex Public Licence v0.1 (WIP DRAFT) - -# Preamble - -This license is intended to support the open development of software within the Convex ecosystem, with two primary objectives: - -A) Enable Open Source development and usage of systems based on Convex technology -B) Ensure effective governance of economic systems and networks based on Convex technology - -As such the license has been written based on the following principles: - -- It is a "weak Copyleft" license, similar in spirit to the Eclipse Public License. Derivative Works must be released under the same license. However, you may freely link and utilise the Work from software using a different license. -- When used to manage assets in a decentralised system, the Convex license places a requirement to operate the system according to the Convex Network Governance Rules. This is primarily done to prevent unauthorised forking, which would be contrary to the objective of Convex to support open, shared global public networks for the Internet of Value - - -==== MODIFIED FROM APACHE 2.0 LICENSE - -# Definitions. - -"Licence" shall mean the terms and conditions for use, reproduction, and distribution as defined by this document. - -"Licensor" shall mean the copyright owner or entity authorised by the copyright owner that is granting the Licence. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorised to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - - - -"Legal Entity" shall mean any of: - 1. An individual - 2. A legally established company or organisation in any jurisdiction - 3. The union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - 4. A decentralised entity operating either autonomously or under the control of one or more Legal Entities. - - - -"Distribute" means the acts of a) distributing or b) making available in any manner that enables the transfer of a copy. - -==== Extra definitions - TBC - -"Economic Assets" shall mean entities with economic or commercial value, or from which economic or commercial value may be derived, through the exercise of partial or exclusive control. An Economic Asset may be entirely digital, a legal right, an asset that exists in the physical world, or any combination of these. - -"Decentralised Economic System" shall mean any system where any of the following are true: -- Consensus over some information of economic interest is determined between two or more Legal Entities -- Economic Assets are under the control of two or more Legal Entities -- Transactions are executed that represent exchange of value between two or more Legal Entities - -"Convex Network" shall mean the union of all systems using the Work as part of a public utility network or operating a Decentralised Economic System. - -"Originator" shall mean the original author of the Convex software. - -"Governance Body" shall mean the the Legal Entity legally authorised by the Originator to administer governance decisions for the Convex Network, which as of June 2021 is the Convex Foundation. - -"Convex Network Governance Rules" shall mean the set of rules established and updated by the governance body for the secure, equitable and efficient operation of the Convex Network - -# License Terms - - - -## 1. Grant of Copyright License. - -Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, and distribute the Work and such Derivative Works in Source or Object form. - - - -## 2. Grant of Patent Licence. - -Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - - - -## 3. Distribution - -You may distribute the Work (or Derivative Work) under this Licence, provided that: - - 1. The Work (or Derivative Work) must also be made available as Source Code, and You must accompany the Program with a statement that the Source Code for the Program is available under this Agreement, and informs Recipients how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. - 2. The Work (or Derivative Work) must be distributed either under this license, or a later version of this license formally approved by the Governance Body. - - - - -## 4. Governance - -If you utilise the Work in a Decentralised Economic System, you must ensure that this system is operated in accordance with the Convex Network Governance Rules. This term applies whether or not you have made any modifications to the Work. - - - - -## 5. Submission of Contributions. - -Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this Licence, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -## 6. Trademarks. - -This Licence does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -## 7. Disclaimer of Warranty. - -Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this Licence. - -## 8. Limitation of Liability. - -In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -## 9. Accepting Warranty or Additional Liability. - -While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this Licence. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -<-- Extra clause for license updates --> - -## 10. Relicensing. - -Contributor grants to the Governing Body a perpetual, worldwide, no-charge, royalty-free, irrevocable right to re-distribute contributions under any future revision of this license approved by the Governing Body. The Convex Foundation will ensure that the main Convex distribution is always free and open source for anyone to use with the Convex Network. - -<--- Licence copyright ---> - -# Copyright - -Copyright The Originator 2018-2021. Includes terms adapted from the Apache 2.0 License and Eclipse Public License v2.0 \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 8710ee2a2..000000000 --- a/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# Convex - -[![Maven Central](https://img.shields.io/maven-central/v/world.convex/convex.svg?label=Maven%20Central)](https://search.maven.org/artifact/world.convex) - -Convex is a decentralised network and execution engine for the Internet of Value. - -It is designed as a full stack solution for decentralised application and economic systems that manage digital assets, where asset ownership is cryptographically secured and can be managed (optionally) with Smart Contracts. It can be considered functionally similar to a decentralised public blockchain, but offers some significant advantages: - -- High transaction throughput (tens of thousands of write transactions per second, potentially scaling to millions) -- Low latency for transaction confirmation (milliseconds for global consensus, depending on network speed) -- 100% Green - energy efficiency using the the Convergent Proof of Stake consensus algorithm -- Global State model with immutable data structures and atomic transactions -- Lambda Calculus based VM supporting Turing complete Smart Contracts -- Integrated on-chain compiler (Convex Lisp) - -## About this repository - -This repository contains the core Convex distribution including: - -- The Convex Virtual Machine (CVM) including data structures and execution environment -- The standard Convex Peer server implementation (NIO based) implementing Convergent Proof of Stake (CPoS) for consensus -- CLI Tools for operating Peers, scripting transactions and more -- The Etch database for persistent data storage -- A Swing GUI for managing local peers / exploring the network -- JMH Benchmarking suite -- Java Client API - -The repository also contains core "on-chain" libraries providing key full-stack functionality and tools for decentralised applications, including: - -- Fungible Tokens -- Non-fungible tokens -- `convex.asset` - library for managing arbitrary digital assets using a common abstraction -- `convex.trust` - library for access control and trusted operations -- `torus.exchange` - decentralised exchange for trading fungible tokens and currencies -- Example code and templates for various forms of smart contracts - -## Key features - -* *Virtual Machine* - The Convex Virtual Machine provides a secure execution environment based on the Lambda Calculus and capable of acting as the execution layer for smart contracts and autonomous agents. -* *Decentralised Consensus* - Similar to Blockchain technology, Convex incorporates a consensus mechanism that ensures all nodes ultimately agree on true values in the system without the control of any single entity. This property means that it is inherently tamper-proof and censorship-resistant. -* *Performance and Scalability* - Convex is capable of executing large volumes of transactions (tens of thousands of transactions per second) with low latency (sub-second global consensus) -* *100% Green* - No wasteful consumption of energy or computing resources - -## Running Convex - -### Command Line Interface (CLI) - -For more information about running a Convex Peer and the Command Line Interface see the [documentation](https://billbsing.github.io/convex/convex-cli/ -) - -### Local GUI Peers - -The convex Peer Manager (GUI application) can be used to run a local test network. - -This can be invoked by running the jar archive directly e.g. with the following command: - -`java -jar convex-gui/target/convex-gui-0.7.0-SNAPSHOT-jar-with-dependencies.jar` - -or you can run this from the command line by using the `local gui` command: - -``` -./convex local gui -``` - - -## Contributing - -Open Source contributions are welcome under the terms of the Convex Public License. Contributors retain copyright to their work, but must accept the terms of the license. - -We are planning to institute a Contributors Agreement for all contributions to the core Convex repository. - -The Convex Foundation may, at its sole discretion, award contributors with Convex Coins as recognition of value contributed to the Convex ecosystem. Convex coins are the native coin of the Convex network, and function as a utility token that provides the right to make use of the services of the network. Convex coins may be exchangeable for other digital assets and currencies. - -## Community - -We use Discord as the primary means for discussing Convex - you can join the public server at [https://discord.gg/5j2mPsk](https://discord.gg/5j2mPsk) - -Alternatively, email: info(at)convex.world - -## Copyright - -Copyright 2017-2021 The Convex Foundation diff --git a/convex b/convex deleted file mode 100755 index 4e193e861..000000000 --- a/convex +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -BASE_PATH=$(dirname $0) -JAR_NAME=`ls -1 $BASE_PATH/convex-cli/target/convex-cli.jar` - -# echo "using: $JAR_NAME" - -java -jar $JAR_NAME $@ diff --git a/convex-benchmarks/.gitignore b/convex-benchmarks/.gitignore deleted file mode 100644 index acd9eef61..000000000 --- a/convex-benchmarks/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/.settings/ -/.classpath -/.project -/pom.xml.versionsBackup diff --git a/convex-benchmarks/README.md b/convex-benchmarks/README.md deleted file mode 100644 index f4e185ca6..000000000 --- a/convex-benchmarks/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Convex Benchmarks - -## Benchmarking - -Convex includes a wide set of benchmarks, which are used to evaluate performance enhancements. These are mostly implemented with the JMH framework, and reside in the `convex.benchmarks` package. - -## Preparing to run benchmarks - -To run benchmarks, it is best to build the full `convex-benchmarks` jar with dependencies which includes all benchmarks, tests and dependencies. This can be done with the following commend: - -`mvn clean install` - -## Directly running benchmarks - -After building the testing `.jar`, you can launch benchmarks as main classes in the `convex.benchmarks` package, e.g. - -``` -java -cp target/convex-benchmarks-jar-with-dependencies.jar convex.benchmarks.EtchBenchmark -``` - -## Running with Java Flight Recorder - -If you want to analyse profiling results for the benchmarks, you can run using JFR to produce a profiling output file `flight.jfr` - -``` -java -cp target/convex-benchmarks-jar-with-dependencies.jar -XX:+FlightRecorder -XX:StartFlightRecording=duration=200s,filename=flight.jfr convex.benchmarks.CVMBenchmark -``` - -The resulting `flight.jfr` can the be opened in tools such as JDK Mission Control which enables detailed analysis and visualisation of profiling results. This is a useful approach that the Convex team use to identify performance bottlenecks. - -## Benchmark results - -After running benchmarks, you should see results similar to this: - -``` -Benchmark Mode Cnt Score Error Units -EtchBenchmark.readDataRandom thrpt 5 4848620.857 ± 110622.054 ops/s -EtchBenchmark.writeData thrpt 5 728486.145 ± 168739.491 ops/s -``` - -For example, this can be interpreted as an indication that the Etch database layer is handling approximately 4.8 million reads and 729k million atomic writes per second in the testing environment. Usual benchmarking caveats apply and results may vary considerably based on your system setup (available RAM, disk performance etc.) - it is advisable to examine the benchmark source to determine precisely which operations are being performed. diff --git a/convex-benchmarks/pom.xml b/convex-benchmarks/pom.xml deleted file mode 100644 index c33afcc52..000000000 --- a/convex-benchmarks/pom.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - world.convex - convex - 0.7.0-rc3 - - - 4.0.0 - - convex-benchmarks - - Convex Benchmarks - Convex Benchmark Suite - https://convex.world - - - convex-benchmarks - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - 11 - - - org.openjdk.jmh - jmh-generator-annprocess - ${jmh.version} - - - - - - - maven-assembly-plugin - 3.3.0 - - ${project.directory} - - - convex.cli.Main - - - - jar-with-dependencies - - - - - - create-archive - package - - single - - - - - - - - - - world.convex - convex-peer - ${convex.version} - - - org.openjdk.jmh - jmh-core - ${jmh.version} - - - org.openjdk.jmh - jmh-generator-annprocess - ${jmh.version} - true - - - - diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/Benchmarks.java b/convex-benchmarks/src/main/java/convex/benchmarks/Benchmarks.java deleted file mode 100644 index 5f3494de0..000000000 --- a/convex-benchmarks/src/main/java/convex/benchmarks/Benchmarks.java +++ /dev/null @@ -1,55 +0,0 @@ -package convex.benchmarks; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.stream.Collectors; - -import org.openjdk.jmh.runner.options.Options; -import org.openjdk.jmh.runner.options.OptionsBuilder; -import org.openjdk.jmh.runner.options.TimeValue; - -import convex.core.State; -import convex.core.crypto.AKeyPair; -import convex.core.data.AccountKey; -import convex.core.data.Address; -import convex.core.init.Init; -import convex.core.lang.Context; - -public class Benchmarks { - - public static final AKeyPair[] KEYPAIRS = new AKeyPair[] { - AKeyPair.createSeeded(2), - AKeyPair.createSeeded(3), - AKeyPair.createSeeded(5), - AKeyPair.createSeeded(7), - AKeyPair.createSeeded(11), - AKeyPair.createSeeded(13), - AKeyPair.createSeeded(17), - AKeyPair.createSeeded(19), - }; - - public static ArrayList PEER_KEYPAIRS=(ArrayList) Arrays.asList(KEYPAIRS).stream().collect(Collectors.toList()); - public static ArrayList PEER_KEYS=(ArrayList) Arrays.asList(KEYPAIRS).stream().map(kp->kp.getAccountKey()).collect(Collectors.toList()); - - public static final AKeyPair FIRST_PEER_KEYPAIR = KEYPAIRS[0]; - public static final AccountKey FIRST_PEER_KEY = FIRST_PEER_KEYPAIR.getAccountKey(); - - public static final AKeyPair HERO_KEYPAIR = KEYPAIRS[0]; - public static final AKeyPair VILLAIN_KEYPAIR = KEYPAIRS[1]; - - public static Address HERO=Init.getGenesisAddress(); - public static Address VILLAIN=HERO.offset(2); - - public static final AccountKey HERO_KEY = HERO_KEYPAIR.getAccountKey(); - - public static final State STATE = Init.createState(PEER_KEYS); - - static Options createOptions(Class c) { - return new OptionsBuilder().include(c.getSimpleName()).warmupIterations(1).measurementIterations(5) - .warmupTime(TimeValue.seconds(1)).measurementTime(TimeValue.seconds(1)).forks(0).build(); - } - - public static Context context() { - return Context.createFake(STATE,HERO); - } -} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/BigBlockBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/BigBlockBenchmark.java deleted file mode 100644 index 2dc14120c..000000000 --- a/convex-benchmarks/src/main/java/convex/benchmarks/BigBlockBenchmark.java +++ /dev/null @@ -1,65 +0,0 @@ -package convex.benchmarks; - -import java.util.ArrayList; -import java.util.Random; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; - -import convex.core.Block; -import convex.core.BlockResult; -import convex.core.State; -import convex.core.crypto.AKeyPair; -import convex.core.crypto.Ed25519KeyPair; -import convex.core.data.ACell; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.core.data.SignedData; -import convex.core.exceptions.BadSignatureException; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Transfer; - -public class BigBlockBenchmark { - - static final int NUM_ACCOUNTS = 1000; - static final int NUM_TRANSACTIONS = 1000; - private static final long INITIAL_FUNDS = 1000000000; - static ArrayList keyPairs = new ArrayList(); - static ArrayList
addresses = new ArrayList
(); - public static State state = Benchmarks.STATE; - public static Block block; - static ArrayList> transactions = new ArrayList>(); - - static { - for (int i = 0; i < NUM_ACCOUNTS; i++) { - AKeyPair kp = Ed25519KeyPair.generate(); - keyPairs.add(kp); - - // Create synthetic accounts - Address a=state.nextAddress(); - state = state.putAccount(a, (AccountStatus.create(INITIAL_FUNDS,kp.getAccountKey()))); - addresses.add(a); - } - for (int i = 0; i < NUM_TRANSACTIONS; i++) { - int src=new Random().nextInt(NUM_ACCOUNTS); - AKeyPair kp = keyPairs.get(src); - Address source=addresses.get(src); - Address target = addresses.get(new Random().nextInt(NUM_ACCOUNTS)); - Transfer t = Transfer.create(source,1, target, 1); - transactions.add(kp.signData(t)); - } - block = Block.create(System.currentTimeMillis(),transactions,Benchmarks.FIRST_PEER_KEY); - } - - @Benchmark - public void benchmark() throws BadSignatureException { - BlockResult br=state.applyBlock(block); - ACell.createPersisted(br.getState()); - } - - public static void main(String[] args) throws Exception { - Options opt = Benchmarks.createOptions(BigBlockBenchmark.class); - new Runner(opt).run(); - } -} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/CVMBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/CVMBenchmark.java deleted file mode 100644 index 2d57cfa71..000000000 --- a/convex-benchmarks/src/main/java/convex/benchmarks/CVMBenchmark.java +++ /dev/null @@ -1,84 +0,0 @@ -package convex.benchmarks; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; - -import convex.core.State; -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.data.Keywords; -import convex.core.data.Maps; -import convex.core.data.Strings; -import convex.core.data.Vectors; -import convex.core.init.Init; -import convex.core.lang.Context; -import convex.core.lang.Core; -import convex.core.lang.Symbols; -import convex.core.lang.ops.Constant; -import convex.core.lang.ops.Lookup; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Call; -import convex.core.transactions.Invoke; -import convex.core.transactions.Transfer; - -/** - * Benchmark for applying transactions to CVM state. This is measuring the end-to-end time for processing - * transactions themselves on the CVM. - * - * Skips stuff around transactions, block overhead, signatures etc. - */ -public class CVMBenchmark { - static State STATE=Benchmarks.STATE; - static Address HERO=Benchmarks.HERO; - - @Benchmark - public void smallTransfer() { - State s=STATE; - Address addr=HERO; - ATransaction trans=Transfer.create(addr,1, Benchmarks.VILLAIN, 1000); - Context ctx=s.applyTransaction(trans); - ctx.getValue(); - } - - @Benchmark - public void simpleCalculationStatic() { - State s=STATE; - Address addr=HERO; - ATransaction trans=Invoke.create(addr,1, convex.core.lang.ops.Invoke.create(Constant.create(Core.PLUS),Constant.of(1L),Constant.of(2L))); - Context ctx=s.applyTransaction(trans); - ctx.getValue(); - } - - @Benchmark - public void simpleCalculationDynamic() { - State s=STATE; - Address addr=HERO; - ATransaction trans=Invoke.create(addr,1, convex.core.lang.ops.Invoke.create(Lookup.create("+"),Constant.of(1L),Constant.of(2L))); - Context ctx=s.applyTransaction(trans); - ctx.getValue(); - } - - @Benchmark - public void defInEnvironment() { - State s=STATE; - Address addr=HERO; - ATransaction trans=Invoke.create(addr,1, convex.core.lang.ops.Def.create("a", Constant.of(13L))); - Context ctx=s.applyTransaction(trans); - ctx.getValue(); - } - - @Benchmark - public void contractCall() { - State s=STATE; - Address addr=HERO; - ATransaction trans=Call.create(addr,1L, Init.REGISTRY_ADDRESS, Symbols.REGISTER, Vectors.of(Maps.of(Keywords.NAME,Strings.create("Bob")))); - Context ctx=s.applyTransaction(trans); - ctx.getValue(); - } - - public static void main(String[] args) throws Exception { - Options opt = Benchmarks.createOptions(CVMBenchmark.class); - new Runner(opt).run(); - } -} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/EtchBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/EtchBenchmark.java deleted file mode 100644 index 2ed6a77e8..000000000 --- a/convex-benchmarks/src/main/java/convex/benchmarks/EtchBenchmark.java +++ /dev/null @@ -1,53 +0,0 @@ -package convex.benchmarks; - -import java.util.Random; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; - -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.Ref; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMLong; -import etch.EtchStore; - -public class EtchBenchmark { - - static final EtchStore store=EtchStore.createTemp(); - - static long nonce=0; - - @SuppressWarnings("unchecked") - static Ref[] refs=new Ref[1000]; - - static final Random rand=new Random(); - - static { - for (int i=0; i<1000; i++) { - AVector v=Vectors.of(0L,(long)i); - Ref r=v.getRef(); - refs[i]=r; - r.getHash(); - store.storeTopRef(r, Ref.STORED, null); - } - } - - @Benchmark - public void writeData() { - AVector v=Vectors.of(1L,nonce++); - store.storeTopRef(v.getRef(), Ref.STORED, null); - } - - @Benchmark - public void readDataRandom() { - int ix=rand.nextInt(1000); - store.refForHash(refs[ix].getHash()); - } - - public static void main(String[] args) throws Exception { - Options opt = Benchmarks.createOptions(EtchBenchmark.class); - new Runner(opt).run(); - } -} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/EvalBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/EvalBenchmark.java deleted file mode 100644 index ba5e24b0d..000000000 --- a/convex-benchmarks/src/main/java/convex/benchmarks/EvalBenchmark.java +++ /dev/null @@ -1,49 +0,0 @@ -package convex.benchmarks; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; - -import convex.core.data.ACell; -import convex.core.lang.Context; -import convex.core.lang.Reader; - -public class EvalBenchmark { - - static final Context CTX=Benchmarks.context(); - - private static final ACell eval(ACell form) { - return CTX.fork().eval(form).getResult(); - } - - static final ACell loopOp=Reader.read("(dotimes [i 1000])"); - @Benchmark - public void emptyLoop() { - eval(loopOp); - } - - static final ACell constantOp=Reader.read("1"); - @Benchmark - public void constant() { - eval(constantOp); - } - - // sum with dynamic core lookup - static final ACell simpleSum=Reader.read("(+ 1 2)"); - @Benchmark - public void simpleSum() { - eval(simpleSum); - } - - // sum with eval - static final ACell simpleSum2=Reader.read("(eval '(+ 1 2))"); - @Benchmark - public void evalSum() { - eval(simpleSum2); - } - - public static void main(String[] args) throws Exception { - Options opt = Benchmarks.createOptions(EvalBenchmark.class); - new Runner(opt).run(); - } -} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/HashBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/HashBenchmark.java deleted file mode 100644 index 1a50783d0..000000000 --- a/convex-benchmarks/src/main/java/convex/benchmarks/HashBenchmark.java +++ /dev/null @@ -1,32 +0,0 @@ -package convex.benchmarks; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; - -import convex.core.crypto.Hashing; -import convex.core.data.AArrayBlob; -import convex.core.data.Blob; -import convex.core.data.Format; -import convex.core.data.prim.CVMLong; - -public class HashBenchmark { - - @Benchmark - public void longHash_SHA_256() { - CVMLong l = CVMLong.create(17L); - AArrayBlob d = Format.encodedBlob(l); - Hashing.sha256(d.getInternalArray()); - } - - @Benchmark - public void kilobyteHash() { - Blob b = Blob.wrap(new byte[1024]); - b.getHash(); - } - - public static void main(String[] args) throws Exception { - Options opt = Benchmarks.createOptions(HashBenchmark.class); - new Runner(opt).run(); - } -} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/LatencyBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/LatencyBenchmark.java deleted file mode 100644 index 38f2b0bef..000000000 --- a/convex-benchmarks/src/main/java/convex/benchmarks/LatencyBenchmark.java +++ /dev/null @@ -1,113 +0,0 @@ -package convex.benchmarks; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; - -import convex.api.Convex; -import convex.core.Coin; -import convex.core.Result; -import convex.core.crypto.AKeyPair; -import convex.core.data.Address; -import convex.core.lang.ops.Constant; -import convex.core.transactions.Invoke; -import convex.peer.API; -import convex.peer.Server; - -/** - * Benchmark for full round-trip latencies - */ -public class LatencyBenchmark { - - static Address HERO=null; - static Address VILLAIN=null; - static final AKeyPair[] KPS=new AKeyPair[] {AKeyPair.generate(),AKeyPair.generate()}; - - static Server server; - static Convex client; - static Convex client2; - static Convex peer; - static { - List servers=API.launchLocalPeers(Benchmarks.PEER_KEYPAIRS, Benchmarks.STATE, null, null); - server=servers.get(0); - try { - Thread.sleep(1000); - peer=Convex.connect(server); - HERO=peer.createAccountSync(KPS[0].getAccountKey()); - VILLAIN=peer.createAccountSync(KPS[1].getAccountKey()); - peer.transfer(HERO, Coin.EMERALD); - peer.transfer(VILLAIN, Coin.EMERALD); - - client=Convex.connect(server.getHostAddress(), HERO,KPS[0]); - client2=Convex.connect(server.getHostAddress(), VILLAIN,KPS[1]); - } catch (IOException | TimeoutException e) { - e.printStackTrace(); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - } - - @Benchmark - public void roundTripTransaction() throws TimeoutException, IOException { - client.transactSync(Invoke.create(Benchmarks.HERO,-1, Constant.of(1L))); - // System.out.println(server.getBroadcastCount()); - } - - @Benchmark - public void roundTripTwoTransactions() throws TimeoutException, IOException, InterruptedException, ExecutionException { - Future r1=client.transact(Invoke.create(HERO,-1, Constant.of(1L))); - Future r2=client2.transact(Invoke.create(VILLAIN,-1, Constant.of(1L))); - r1.get(1000,TimeUnit.MILLISECONDS); - r2.get(1000,TimeUnit.MILLISECONDS); - } - - @Benchmark - public void roundTrip10Transactions() throws TimeoutException, IOException, InterruptedException, ExecutionException { - doTransactions(10); - } - - @Benchmark - public void roundTrip50Transactions() throws TimeoutException, IOException, InterruptedException, ExecutionException { - doTransactions(50); - } - - @Benchmark - public void roundTrip1000Transactions() throws TimeoutException, IOException, InterruptedException, ExecutionException { - doTransactions(1000); - } - - @SuppressWarnings("unchecked") - private void doTransactions(int n) throws IOException, InterruptedException, ExecutionException, TimeoutException { - CompletableFuture[] rs=new CompletableFuture[n]; - for (int i=0; i f=client.transact(Invoke.create(HERO,-1, Constant.of(i))); - rs[i]=f; - } - CompletableFuture.allOf(rs).get(1000,TimeUnit.MILLISECONDS); - Result r0=rs[0].get(); - if (r0.isError()) { - throw new Error("Transaction failed: "+r0); - } - } - - @Benchmark - public void roundTripQuery() throws TimeoutException, IOException, InterruptedException, ExecutionException { - client.querySync(Constant.of(1L)); - } - - - public static void main(String[] args) throws Exception { - Options opt = Benchmarks.createOptions(LatencyBenchmark.class); - new Runner(opt).run(); - } -} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/ListDataBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/ListDataBenchmark.java deleted file mode 100644 index 6f0ae326d..000000000 --- a/convex-benchmarks/src/main/java/convex/benchmarks/ListDataBenchmark.java +++ /dev/null @@ -1,25 +0,0 @@ -package convex.benchmarks; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; - -import convex.core.data.AVector; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMLong; - -public class ListDataBenchmark { - - @Benchmark - public void append1000() { - AVector list = Vectors.empty(); - for (long i = 0; i < 1000; i++) { - list = list.append(CVMLong.create(i)); - } - } - - public static void main(String[] args) throws Exception { - Options opt = Benchmarks.createOptions(ListDataBenchmark.class); - new Runner(opt).run(); - } -} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/MapBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/MapBenchmark.java deleted file mode 100644 index 8d2ece0b3..000000000 --- a/convex-benchmarks/src/main/java/convex/benchmarks/MapBenchmark.java +++ /dev/null @@ -1,26 +0,0 @@ -package convex.benchmarks; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; - -import convex.core.data.AMap; -import convex.core.data.Maps; -import convex.core.data.prim.CVMLong; - -public class MapBenchmark { - - @Benchmark - public void assocMap1000() { - AMap m = Maps.empty(); - for (long i = 0; i < 1000; i++) { - CVMLong ci=CVMLong.create(i); - m = m.assoc(ci, ci); - } - } - - public static void main(String[] args) throws Exception { - Options opt = Benchmarks.createOptions(MapBenchmark.class); - new Runner(opt).run(); - } -} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/OpBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/OpBenchmark.java deleted file mode 100644 index 3dd0d4a7b..000000000 --- a/convex-benchmarks/src/main/java/convex/benchmarks/OpBenchmark.java +++ /dev/null @@ -1,55 +0,0 @@ -package convex.benchmarks; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; - -import convex.core.data.ACell; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.Core; -import convex.core.lang.Reader; -import convex.core.lang.ops.Constant; -import convex.core.lang.ops.Invoke; - -public class OpBenchmark { - - static final Context CTX=Benchmarks.context(); - - private static final ACell runOp(AOp op) { - return CTX.fork().execute(op).getResult(); - } - - static final AOp loopOp=CTX.expandCompile(Reader.read("(dotimes [i 1000])")).getResult(); - @Benchmark - public void emptyLoop() { - runOp(loopOp); - } - - static final AOp constantOp=CTX.expandCompile(Reader.read("1")).getResult(); - @Benchmark - public void constant() { - runOp(constantOp); - } - - // sum with dynamic core lookup - static final AOp simpleSum=CTX.expandCompile(Reader.read("(+ 1 2)")).getResult(); - @Benchmark - public void simpleSum() { - runOp(simpleSum); - } - - // sum without dynamic core lookup (much faster!!) - static final AOp simpleSum2=Invoke.create(Constant.create(Core.PLUS),Constant.of(1L),Constant.of(2)); - @Benchmark - public void simpleSumPrecompiled() { - runOp(simpleSum2); - } - - - - public static void main(String[] args) throws Exception { - Options opt = Benchmarks.createOptions(OpBenchmark.class); - new Runner(opt).run(); - } -} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/SignatureBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/SignatureBenchmark.java deleted file mode 100644 index 5a7cb49b0..000000000 --- a/convex-benchmarks/src/main/java/convex/benchmarks/SignatureBenchmark.java +++ /dev/null @@ -1,61 +0,0 @@ -package convex.benchmarks; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; - -import convex.core.crypto.AKeyPair; -import convex.core.crypto.ASignature; -import convex.core.data.ABlob; -import convex.core.data.ACell; -import convex.core.data.Blobs; -import convex.core.data.Ref; -import convex.core.data.SignedData; - -public class SignatureBenchmark { - - private static final AKeyPair KEYPAIR=AKeyPair.generate(); - private static final SignedData SIGNED=makeSigned(); - - private static SignedData makeSigned() { - SignedData signed= KEYPAIR.signData(Blobs.fromHex("cafebabe")); - ACell.createPersisted(signed); - return signed; - } - - private static final ASignature SIGNATURE=SIGNED.getSignature(); - - @Benchmark - public void signData() { - ABlob b=Blobs.createRandom(16); - KEYPAIR.signData(b); - } - - @Benchmark - public void signVerify() { - ABlob b=Blobs.createRandom(16); - SignedData sd=KEYPAIR.signData(b); - ASignature sig=sd.getSignature(); - - sig.verify(b.getHash(), KEYPAIR.getAccountKey()); - } - - @Benchmark - public void verify() { - ASignature sig=SIGNATURE; - sig.verify(SIGNED.getValue().getHash(), KEYPAIR.getAccountKey()); - } - - @SuppressWarnings("unchecked") - @Benchmark - public void verifyFromStore() { - SignedData signed=(SignedData) Ref.forHash(SIGNED.getHash()).getValue(); - signed.checkSignature(); - } - - - public static void main(String[] args) throws Exception { - Options opt = Benchmarks.createOptions(SignatureBenchmark.class); - new Runner(opt).run(); - } -} diff --git a/convex-benchmarks/src/main/java/convex/benchmarks/ThreadCoordinationBenchmark.java b/convex-benchmarks/src/main/java/convex/benchmarks/ThreadCoordinationBenchmark.java deleted file mode 100644 index 24f56e26f..000000000 --- a/convex-benchmarks/src/main/java/convex/benchmarks/ThreadCoordinationBenchmark.java +++ /dev/null @@ -1,48 +0,0 @@ -package convex.benchmarks; - -import java.util.concurrent.ArrayBlockingQueue; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; - -import convex.core.util.Utils; - -public class ThreadCoordinationBenchmark { - - public static final ArrayBlockingQueue INPUT =new ArrayBlockingQueue<>(100); - public static final ArrayBlockingQueue OUTPUT =new ArrayBlockingQueue<>(100); - - static { - Thread processor = new Thread(()->{ - while (true) { - try { - Object o; - o = INPUT.take(); - OUTPUT.put(o); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - }); - processor.setDaemon(true); - processor.start(); - } - - @Benchmark - public void pushThroughQueue() { - try { - INPUT.put(1L); - OUTPUT.take(); - } catch (InterruptedException e) { - throw Utils.sneakyThrow(e); - } - } - - public static void main(String[] args) throws Exception { - Options opt = Benchmarks.createOptions(ThreadCoordinationBenchmark.class); - new Runner(opt).run(); - } - -} diff --git a/convex-cli/.gitignore b/convex-cli/.gitignore deleted file mode 100644 index e661051a1..000000000 --- a/convex-cli/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -/target/ -/.settings/ -/.classpath -/.project -/pom.xml.versionsBackup diff --git a/convex-cli/pom.xml b/convex-cli/pom.xml deleted file mode 100644 index 4e3884d0f..000000000 --- a/convex-cli/pom.xml +++ /dev/null @@ -1,213 +0,0 @@ - - - world.convex - convex - 0.7.0-rc3 - - 4.0.0 - - convex-cli - - Convex CLI - Convex command line integration and tools - https://convex.world - - - - 1.2.3 - 1.6.0 - 0.6.0 - - - - - spring-repo - https://repo.spring.io/release - - - - - - maven-assembly-plugin - 3.3.0 - - ${project.directory} - - - convex.cli.Main - - - - jar-with-dependencies - - convex-cli - - false - - - - create-archive - package - - single - - - - - - org.asciidoctor - asciidoctor-maven-plugin - 2.2.1 - - - io.spring.asciidoctor - spring-asciidoctor-extensions-block-switch - ${asciidoctor.spring.version} - - - org.asciidoctor - asciidoctorj-pdf - ${asciidoctorj.pdf.version} - - - - - convert-to-html - generate-resources - - process-asciidoc - - - ${project.build.directory}/html - - coderay - ./images - left - font - - - - - - - - - - - - org.eclipse.m2e - lifecycle-mapping - 1.0.0 - - - - - - - org.asciidoctor - - - asciidoctor-maven-plugin - - - [2.2.1,) - - - - process-asciidoc - - - - - - - - - - - - - - - - - - world.convex - convex-peer - ${convex.version} - - - world.convex - convex-core - ${convex.version} - - - world.convex - convex-gui - ${convex.version} - - - org.slf4j - slf4j-simple - - - - - info.picocli - picocli - 4.6.1 - - - com.pholser - junit-quickcheck-core - 1.0 - test - - - com.pholser - junit-quickcheck-generators - 1.0 - test - - - org.junit.jupiter - junit-jupiter-engine - ${junit.version} - test - - - org.junit.vintage - junit-vintage-engine - ${junit.version} - test - - - org.junit.jupiter - junit-jupiter-params - ${junit.version} - test - - - org.slf4j - slf4j-api - ${slf4j.version} - - - ch.qos.logback - logback-core - ${logback.version} - - - ch.qos.logback - logback-classic - ${logback.version} - - - diff --git a/convex-cli/src/docs/asciidoc/images/convex_logo.svg b/convex-cli/src/docs/asciidoc/images/convex_logo.svg deleted file mode 100644 index 84fcb7786..000000000 --- a/convex-cli/src/docs/asciidoc/images/convex_logo.svg +++ /dev/null @@ -1 +0,0 @@ -Convex logoCreated with Sketch. diff --git a/convex-cli/src/docs/asciidoc/index.adoc b/convex-cli/src/docs/asciidoc/index.adoc deleted file mode 100644 index 52ea07d8e..000000000 --- a/convex-cli/src/docs/asciidoc/index.adoc +++ /dev/null @@ -1,660 +0,0 @@ -= Convex Command Line Interface -:toc: -:toc-title: Convex CLI - -image::convex_logo.svg[Convex Command Line Interface,100,float=right,opts=inline] - -== Introduction -Convex Command Line Interface or CLI, allows you to control and setup a local Convex network, or add a peer to an already existing network. -The current test Convex network can be found at https://convex.world[convex.world]. - - - -=== Overview -This CLI is part of the Convex code base. The CLI has been built to do the following things: - -. Run a Local Convex network. - -. Run a Local Peer(s) connected to a local network. - -. Send and Query transactions within a local network. - -. Setup accounts within a local network. - -. Step key pairs to use for accessing local and remote networks. - -. Run a Local Peer connected to a remote Convex network. - - - -== Getting Started -As yet we have not packaged this in any package manager for downloading so currently the CLI can only be downloaded via github. - -=== Get the source -You will need to visit https://github.com/Convex-Dev/convex[convex-dev @ github] and clone the repository onto your local computer. - -The develop branch is currently the latest version. - -.terminal commands - git clone https://github.com/Convex-Dev/convex.git - cd convex - git checkout develop - git pull - -=== Convex Projects -The Convex code repository is made up of the following sub projects: - -convex-benchmaks:: Run benchmarks on the convex network. -convex-cli:: This CLI project. -convex-core:: The main convex core library. -convex-gui:: A local convex network running as a GUI application. -convex-peer:: Peer library used to run convex peers. - - -=== Compile and Setup -Once you have downloaded the latest source of Convex, you can now compile the suite of projects. - -To do this you need to execute the `Maven` command: - -.terminal command - mvn install - -or - -.terminal command - mvn package - -If you wish to build without running the tests you can append the option `-DskipTests` - - -After building and installing the maven dependencies you should eventually see the following lines -generated by the Maven build process: - -.output ----- -[INFO] ------------------------------------------------------------------------ -[INFO] Reactor Summary for convex 0.7.0-SNAPSHOT: -[INFO] -[INFO] convex ............................................. SUCCESS [ 0.146 s] -[INFO] convex-core ........................................ SUCCESS [ 5.003 s] -[INFO] convex-peer ........................................ SUCCESS [ 0.027 s] -[INFO] convex-gui ......................................... SUCCESS [ 2.474 s] -[INFO] convex-cli ......................................... SUCCESS [ 4.665 s] -[INFO] convex-benchmarks .................................. SUCCESS [ 1.644 s] -[INFO] ------------------------------------------------------------------------ -[INFO] BUILD SUCCESS -[INFO] ------------------------------------------------------------------------ -[INFO] Total time: 14.463 s -[INFO] Finished at: 0000-00-00T00:00:00+00:00 -[INFO] ------------------------------------------------------------------------ ----- - -=== Files needed by CLI run a local Network or Peer -The CLI needs 3 types of files before running a local Convex network or as a Peer on any network. -The type of files are: - -. _Etch Storage database_ file. This contains the stored state of the Convex network. Usually when starting up the initial cluster the first set of peers share the same Etch database. CLI Parameter: *--etch* - -. _Keystore database_ file. This file contains the private/public key pairs used for the peers and any subsequent users. CLI Parameters: *--keystore*, *--password* - -. _Session_ file. This is created by the CLI to keep track of the locally running peers, so that if you want to access the local network or add another peer to the local network, the CLI will look at the session file for a randomly available peer to connect too. CLI Parameter: *--session* - -[CAUTION] -==== -The GUI version and the CLI run the same local network. The only difference is that the GUI does not create a session file. This means that some of the CLI features cannot be used with the GUI local network. -==== - - -== Running the CLI -Once you have successfully compiled and built Convex projects, you can now run the command line tool. - -.Mac -[source,bash,role="primary"] ----- -./convex help ----- - -.Linux -[source,bash,role="secondary"] ----- -./convex help ----- - -.Windows -[source,bash,role="secondary"] ----- -convex help - ----- - -=== Commands -The CLI is split into command the following commands and subcommands: - -Account Commands:: - -[cols="1,1,2"] -|=== -|Command|Sub command|Description - -|account, ac| |Manages convex accounts. -||balance, bal, ba |Get an account balance. - -||create, cr| Creates an account on a local network using a public/private key from the keystore. -||fund, fu|Transfers funds to an account using a public/private key from the keystore. -||information, info, in|Get account information. -|=== - -Key Commands:: -[cols="1,1,2"] -|=== -|Command|Sub command|Description - -|key, ke| |Manage local Convex key store. - -||import, im|Import key pairs to the keystore. -||generate, ge|Generate one or more key pairs. -||list, li|List available key pairs. -||export, ex|Export key pair from the keystore. -|=== - -Local Commands:: -[cols="1,1,2"] -|=== -|Command|Sub command|Description - -|local, lo||Operates a local convex network. -||gui|Starts a local convex test network using the peer manager GUI application. -||start, st|Starts a local convex test network, same as GUI but using a command line. -|=== - -Peer Commands:: -[cols="1,1,2"] -|=== -|Command|Sub command|Description - -|peer, pe||Operates a local peer. -||create, cr|Creates a keypair, new account and a funding stake: to run a local peer. -||start, st|Starts a local peer. -|=== - -Query Command:: -[cols="1,1"] -|=== -|Command|Description - -|query, qu|Execute a query on the current peer. -|=== - -Status Command:: -[cols="1,1"] -|=== -|Command|Description - -|status, st|Reports on the current status of the network. -|=== - -Transaction Command:: -[cols="1,1"] -|=== -|Command|Description - - -|transaction, transact, tr|Execute a transaction on the network via a peer. -|=== - -Help Command:: -[cols="1,1"] -|=== -|Command|Description - -|help|Displays help information about the specified command -|=== - -=== Shared Options -There are a few common options that can be used with any command or sub command. They are as follows: - -[cols="1,2,4"] -|=== -|Short Option|Long Option|Description - -|-c|--config= |Use the specified config file. -|-e|--etch= |Convex state storage filename. The default is to use a temporary storage filename. -|-k|--keystore= |keystore filename. Default: ~/.convex/keystore.pfx -|-p|--password= |Password to read/write to the Keystore -|-s|--session= |Session filename. Defaults ~/.convex/session.conf -|-v|--verbose |Show more verbose log information. You can increase verbosity by using multiple -v or -vvv -|-h|--help |Show this help message and exit. -|-V|--version |Print version information and exit. -|=== - -=== Requesting Help -The CLI supports help using the *-h* or *--help* options or the command *help*. For each sub command there are more help options. - -So for example - -.terminal command - ./convex --help - -will show the common options for all commands, and the list of available commands. - -.terminal command - ./convex local start --help - -will show the common options as well as the specific options for the *convex.local.start* command - -[#command-local-start] -== Starting a local network -The CLI is designed to start a local Convex network. This will allow for the developer/tester to try out Convex in a local environment without -effecting any other networks. - -=== Simple local start -The simplest way to start up the local Convex network is to run the following command: - -.terminal command - ./convex local start --password=my-password - - -[WARNING] -==== -In this document the password option will always be shown as `--password=my-password`. This is an example of a not very good password to use for storing your keys. We suggest that you use a more secure password instead of `my-password`. -==== - -You wil always need to pass the password to the *keystore* file since the CLI will need access the keys to create and start up the local peers. - -The CLI will automatically create 4 keypairs and place them in the keystore. The CLI will then start up 4 peers all sharing a single -temporary local _Etch Database_ in the /tmp folder. - -The Simple local start consists of the following steps: - -. Create the _count_ number of peer keypairs. -. Store the new keypairs in the keystore. -. Start up the local network using the new created keys. - - - -=== Local start with peer keys -While the simple local network start will auto generate public keys for the local peers and create the peer accounts. You have the option instead to start the local network using a predetermined set of keys from your keystore. To do this you need to provide a list of public keys that you want the CLI to use to start up the local network. - - -If you have already used the simple local start, you can get the list of keys created by running the <>, -this will show you the list of keys that have been stored in the key store. - -.terminal session ----- -./convex key list --password=my-password - -Index Public Key -1 6e89035fce6d842b65e7831433fb3426928865a3c8de9536cfa50a1928eb0276 <1> -2 13e691e05dee5a2c5ad90f6802f4ac5c274582ca5332516dc4740ae55d817856 -3 8291e8976e0ee0363f98f819712552924e1dd1d8ab77c4dc8577765ee3eb2d36 -4 ce55bb850cefaf87c5a16ab7c410f942e11463d0000eb71e8a22e6ce76301b5c -5 21076aa0c88baba170e62196b5735316f6cc1c5bfe672c0c1e5f9b85d8aaf8cb - ----- - -<1> First keypair stored in the keystore with the public key starting with `6e89035fce6...` or at index position #1 - -See <> for more informaton. - - -To start up the local Convex network with the first 4 public keys for the first 4 peers you can run the following command: - -.terminal command - ./convex local start --public-key=6e89035 --public-key=13e691e --public-key=8291e89 --public-key=ce55bb8 --password=my-password - -or you can combine the public key fields together into a single comma seperated list option such as: - -.terminal command - ./convex local start --public-key=6e89035,13e691e,8291e89,ce55bb8 --password=my-password - -This will now start up a local Convex network with 4 peers each using a public key from the list provided in the keystore. - -[TIP] -==== -To start the same peers using the same public keys you can also use the index number in the keystore. So the line: - - ./convex local start --index-key=1,2,3,4 --password=my-password - -Will start the same set of peers as above using the first 4 key pairs from the keystore. -==== - -=== Local start with port numbers -By default the CLI start a local network with each peer assigned a random port number. You can specify the port numbers used for each peer, by setting the `--ports` option. - -The `--ports` option takes a list or range of port numbers. - -You can use multiple `--ports` options such as: - - ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081 --ports=8082 --ports=8083 --ports=8084 - -or you can provide a list of ports to use for each peer: - - ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081,8082,8083,8084 - -or a range of port numbers: - - ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081-8084 - -or an open range for any number of peers: - - ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081- - -or a combination of the above, where the first peer uses port 8088, and all subsequent peers use ports from 8090: - - ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8088 --ports=8090- - - -=== Local start with config file -You can create a config file and assign the command options as config items. You can then start your -local network using a config file, instead of providing a list of keys. - -.terminal command - ./convex local start --config=example_convex_local_start.conf - - -==== Config Parameters for convex.local.start -.file: example_convex_local_start.conf ----- - # etch storage database - convex.etch = <.> - - # default keystore filename - convex.keystore =$HOME/.convex/keystore.pfx - - # default session filename - convex.session = $HOME/.convex/session.conf - - # number of peers to start - convex.local.start = 4 - - # comma list of index of keys or items <.> - convex.local.start.index-key= - - # comma list of public-key hex values, or multiple items - convex.local.start.public-key=6e89035 - convex.local.start.public-key=13e691e - convex.local.start.public-key=8291e89 - convex.local.start.public-key=ce55bb8 - - convex.local.start.ports=8090- <.> - - # keystore password - convex.local.password = <.> ----- - -<.> If no filename is provided, then the CLI will create a temporary etch storage database in the temp folder. -<.> You can provide a list of public keys or indexes or duplicate settings with different values. - - convex.local.index-key = 1,2,3 - # is the same as - convex.local.index-key = 1 - convex.local.index-key = 2 - convex.local.index-key = 3 - -<.> The peers will use port 8090 onwards -<.> If you do not provide a password, then the CLI will request a password on starting the local network. - -[#command-peer-start-local] -== Starting a local Peer -How to start a local peer, and join a local Convex network. - -To start a local peer you first need to do the following: - -. Start a local Convex network. see <>. - -. Create a keypair, or select an unused keypair to use for the peer. - -. Create an account for the peer. - -. Assign funds to the peer account. - -. Assign the peer account funds for the peer stake. - - -[NOTE] -==== -This type of block chain technology uses Convergent Proof of Stake (CPoS) algorithm, where each peer has a public key and a stake amount. The stake amount decides the peers voting control in the CPoS algorithm. See https://convex.world/technology[Convex Technology] -==== - -The following command does all of the above except step #1: - - ./convex peer create --password=my-password - -You will then get back from the `peer create` command something like this: - - Public Peer Key: 0xbc1290834e1953b2952624ab8ce34e87d308ba975d655163f9fe47283f0436aa - Address: 45 - Balance: 199945799 - Inital stake amount: 9800000000 - Peer start line: ./convex peer start --password=my-password --address=45 --public-key=bc1290 - -you can then copy the *Peer start line:* and run a peer with the local network. - - ./convex peer start --password=my-password --address=45 --public-key=bc1290 - -[#command-peer-start-remote] -== Starting a local Peer to a remote Convex network -How to start a local peer, that connects to a remote Convex network. - -If you whish to connect to your own remote peer, you can by adding the `--peer` option. This tells the new peer you are starting where a remote peer is located. Once found the started peer will try and sync with the remote peer. - -To connect to someone elses remote network or to connect to the test network at https://convex.world[convex] you will need to obtain a peer keypair, account with sufficient funds and the peer registered with a stake amount. - - -[#command-local-gui] -== Starting the GUI local network -How to start the gui local network. - -To start the local GUI network, you can call the command: - - ./convex local gui - -This starts the a local network in GUI mode. At the moment the GUI local network does not publish the keypairs used for the network, so the CLI cannot do the following when the GUI network is running: - -. Account Fund Request - -. Account Create - -. Peer Create - - -== Peer Output -Describes the output fields - -[.small] -.Sample output ----- -Starting network Id: 0xefe75ea61ad52b38f4455a88911b7bd851dc080090e1b1cb4ec75d85a44eb92d -#2: Peer:1770c3 URL: localhost:43849 Status: J NS Connections: 1/ 0 Consensus: 0 State:efe75e Belief:46bbe3 Msg: connection -#1: Peer:fa26c5 URL: localhost:41635 Status: J NS Connections: 1/ 0 Consensus: 0 State:efe75e Belief:7c7542 Msg: connection -#3: Peer:556deb URL: localhost:37985 Status: J NS Connections: 1/ 0 Consensus: 0 State:efe75e Belief:a43082 Msg: connection -#4: Peer:0fce50 URL: localhost:46559 Status: J NS Connections: 1/ 0 Consensus: 0 State:efe75e Belief:a98ea8 Msg: connection - ----- - -then later - -[.small] -.Sample output ----- - -#2: Peer:1770c3 URL: localhost:43849 Status: J S Connections: 3/ 3 Consensus: 20 State:cfa8fe Belief:2c6f2a Msg: trusted connection -#4: Peer:0fce50 URL: localhost:46559 Status: J S Connections: 3/ 2 Consensus: 20 State:cfa8fe Belief:2c6f2a Msg: connection -#3: Peer:556deb URL: localhost:37985 Status: J S Connections: 3/ 3 Consensus: 20 State:cfa8fe Belief:2c6f2a Msg: trusted connection -#4: Peer:0fce50 URL: localhost:46559 Status: J S Connections: 3/ 3 Consensus: 20 State:cfa8fe Belief:2c6f2a Msg: trusted connection ----- - -On every event that occurs for a peer in the cluster, on it's own an event is shown as a line. - -The event data can be split up into the following fields: - -[cols="1,2a,1m"] -|=== -|Name |Description|Example - -|Index |Peer index starting at 1 within the cluster of peers |#4 -|Peer |First 6 characters of the public key of the peer |Peer:0fce50 -|URL |URL of the peer|URL: localhost:46559 -|Status -| -[horizontal] -NJ:: Not Joined -J:: Joined -NS:: Not Synced -S:: Synced -|Status: J S - -|Connections |_Peer connection count_ / _Peer trusted connection count_|Connections: 3/ 2 -|Consensus |Consensus level |Consensus: 20 -|State | First 6 characters of the State hash |State:cfa8fe -|Belief |First 6 characters of the Belief hash |Belief:2c6f2a -|Msg |Short message of the event that occured on this peer |Msg: trusted connection -|=== - -[#command-keys] -== Managing your Keys - The Keystore -How to manage the local public/private key pairs. - -When using any of the `key` sub commands, you do not need to be connected to any network. - -The option `--keystore` can be used with any sub command to specify which keystore to use. - - -[#command-key-generate] -=== Generating keypairs -How to generate a new set of public/private keys. - -You need to generate keypairs when: - -. Creating an account - -. Creating a new peer - -This command allows you to create 1+ keypairs in the keystore. - -So for example this will create 10 keypairs: - - ./convex key generate 10 --password=my-password - -[#command-key-list] -=== List keys -How to list the keys store in the keystore. - -To list out your keystore and view the public keys of each keypair. - - ./convex key list --password=my-password - - -[#command-key-export] -=== Exporting keys -How to export the keys from your keystore to encrypted text. - -You can export a keypair from the keystore to an encrypted PEM formated text. This is usefull if you need -to give another user, or application access to your network. - -You need to provide an `--export-password` option with the password of the encrypted PEM formated text. - -You also need to provide the location of the keypair you wish to export, this can be done using the `--index-key` or `--public-key` option. - -In this example first list out the keys from the keystore. - - ./convex key list --password=my-password - - 1 e7fdcb0bfdfb786b51eedf33b575.... - 2 373d2a583695ff367dd986e12785.... - .. - - -If we now want to export the key #2, then we can use the following command: - - ./convex key export --index-key=2 --export-password=my-password --password=my-password - -or a more safer way is to use the first hex of the public key - - ./convex key export --publi-key=373d2a583695ff --export-password=my-password --password=my-password - - -[WARNING] -==== -In this example we have used a insecure password of `my-password` to encrypt the exported key. We suggest that you use a better password when exporting your keys, and keep the exported PEM formated text secure. -==== - -[#command-key-import] -=== Importing keys -How to import keys into the keystore. - -You would need to import keys, when you want to run a peer or send a transaction for an account on another network. - -To import a keypair you need to set the options `--import-file` or `--import-file` and `--import-password`. - -So for example: - - ./convex key import --import-file=my_key.pem --import-password=my-password --password=my-password - -If the import password is successfull, this will import the keypair into the keystore, and show the public key of the imported keypair. - - -[#command-accounts] -== Managing Accounts -Information on how to create, fund and get information about the local accounts. - -This set of sub commands manage accounts on the local network. You need to have a local network running on the same computer for these commands to work. - -The reason is that the keystore needs to contain the keys for the first genesis accounts in the network. With the access to the genesis keypair, the account commands can create an account, and transfer sufficient funds to the new account. - - -[#command-account-create] -=== Create an account -How to create a local account. - -To create a new account and new keypair, you can just run: - - ./convex account create --password=my-password - -If you wish to use an already defined keypair in your keystore, you can set the `--index-key` or `--public-key` options. - - ./convex account create --public-key=eb1234 --password=my-password - - -The command returns the account address and public key used to create the account. - - -[#command-account-balance] -=== Get an accounts balance -How to get an account's balance. - -To obtain the balance of an account, you just need to provide an address of the account. - -So to run: - - ./convex account balance 45 - -Returns the balance for account #45 - - -[#command-account-fund] -=== Request funds for an account -How to request funds for an account. - -[#command-account-info] -=== Get information about an account -How to get information about an account. - -[#command-status] -== Status -How to get the local network status. - -[#command-query] -== Queries -How to execute queries on a local Convex network. - -[#command-transaction] -== Trancsactions -How to execute transactions on a local Convex network. - - - - - - - diff --git a/convex-cli/src/main/java/convex/cli/Account.java b/convex-cli/src/main/java/convex/cli/Account.java deleted file mode 100644 index b7f1a2e6a..000000000 --- a/convex-cli/src/main/java/convex/cli/Account.java +++ /dev/null @@ -1,38 +0,0 @@ -package convex.cli; - -import picocli.CommandLine; -import picocli.CommandLine.Command; -import picocli.CommandLine.ParentCommand; - - -/** - * - * Convex account sub commands - * - * convex.account - * - */ -@Command(name="account", - aliases={"ac"}, - subcommands = { - AccountBalance.class, - AccountCreate.class, - AccountFund.class, - AccountInformation.class, - CommandLine.HelpCommand.class - }, - mixinStandardHelpOptions=true, - description="Manages convex accounts.") -public class Account implements Runnable { - - // private static final Logger log = Logger.getLogger(Account.class.getName()); - - @ParentCommand - protected Main mainParent; - - @Override - public void run() { - // sub command run with no command provided - CommandLine.usage(new Account(), System.out); - } -} diff --git a/convex-cli/src/main/java/convex/cli/AccountBalance.java b/convex-cli/src/main/java/convex/cli/AccountBalance.java deleted file mode 100644 index 485f49fc4..000000000 --- a/convex-cli/src/main/java/convex/cli/AccountBalance.java +++ /dev/null @@ -1,75 +0,0 @@ -package convex.cli; - -import convex.api.Convex; -import convex.core.Result; -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.lang.Reader; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine.Command; -import picocli.CommandLine.Option; -import picocli.CommandLine.Parameters; -import picocli.CommandLine.ParentCommand; - -/** - * - * Convex account balance command - * - * convex.account.balance - * - */ - -@Command(name="balance", - aliases={"bal", "ba"}, - mixinStandardHelpOptions=true, - description="Get account balance.") -public class AccountBalance implements Runnable { - private static final Logger log = LoggerFactory.getLogger(AccountBalance.class); - - @ParentCommand - private Account accountParent; - - @Option(names={"--port"}, - description="Port number to connect to a peer.") - private int port = 0; - - @Option(names={"--host"}, - defaultValue=Constants.HOSTNAME_PEER, - description="Hostname to connect to a peer. Default: ${DEFAULT-VALUE}") - private String hostname; - - - @Parameters(paramLabel="address", - description="Address of the account to get the balance .") - private long addressNumber; - - @Option(names={"-t", "--timeout"}, - description="Timeout in miliseconds.") - private long timeout = Constants.DEFAULT_TIMEOUT_MILLIS; - - - @Override - public void run() { - - Main mainParent = accountParent.mainParent; - - if (addressNumber == 0) { - log.warn("You need to provide a valid address number"); - return; - } - - Convex convex = null; - Address address = Address.create(addressNumber); - try { - convex = mainParent.connectToSessionPeer(hostname, port, address, null); - String queryCommand = String.format("(balance #%d)", address.longValue()); - ACell message = Reader.read(queryCommand); - Result result = convex.querySync(message, timeout); - mainParent.output.setResult(result); - } catch (Throwable t) { - mainParent.showError(t); - } - } -} diff --git a/convex-cli/src/main/java/convex/cli/AccountCreate.java b/convex-cli/src/main/java/convex/cli/AccountCreate.java deleted file mode 100644 index 59141f4f5..000000000 --- a/convex-cli/src/main/java/convex/cli/AccountCreate.java +++ /dev/null @@ -1,117 +0,0 @@ -package convex.cli; - -import java.util.List; - -import convex.api.Convex; -import convex.core.crypto.AKeyPair; -import convex.core.data.Address; -import convex.core.util.Utils; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine.Command; -import picocli.CommandLine.Option; -import picocli.CommandLine.ParentCommand; - -/** - * - * Convex account create command - * - * convex.account.create - * - */ - -@Command(name="create", - aliases={"cr"}, - mixinStandardHelpOptions=true, - description="Creates an account using a public/private key from the keystore.%n" - + "You must provide a valid keystore password to the keystore.%n" - + "If the keystore is not at the default location also the keystore filename.") -public class AccountCreate implements Runnable { - - private static final Logger log = LoggerFactory.getLogger(AccountCreate.class); - - @ParentCommand - private Account accountParent; - - @Option(names={"-i", "--index-key"}, - defaultValue="0", - description="Keystore index of the public/private key to use to create an account.") - private int keystoreIndex; - - @Option(names={"--public-key"}, - defaultValue="", - description="Hex string of the public key in the Keystore to use to create an account.%n" - + "You only need to enter in the first distinct hex values of the public key.%n" - + "For example: 0xf0234 or f0234") - private String keystorePublicKey; - - @Option(names={"--port"}, - description="Port number to connect to a peer.") - private int port = 0; - - @Option(names={"--host"}, - defaultValue=Constants.HOSTNAME_PEER, - description="Hostname to connect to a peer. Default: ${DEFAULT-VALUE}") - private String hostname; - - @Option(names={"-f", "--fund"}, - description="Fund the account with the default fund amount.") - private boolean isFund; - - @Override - public void run() { - - Main mainParent = accountParent.mainParent; - - AKeyPair keyPair = null; - - if (keystoreIndex > 0 || !keystorePublicKey.isEmpty()) { - try { - keyPair = mainParent.loadKeyFromStore(keystorePublicKey, keystoreIndex); - } catch (Error e) { - mainParent.showError(e); - return; - } - if (keyPair == null) { - log.warn("cannot find the provided public key"); - return; - } - } - if (keyPair == null) { - try { - List keyPairList = mainParent.generateKeyPairs(1); - keyPair = keyPairList.get(0); - mainParent.output.setField("Public Key", keyPair.getAccountKey().toHexString()); - } - catch (Error e) { - mainParent.showError(e); - return; - } - } - - Convex convex = null; - try { - - convex = mainParent.connectAsPeer(0); - - Address address = convex.createAccountSync(keyPair.getAccountKey()); - mainParent.output.setField("Address", address.longValue()); - if (isFund) { - convex.transferSync(address, Constants.ACCOUNT_FUND_AMOUNT); - convex = mainParent.connectToSessionPeer(hostname, port, address, keyPair); - Long balance = convex.getBalance(address); - mainParent.output.setField("Balance", balance); - } - mainParent.output.setField("Account usage", - String.format( - "to use this key can use the options --address=%d --public-key=%s", - address.toLong(), - Utils.toFriendlyHexString(keyPair.getAccountKey().toHexString(), 6) - ) - ); - } catch (Throwable t) { - mainParent.showError(t); - } - } -} diff --git a/convex-cli/src/main/java/convex/cli/AccountFund.java b/convex-cli/src/main/java/convex/cli/AccountFund.java deleted file mode 100644 index 13400cb42..000000000 --- a/convex-cli/src/main/java/convex/cli/AccountFund.java +++ /dev/null @@ -1,96 +0,0 @@ -package convex.cli; - -import convex.api.Convex; -import convex.core.crypto.AKeyPair; -import convex.core.data.Address; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine.Command; -import picocli.CommandLine.Option; -import picocli.CommandLine.Parameters; -import picocli.CommandLine.ParentCommand; - -/** - * - * Convex account fund command - * - * convex.account.fund - * - */ - -@Command(name="fund", - aliases={"fu"}, - mixinStandardHelpOptions=true, - description="Transfers funds to account using a public/private key from the keystore.%n" - + "You must provide a valid keystore password to the keystore and a valid address.%n" - + "If the keystore is not at the default location also the keystore filename.") -public class AccountFund implements Runnable { - - private static final Logger log = LoggerFactory.getLogger(AccountFund.class); - - @ParentCommand - private Account accountParent; - - @Option(names={"-i", "--index"}, - defaultValue="-1", - description="Keystore index of the public/private key to use to create an account.") - private int keystoreIndex; - - @Option(names={"--public-key"}, - defaultValue="", - description="Hex string of the public key in the Keystore to use to create an account.%n" - + "You only need to enter in the first distinct hex values of the public key.%n" - + "For example: 0xf0234 or f0234") - private String keystorePublicKey; - - @Option(names={"--port"}, - description="Port number to connect to a peer.") - private int port = 0; - - @Option(names={"--host"}, - defaultValue=Constants.HOSTNAME_PEER, - description="Hostname to connect to a peer. Default: ${DEFAULT-VALUE}") - private String hostname; - - @Option(names={"-a", "--address"}, - description="Account address to use to request funds.") - private long addressNumber; - - - @Parameters(paramLabel="amount", - defaultValue=""+Constants.ACCOUNT_FUND_AMOUNT, - description="Amount to fund the account") - private long amount; - - @Override - public void run() { - - Main mainParent = accountParent.mainParent; - - AKeyPair keyPair = null; - try { - keyPair = mainParent.loadKeyFromStore(keystorePublicKey, keystoreIndex); - } catch (Error e) { - mainParent.showError(e); - return; - } - - if (addressNumber == 0) { - log.warn("--address. You need to provide a valid address number"); - return; - } - - Convex convex = null; - Address address = Address.create(addressNumber); - try { - convex = mainParent.connectAsPeer(0); - convex.transferSync(address, amount); - convex = mainParent.connectToSessionPeer(hostname, port, address, keyPair); - Long balance = convex.getBalance(address); - mainParent.output.setField("Balance", balance); - } catch (Throwable t) { - mainParent.showError(t); - } - } -} diff --git a/convex-cli/src/main/java/convex/cli/AccountInformation.java b/convex-cli/src/main/java/convex/cli/AccountInformation.java deleted file mode 100644 index 0ac08f9e5..000000000 --- a/convex-cli/src/main/java/convex/cli/AccountInformation.java +++ /dev/null @@ -1,77 +0,0 @@ -package convex.cli; - -import convex.api.Convex; -import convex.core.Result; -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.lang.Reader; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine.Command; -import picocli.CommandLine.Option; -import picocli.CommandLine.Parameters; -import picocli.CommandLine.ParentCommand; - -/** - * - * Convex account infomation command - * - * convex.account.infomation - * - */ - -@Command(name="information", - aliases={"info", "in"}, - mixinStandardHelpOptions=true, - description="Get account information.") -public class AccountInformation implements Runnable { - - private static final Logger log = LoggerFactory.getLogger(AccountInformation.class); - - @ParentCommand - private Account accountParent; - - @Option(names={"--port"}, - description="Port number to connect to a peer.") - private int port = 0; - - @Option(names={"--host"}, - defaultValue=Constants.HOSTNAME_PEER, - description="Hostname to connect to a peer. Default: ${DEFAULT-VALUE}") - private String hostname; - - - @Parameters(paramLabel="address", - description="Address of the account to get information.") - private long addressNumber; - - @Option(names={"-t", "--timeout"}, - description="Timeout in miliseconds.") - private long timeout = Constants.DEFAULT_TIMEOUT_MILLIS; - - - @Override - public void run() { - - Main mainParent = accountParent.mainParent; - - if (addressNumber == 0) { - log.warn("You need to provide a valid address number"); - return; - } - - Convex convex = null; - Address address = Address.create(addressNumber); - try { - convex = mainParent.connectToSessionPeer(hostname, port, address, null); - String queryCommand = String.format("(account #%d)", address.longValue()); - ACell message = Reader.read(queryCommand); - Result result = convex.querySync(message, timeout); - mainParent.output.setResult(result); - } catch (Throwable t) { - mainParent.showError(t); - } - - } -} diff --git a/convex-cli/src/main/java/convex/cli/Constants.java b/convex-cli/src/main/java/convex/cli/Constants.java deleted file mode 100644 index 640a574e1..000000000 --- a/convex-cli/src/main/java/convex/cli/Constants.java +++ /dev/null @@ -1,27 +0,0 @@ -package convex.cli; - - -/** -* -* Static constants for the CLI interface -* -*/ -public class Constants { - - public static final String HOSTNAME_REMOTE = "convex.world"; - - public static final String HOSTNAME_PEER = "localhost"; - - public static final String KEYSTORE_FILENAME = "~/.convex/keystore.pfx"; - - public static final int KEY_GENERATE_COUNT = 1; - - public static final String SESSION_FILENAME = "~/.convex/session.conf"; - - public static final int ACCOUNT_FUND_AMOUNT = 100000000; - - public static final int LOCAL_START_PEER_COUNT = 4; - - public static final long DEFAULT_TIMEOUT_MILLIS = 5000; - -} diff --git a/convex-cli/src/main/java/convex/cli/Helpers.java b/convex-cli/src/main/java/convex/cli/Helpers.java deleted file mode 100644 index 60b7f88b8..000000000 --- a/convex-cli/src/main/java/convex/cli/Helpers.java +++ /dev/null @@ -1,114 +0,0 @@ -package convex.cli; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; - -import convex.cli.peer.Session; -import convex.cli.peer.SessionItem; - -/** - * - * Helpers - * - * Helper functions for the CLI classes. - * -*/ -public class Helpers { - - /** - * Expand a path string with a '~'. The tilde is expanded to the users home path. - * - * @param path Path string to expand. - * - * @return Expanded string if a tilde is present. - * - */ - public static String expandTilde(String path) { - if (path!=null) { - return path.replaceFirst("^~", System.getProperty("user.home")); - } - return null; - } - - /** - * Create a path from a File object. This is to provide a feature to add the - * default `.convex` folder if it does not exist. - * - * @param file File object to see if the path part of the filename exists, if not then create it. - * - */ - public static void createPath(File file) { - File path = file.getParentFile(); - if (!path.exists()) { - path.mkdir(); - } - } - - /** - * Return a random session hostname, by looking at the session file. - * The session file has a list of local peers open. - * This helper will find a random peer in the collection and returns hostname. - * - * @param sessionFilename Session filename to open and get the random port nummber. - * - * @return A random hostname or null if none can be found - * @throws IOException - * - */ - public static SessionItem getSessionItem(String sessionFilename) throws IOException { - return getSessionItem(sessionFilename, -1); - } - - /** - * Return an indexed session item, by looking at the session file. - * The session file has a list of local peers open. - * This helper will find a random peer in the collection and returns session item. - * - * @param sessionFilename Session filename to open and get the random port nummber. - * - * @param index The index of the peer in the session list or if -1 a random selection is made. - * - * @return A random session item or null if none can be found - * @throws IOException - * - */ - public static SessionItem getSessionItem(String sessionFilename, int index) throws IOException { - SessionItem item = null; - Session session = new Session(); - Random random = new Random(); - File sessionFile = new File(sessionFilename); - session.load(sessionFile); - int sessionCount = session.getSize(); - if (sessionCount > 0) { - if (index < 0) { - index = random.nextInt(sessionCount - 1); - } - item = session.getItemFromIndex(index); - } - return item; - } - - public static List splitArrayParameter(String[] parameterValue) { - List result = new ArrayList<>(parameterValue.length); - for (int index = 0; index < parameterValue.length; index ++) { - String value = parameterValue[index]; - String[] items = new String[1]; - items[0] = value; - if (value.indexOf(",") > 0) { - items = value.split(","); - } - for (int itemIndex = 0; itemIndex < items.length; itemIndex ++ ) { - String newValue = items[itemIndex].trim(); - if (newValue.length() > 0) { - result.add(newValue); - } - } - } - return result; - } -} - - diff --git a/convex-cli/src/main/java/convex/cli/Key.java b/convex-cli/src/main/java/convex/cli/Key.java deleted file mode 100644 index 081a9127b..000000000 --- a/convex-cli/src/main/java/convex/cli/Key.java +++ /dev/null @@ -1,36 +0,0 @@ -package convex.cli; - -import picocli.CommandLine; -import picocli.CommandLine.Command; -import picocli.CommandLine.ParentCommand; - -/** - * - * Convex key sub commands - * - * convex.key - * - */ -@Command(name="key", - aliases={"ke"}, - subcommands = { - KeyImport.class, - KeyGenerate.class, - KeyList.class, - KeyExport.class, - CommandLine.HelpCommand.class - }, - mixinStandardHelpOptions=true, - description="Manage local Convex key store.") -public class Key implements Runnable { - - @ParentCommand - protected Main mainParent; - - @Override - public void run() { - // sub command run with no command provided - CommandLine.usage(new Key(), System.out); - } -} - diff --git a/convex-cli/src/main/java/convex/cli/KeyExport.java b/convex-cli/src/main/java/convex/cli/KeyExport.java deleted file mode 100644 index 761feaf12..000000000 --- a/convex-cli/src/main/java/convex/cli/KeyExport.java +++ /dev/null @@ -1,98 +0,0 @@ -package convex.cli; - - - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine.Command; -import picocli.CommandLine.ParentCommand; -import picocli.CommandLine.Option; - -import convex.core.crypto.AKeyPair; -import convex.core.crypto.PEMTools; - - -/** - * - * Convex key sub commands - * - * convex.key.export - * - * - */ -@Command(name="export", - aliases={"ex"}, - mixinStandardHelpOptions=true, - description="Export 1 or more key pairs from the keystore.") -public class KeyExport implements Runnable { - - private static final Logger log = LoggerFactory.getLogger(KeyExport.class); - - @ParentCommand - protected Key keyParent; - - @Option(names={"-i", "--index-key"}, - description="Keystore index of the public/private key to use for the peer.") - - private int[] keystoreIndex; - - @Option(names = {"--public-key" }, - description = "Hex string of the public key in the Keystore to use for the peer.%n" - + "You only need to enter in the first distinct hex values of the public key.%n" - + "For example: 0xf0234 or f0234") - private String[] keystorePublicKey ; - - @Option(names={"--export-password"}, - description="Password of the exported key.") - private String exportPassword; - - - @Override - public void run() { - // sub command to generate keys - Main mainParent = keyParent.mainParent; - - if (keystoreIndex == null && keystorePublicKey == null) { - log.warn("You need to provide at least on --index-key or --public-key parameter"); - return; - } - - if (exportPassword == null || exportPassword.length() == 0) { - log.warn("You need to provide an export password '--export-password' of the exported key"); - return; - } - - try { - int index = 0; - int count = 0; - if (keystoreIndex != null) { - count = keystoreIndex.length; - } - if (keystorePublicKey != null ) { - count = keystorePublicKey.length; - } - while (index < count) { - String publicKey = null; - int indexKey = 0; - if (keystoreIndex != null) { - indexKey = keystoreIndex[index]; - } - if (keystorePublicKey != null) { - publicKey = keystorePublicKey[index]; - } - AKeyPair keyPair = mainParent.loadKeyFromStore(publicKey, indexKey); - String pemText = PEMTools.encryptPrivateKeyToPEM(keyPair.getPrivate(), exportPassword.toCharArray()); - - mainParent.output.setField("index", String.format("%5d", index + 1)); - mainParent.output.setField("publicKey", keyPair.getAccountKey().toHexString()); - mainParent.output.setField("export", pemText); - mainParent.output.addRow(); - - index ++; - } - - } catch (Error e) { - mainParent.showError(e); - } - } -} diff --git a/convex-cli/src/main/java/convex/cli/KeyGenerate.java b/convex-cli/src/main/java/convex/cli/KeyGenerate.java deleted file mode 100644 index 7ff709767..000000000 --- a/convex-cli/src/main/java/convex/cli/KeyGenerate.java +++ /dev/null @@ -1,62 +0,0 @@ -package convex.cli; - -import java.util.List; - -import convex.core.crypto.AKeyPair; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine.Command; -import picocli.CommandLine.Parameters; -import picocli.CommandLine.ParentCommand; - - -/** - * - * Convex key sub commands - * - * convex.key.generate - * - * - */ -@Command(name="generate", - aliases={"ge"}, - mixinStandardHelpOptions=true, - description="Generate 1 or more private key pairs.") -public class KeyGenerate implements Runnable { - - private static final Logger log = LoggerFactory.getLogger(KeyGenerate.class); - - @ParentCommand - protected Key keyParent; - - - @Parameters(paramLabel="count", - defaultValue="" + Constants.KEY_GENERATE_COUNT, - description="Number of keys to generate. Default: ${DEFAULT-VALUE}") - private int count; - - @Override - public void run() { - // sub command to generate keys - Main mainParent = keyParent.mainParent; - // check the number of keys to generate. - if (count <= 0) { - log.warn("You to provide 1 or more count of keys to generate"); - return; - } - log.info("Generating {} keys",count); - - try { - List keyPairList = mainParent.generateKeyPairs(count); - for ( int index = 0; index < keyPairList.size(); index ++) { - String publicKeyHexString = keyPairList.get(index).getAccountKey().toHexString(); - mainParent.output.setField("Index", String.format("%5d", index)); - mainParent.output.setField("Public Key", publicKeyHexString); - mainParent.output.addRow(); - } - } catch (Error e) { - mainParent.showError(e); - } - } -} diff --git a/convex-cli/src/main/java/convex/cli/KeyImport.java b/convex-cli/src/main/java/convex/cli/KeyImport.java deleted file mode 100644 index 4d36f11d4..000000000 --- a/convex-cli/src/main/java/convex/cli/KeyImport.java +++ /dev/null @@ -1,83 +0,0 @@ -package convex.cli; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.charset.StandardCharsets; -import java.security.PrivateKey; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine.Command; -import picocli.CommandLine.ParentCommand; -import picocli.CommandLine.Option; - -import convex.core.crypto.AKeyPair; -import convex.core.crypto.Ed25519KeyPair; -import convex.core.crypto.PEMTools; - - -/** - * - * Convex key sub commands - * - * convex.key.import - * - * - */ -@Command(name="import", - aliases={"im"}, - mixinStandardHelpOptions=true, - description="Import key pairs to the keystore.") -public class KeyImport implements Runnable { - - private static final Logger log = LoggerFactory.getLogger(KeyImport.class); - - @ParentCommand - protected Key keyParent; - - @Option(names={"-i", "--import-text"}, - description="Import format PEM text of the keypair.") - private String importText; - - - @Option(names={"-f", "--import-file"}, - description="Import file name of the keypair PEM file.") - private String importFilename; - - @Option(names={"--import-password"}, - description="Password of the imported key.") - private String importPassword; - - @Override - public void run() { - // sub command to generate keys - Main mainParent = keyParent.mainParent; - if (importFilename != null && importFilename.length() > 0) { - try { - importText = Files.readString(Paths.get(importFilename), StandardCharsets.UTF_8); - } catch ( IOException e) { - mainParent.showError(e); - return; - } - } - if (importText == null || importText.length() == 0) { - log.warn("You need to provide an import text '--import' or import filename '--import-file' to import a private key"); - return; - } - - if (importPassword == null || importPassword.length() == 0) { - log.warn("You need to provide an import password '--import-password' of the imported encrypted PEM data"); - } - - try { - PrivateKey privateKey = PEMTools.decryptPrivateKeyFromPEM(importText, importPassword.toCharArray()); - AKeyPair keyPair = Ed25519KeyPair.create(privateKey); - mainParent.addKeyPairToStore(keyPair); - mainParent.output.setField("public key", keyPair.getAccountKey().toHexString()); - - } catch (Error e) { - mainParent.showError(e); - } - } -} diff --git a/convex-cli/src/main/java/convex/cli/KeyList.java b/convex-cli/src/main/java/convex/cli/KeyList.java deleted file mode 100644 index 07b2ddb7d..000000000 --- a/convex-cli/src/main/java/convex/cli/KeyList.java +++ /dev/null @@ -1,63 +0,0 @@ -package convex.cli; - -import java.io.File; -import java.security.KeyStore; -import java.util.Enumeration; - -import convex.core.crypto.PFXTools; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine.Command; -import picocli.CommandLine.ParentCommand; - -/** - * - * Convex key sub commands - * - * convex.key.list - * - * - */ -@Command(name="list", - aliases={"li"}, - mixinStandardHelpOptions=true, - description="List available key pairs.") -public class KeyList implements Runnable { - - private static final Logger log = LoggerFactory.getLogger(KeyList.class); - - @ParentCommand - protected Key keyParent; - - @Override - public void run() { - Main mainParent = keyParent.mainParent; - - String password = mainParent.getPassword(); - if (password == null) { - log.warn("You need to provide a keystore password"); - return; - } - File keyFile = new File(mainParent.getKeyStoreFilename()); - try { - if (!keyFile.exists()) { - log.error("Cannot find keystore file {}", keyFile.getCanonicalPath()); - } - KeyStore keyStore = PFXTools.loadStore(keyFile, password); - Enumeration aliases = keyStore.aliases(); - int index = 1; - while (aliases.hasMoreElements()) { - String alias = aliases.nextElement(); - mainParent.output.setField("Index", String.format("%5d", index)); - mainParent.output.setField("Public Key", alias); - mainParent.output.addRow(); - index ++; - } - - } catch (Throwable t) { - mainParent.showError(t); - } - } - -} diff --git a/convex-cli/src/main/java/convex/cli/Local.java b/convex-cli/src/main/java/convex/cli/Local.java deleted file mode 100644 index f1c293dda..000000000 --- a/convex-cli/src/main/java/convex/cli/Local.java +++ /dev/null @@ -1,35 +0,0 @@ -package convex.cli; - -import picocli.CommandLine; -import picocli.CommandLine.Command; -import picocli.CommandLine.ParentCommand; - - -/** - * - * Convex local sub commands - * - * convex.local - * - * - */ -@Command(name="local", - aliases={"lo"}, - subcommands = { - LocalGUI.class, - LocalStart.class, - CommandLine.HelpCommand.class - }, - mixinStandardHelpOptions=true, - description="Operates a local convex network.") -public class Local implements Runnable { - - @ParentCommand - protected Main mainParent; - - @Override - public void run() { - // sub command run with no command provided - CommandLine.usage(new Local(), System.out); - } -} diff --git a/convex-cli/src/main/java/convex/cli/LocalGUI.java b/convex-cli/src/main/java/convex/cli/LocalGUI.java deleted file mode 100644 index 031bc52ee..000000000 --- a/convex-cli/src/main/java/convex/cli/LocalGUI.java +++ /dev/null @@ -1,41 +0,0 @@ -package convex.cli; - -import convex.api.Applications; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine.Command; -import picocli.CommandLine.ParentCommand; - -/** - * - * Convex Local Manager sub command - * - * convex.local.manager - * - * - */ -@Command(name="gui", - aliases={}, - mixinStandardHelpOptions=true, - description="Starts a local convex test network using the peer manager GUI application.") -public class LocalGUI implements Runnable { - - private static final Logger log = LoggerFactory.getLogger(LocalGUI.class); - - @ParentCommand - protected Local localParent; - - @Override - public void run() { - Main mainParent = localParent.mainParent; - - log.warn("You will not be able to use some of the CLI 'account' and 'peer' commands."); - // sub command to launch peer manager - try { - Applications.launchApp(convex.gui.manager.PeerGUI.class); - } catch (Throwable t) { - mainParent.showError(t); - } - } -} diff --git a/convex-cli/src/main/java/convex/cli/LocalStart.java b/convex-cli/src/main/java/convex/cli/LocalStart.java deleted file mode 100644 index 80974b8bb..000000000 --- a/convex-cli/src/main/java/convex/cli/LocalStart.java +++ /dev/null @@ -1,129 +0,0 @@ -package convex.cli; - -import java.lang.NumberFormatException; -import java.util.ArrayList; -import java.util.List; - -import convex.cli.peer.PeerManager; -import convex.core.crypto.AKeyPair; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine.Command; -import picocli.CommandLine.Option; -import picocli.CommandLine.ParentCommand; - -/* - * local start command - * - * convex.local.start - * - */ - -@Command(name="start", - aliases={"st"}, - mixinStandardHelpOptions=true, - description="Starts a local convex test network.") -public class LocalStart implements Runnable { - - private static final Logger log = LoggerFactory.getLogger(LocalStart.class); - - @ParentCommand - private Local localParent; - - @Option(names={"--count"}, - defaultValue = "" + Constants.LOCAL_START_PEER_COUNT, - description="Number of local peers to start. Default: ${DEFAULT-VALUE}") - private int count; - - @Option(names={"-i", "--index-key"}, - defaultValue="0", - description="One or more keystore index of the public/private key to use to run a peer.") - private String[] keystoreIndex; - - @Option(names={"--public-key"}, - defaultValue="", - description="One or more hex string of the public key in the Keystore to use to run a peer.%n" - + "You only need to enter in the first distinct hex values of the public key.%n" - + "For example: 0xf0234 or f0234") - private String[] keystorePublicKey; - - @Option(names={"--ports"}, - description="Range or list of ports to assign each peer in the cluster. This can be a multiple of --ports %n" - + "or a single --ports=8081,8082,8083 or --ports=8080-8090") - private String[] ports; - - @Override - public void run() { - Main mainParent = localParent.mainParent; - PeerManager peerManager = PeerManager.create(mainParent.getSessionFilename()); - - List keyPairList = new ArrayList(); - - // load in the list of public keys to use as peers - if (keystorePublicKey.length > 0) { - List values = Helpers.splitArrayParameter(keystorePublicKey); - for (int index = 0; index < values.size(); index ++) { - String publicKeyText = values.get(index); - try { - AKeyPair keyPair = mainParent.loadKeyFromStore(publicKeyText, 0); - if (keyPair != null) { - keyPairList.add(keyPair); - } - } catch (Error e) { - mainParent.showError(e); - return; - } - } - } - - // load in a list of key indexes to use as peers - if (keystoreIndex.length > 0) { - List values = Helpers.splitArrayParameter(keystoreIndex); - for (int index = 0; index < values.size(); index ++) { - int indexKey = Integer.parseInt(values.get(index)); - if (indexKey > 0) { - try { - AKeyPair keyPair = mainParent.loadKeyFromStore("", indexKey); - if (keyPair != null) { - keyPairList.add(keyPair); - } - } catch (Error e) { - mainParent.showError(e); - return; - } - } - } - } - - if (keyPairList.size() == 0) { - keyPairList = mainParent.generateKeyPairs(count); - } - - if (count > keyPairList.size()) { - log.error( - "Not enougth public keys provided. " + - "You have requested {} peers to start, but only provided {} public keys", - count, - keyPairList.size() - ); - } - int peerPorts[] = null; - if (ports != null) { - try { - peerPorts = mainParent.getPortList(ports, count); - } catch (NumberFormatException e) { - log.warn("cannot convert port number " + e); - return; - } - if (peerPorts.length < count) { - log.warn("you need only provided {} ports you need to provide at least {} ports", peerPorts.length, count); - return; - } - } - log.info("Starting local network with "+count+" peer(s)"); - peerManager.launchLocalPeers(keyPairList, peerPorts); - log.info("Local Peers launched"); - peerManager.showPeerEvents(); - } -} diff --git a/convex-cli/src/main/java/convex/cli/Main.java b/convex-cli/src/main/java/convex/cli/Main.java deleted file mode 100644 index 7b55726fa..000000000 --- a/convex-cli/src/main/java/convex/cli/Main.java +++ /dev/null @@ -1,389 +0,0 @@ -package convex.cli; - -import java.io.File; -import java.lang.NumberFormatException; -import java.net.InetSocketAddress; -import java.security.KeyStore; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - - -import convex.api.Convex; -import convex.cli.peer.SessionItem; -import convex.cli.output.Output; -import convex.core.crypto.AKeyPair; -import convex.core.crypto.PFXTools; -import convex.core.data.AccountKey; -import convex.core.data.Address; -import convex.core.init.Init; - - -import ch.qos.logback.classic.Level; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine; -import picocli.CommandLine.Command; -import picocli.CommandLine.Option; -import picocli.CommandLine.PropertiesDefaultProvider; -import picocli.CommandLine.ScopeType; - -/** -* Convex CLI implementation -*/ -@Command(name="convex", - subcommands = { - Account.class, - Key.class, - Local.class, - Peer.class, - Query.class, - Status.class, - Transaction.class, - CommandLine.HelpCommand.class - }, - mixinStandardHelpOptions=true, - usageHelpAutoWidth=true, - sortOptions = false, - // headerHeading = "Usage:", - // synopsisHeading = "%n", - descriptionHeading = "%nDescription:%n%n", - parameterListHeading = "%nParameters:%n", - optionListHeading = "%nOptions:%n", - commandListHeading = "%nCommands:%n", - description="Convex Command Line Interface") - -public class Main implements Runnable { - - private static Logger log = LoggerFactory.getLogger(Main.class); - - - private static CommandLine commandLine; - public Output output; - - @Option(names={ "-c", "--config"}, - scope = ScopeType.INHERIT, - description="Use the specified config file.%n All parameters to this app can be set by removing the leading '--', and adding" - + " a leading 'convex.'.%n So to set the keystore filename you can write 'convex.keystore=my_keystore_filename.dat'%n" - + "To set a sub command such as `./convex peer start index=4` index parameter you need to write 'convex.peer.start.index=4'") - private String configFilename; - - @Option(names={"-e", "--etch"}, - scope = ScopeType.INHERIT, - description="Convex state storage filename. The default is to use a temporary storage filename.") - private String etchStoreFilename; - - @Option(names={"-k", "--keystore"}, - defaultValue=Constants.KEYSTORE_FILENAME, - scope = ScopeType.INHERIT, - description="keystore filename. Default: ${DEFAULT-VALUE}") - private String keyStoreFilename; - - @Option(names={"-p", "--password"}, - scope = ScopeType.INHERIT, - //defaultValue="", - description="Password to read/write to the Keystore") - private String password; - - @Option(names={"-s", "--session"}, - defaultValue=Constants.SESSION_FILENAME, - scope = ScopeType.INHERIT, - description="Session filename. Defaults ${DEFAULT-VALUE}") - private String sessionFilename; - - @Option(names={ "-v", "--verbose"}, - scope = ScopeType.INHERIT, - description="Show more verbose log information. You can increase verbosity by using multiple -v or -vvv") - private boolean[] verbose = new boolean[0]; - - - public Main() { - output = new Output(); - } - - @Override - public void run() { - // no command provided - so show help - CommandLine.usage(new Main(), System.out); - } - - public static void main(String[] args) { - Main mainApp = new Main(); - int result = mainApp.execute(args); - System.exit(result); - } - - public int execute(String[] args) { - commandLine = new CommandLine(this) - .setUsageHelpLongOptionsMaxWidth(40) - .setUsageHelpWidth(40 * 4); - - // do a pre-parse to get the config filename. We need to load - // in the defaults before running the full execute - try { - commandLine.parseArgs(args); - loadConfig(); - } catch (Throwable t) { - System.err.println("unable to parse arguments " + t); - } - - ch.qos.logback.classic.Logger parentLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); - - Level[] verboseLevels = {Level.WARN, Level.INFO, Level.DEBUG, Level.TRACE, Level.ALL}; - - parentLogger.setLevel(Level.WARN); - if (verbose.length > 0 && verbose.length <= verboseLevels.length) { - parentLogger.setLevel(verboseLevels[verbose.length]); - log.info("set level to {}", parentLogger.getLevel()); - } - - int result = 0; - try { - result = commandLine.execute(args); - output.writeToStream(commandLine.getOut()); - - } catch (Throwable t) { - log.error("Error executing command line: {}",t.getMessage()); - return 2; - } - return result; - } - - protected void loadConfig() { - if (configFilename != null && !configFilename.isEmpty()) { - String filename = Helpers.expandTilde(configFilename); - File configFile = new File(filename); - if (configFile.exists()) { - PropertiesDefaultProvider defaultProvider = new PropertiesDefaultProvider(configFile); - commandLine.setDefaultValueProvider(defaultProvider); - } - } - } - - public String getSessionFilename() { - if (sessionFilename != null) { - return Helpers.expandTilde(sessionFilename.strip()); - } - return null; - } - - public String getPassword() { - return password; - } - - public String getKeyStoreFilename() { - if ( keyStoreFilename != null) { - return Helpers.expandTilde(keyStoreFilename).strip(); - } - return null; - } - - public String getEtchStoreFilename() { - if ( etchStoreFilename != null) { - return Helpers.expandTilde(etchStoreFilename).strip(); - } - return null; - } - - public KeyStore loadKeyStore(boolean isCreate) throws Error { - KeyStore keyStore = null; - if (password == null || password.isEmpty()) { - throw new Error("You need to provide a keystore password"); - } - File keyFile = new File(getKeyStoreFilename()); - try { - if (keyFile.exists()) { - keyStore = PFXTools.loadStore(keyFile, password); - } - else { - if (isCreate) { - Helpers.createPath(keyFile); - keyStore = PFXTools.createStore(keyFile, password); - } - else { - throw new Error("Cannot find keystore file "+keyFile.getCanonicalPath()); - } - } - } catch(Throwable t) { - new Error(t); - } - return keyStore; - } - - public AKeyPair loadKeyFromStore(String publicKey, int indexKey) throws Error { - - AKeyPair keyPair = null; - - String publicKeyClean = ""; - if (publicKey != null) { - publicKeyClean = publicKey.toLowerCase().replaceAll("^0x", "").strip(); - } - - if ( publicKeyClean.isEmpty() && indexKey <= 0) { - return null; - } - - String searchText = publicKeyClean; - if (indexKey > 0) { - searchText += " " + indexKey; - } - if (password == null || password.isEmpty()) { - throw new Error("You need to provide a keystore password"); - } - - - File keyFile = new File(getKeyStoreFilename()); - try { - if (!keyFile.exists()) { - throw new Error("Cannot find keystore file "+keyFile.getCanonicalPath()); - } - KeyStore keyStore = PFXTools.loadStore(keyFile, password); - - int counter = 1; - Enumeration aliases = keyStore.aliases(); - - while (aliases.hasMoreElements()) { - String alias = aliases.nextElement(); - if (counter == indexKey || alias.indexOf(publicKeyClean) == 0) { - log.trace("found keypair " + indexKey + " " + counter + " " + alias + " " + publicKeyClean + " " + alias.indexOf(publicKeyClean)); - keyPair = PFXTools.getKeyPair(keyStore, alias, password); - break; - } - counter ++; - } - } catch (Throwable t) { - throw new Error("Cannot load key store "+t); - } - - if (keyPair==null) { - throw new Error("Cannot find key in keystore '" + searchText + "'"); - } - return keyPair; - } - - public Convex connectToSessionPeer(String hostname, int port, Address address, AKeyPair keyPair) throws Error { - SessionItem item; - Convex convex = null; - try { - if (port == 0) { - item = Helpers.getSessionItem(getSessionFilename()); - if (item != null) { - port = item.getPort(); - } - } - if (port == 0) { - throw new Error("Cannot find a local port or you have not set a valid port number"); - } - InetSocketAddress host=new InetSocketAddress(hostname.strip(), port); - convex = Convex.connect(host, address, keyPair); - } catch (Throwable t) { - throw new Error("Cannot connect to a local peer " + t); - } - return convex; - } - - public Convex connectAsPeer(int peerIndex) throws Error { - Convex convex = null; - try { - SessionItem item = Helpers.getSessionItem(getSessionFilename(), peerIndex); - AccountKey peerKey = item.getAccountKey(); - log.debug("peer public key {}", peerKey.toHexString()); - AKeyPair keyPair = loadKeyFromStore(peerKey.toHexString(), 0); - log.debug("peer key pair {}", keyPair.getAccountKey().toHexString()); - Address address = Init.getGenesisPeerAddress(peerIndex); - log.debug("peer address {}", address); - InetSocketAddress host = item.getHostAddress(); - log.debug("connect to peer {}", host); - convex = Convex.connect(host, address, keyPair); - } catch (Throwable t) { - throw new Error("Cannot connect as a peer " + t); - } - return convex; - } - - public List generateKeyPairs(int count) throws Error { - List keyPairList = new ArrayList<>(count); - - // generate `count` keys - for (int index = 0; index < count; index ++) { - AKeyPair keyPair = AKeyPair.generate(); - keyPairList.add(keyPair); - addKeyPairToStore(keyPair); - } - - return keyPairList; - } - - public void addKeyPairToStore(AKeyPair keyPair) { - // get the password of the key store file - String password = getPassword(); - if (password == null) { - throw new Error("You need to provide a keystore password"); - } - // get the key store file - File keyFile = new File(getKeyStoreFilename()); - - KeyStore keyStore = null; - try { - // try to load the keystore file - if (keyFile.exists()) { - keyStore = PFXTools.loadStore(keyFile, password); - } else { - // create the path to the new key file - Helpers.createPath(keyFile); - keyStore = PFXTools.createStore(keyFile, password); - } - } catch (Throwable t) { - throw new Error("Cannot load key store: "+t); - } - try { - // save the key in the keystore - PFXTools.setKeyPair(keyStore, keyPair, password); - } catch (Throwable t) { - throw new Error("Cannot store the key to the key store "+t); - } - // save the keystore file - try { - PFXTools.saveStore(keyStore, keyFile, password); - } catch (Throwable t) { - throw new Error("Cannot save the key store file "+t); - } - } - - int[] getPortList(String ports[], int count) throws NumberFormatException { - Pattern rangePattern = Pattern.compile(("([0-9]+)\\s*-\\s*([0-9]*)")); - List portTextList = Helpers.splitArrayParameter(ports); - List portList = new ArrayList(); - int countLeft = count; - for (int index = 0; index < portTextList.size() && countLeft > 0; index ++) { - String item = portTextList.get(index); - Matcher matcher = rangePattern.matcher(item); - if (matcher.matches()) { - int portFrom = Integer.parseInt(matcher.group(1)); - int portTo = portFrom + count + 1; - if (!matcher.group(2).isEmpty()) { - portTo = Integer.parseInt(matcher.group(2)); - } - for ( int portIndex = portFrom; portIndex <= portTo && countLeft > 0; portIndex ++, --countLeft ) { - portList.add(portIndex); - } - } - else if (item.strip().length() == 0) { - } - else { - portList.add(Integer.parseInt(item)); - countLeft --; - } - } - return portList.stream().mapToInt(Integer::intValue).toArray(); - } - - void showError(Throwable t) { - log.error(t.getMessage()); - if (verbose.length > 0) { - t.printStackTrace(); - } - } -} diff --git a/convex-cli/src/main/java/convex/cli/Peer.java b/convex-cli/src/main/java/convex/cli/Peer.java deleted file mode 100644 index fe54efaa7..000000000 --- a/convex-cli/src/main/java/convex/cli/Peer.java +++ /dev/null @@ -1,37 +0,0 @@ -package convex.cli; - -import picocli.CommandLine; -import picocli.CommandLine.Command; -import picocli.CommandLine.ParentCommand; - - -/** - * - * Convex peer sub commands - * - * convex.peer - * - */ -@Command(name="peer", - aliases={"pe"}, - subcommands = { - PeerCreate.class, - PeerStart.class, - CommandLine.HelpCommand.class - }, - mixinStandardHelpOptions=true, - description="Operates a local peer.") -public class Peer implements Runnable { - - // private static final Logger log = Logger.getLogger(Peer.class.getName()); - - @ParentCommand - protected Main mainParent; - - @Override - public void run() { - // sub command run with no command provided - CommandLine.usage(new Peer(), System.out); - } - -} diff --git a/convex-cli/src/main/java/convex/cli/PeerCreate.java b/convex-cli/src/main/java/convex/cli/PeerCreate.java deleted file mode 100644 index 10cadfbcc..000000000 --- a/convex-cli/src/main/java/convex/cli/PeerCreate.java +++ /dev/null @@ -1,146 +0,0 @@ -package convex.cli; - -import java.io.File; -import java.security.KeyStore; - -import convex.api.Convex; -import convex.core.crypto.AKeyPair; -import convex.core.crypto.PFXTools; -import convex.core.data.Address; -import convex.core.data.ACell; -import convex.core.lang.Reader; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Invoke; -import convex.core.Result; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine.Command; -import picocli.CommandLine.Model.CommandSpec; -import picocli.CommandLine.Option; -import picocli.CommandLine.ParentCommand; -import picocli.CommandLine.Spec; - -/** - * peer create command - * - * convex.peer.create - * - * This creates an account and provides enougth funds, for a new peer account - * - * - */ - -@Command(name="create", - aliases={"cr"}, - mixinStandardHelpOptions=true, - description="Creates a keypair, new account and a funding stake: to run a local peer.") -public class PeerCreate implements Runnable { - - private static final Logger log = LoggerFactory.getLogger(PeerCreate.class); - - @ParentCommand - private Peer peerParent; - - @Spec CommandSpec spec; - - @Option(names={"-i", "--index-key"}, - defaultValue="0", - description="Keystore index of the public/private key to use for the peer.") - private int keystoreIndex; - - @Option(names={"--public-key"}, - defaultValue="", - description="Hex string of the public key in the Keystore to use for the peer.%n" - + "You only need to enter in the first distinct hex values of the public key.%n" - + "For example: 0xf0234 or f0234") - private String keystorePublicKey; - - @Option(names={"--port"}, - description="Port number of nearest peer to connect too.") - private int port = 0; - - @Option(names={"--host"}, - defaultValue=Constants.HOSTNAME_PEER, - description="Hostname to connect to a peer. Default: ${DEFAULT-VALUE}") - private String hostname; - - @Option(names={"-t", "--timeout"}, - description="Timeout in miliseconds.") - private long timeout = Constants.DEFAULT_TIMEOUT_MILLIS; - - - @Override - public void run() { - - Main mainParent = peerParent.mainParent; - - int port = 0; - long peerStake = 10000000000L; - - AKeyPair keyPair = null; - KeyStore keyStore; - - try { - // create a keystore if it does not exist - keyStore = mainParent.loadKeyStore(true); - } catch (Error e) { - log.info(e.getMessage()); - return; - } - - try { - keyPair = AKeyPair.generate(); - - // save the new keypair in the keystore - PFXTools.setKeyPair(keyStore, keyPair, mainParent.getPassword()); - - File keyFile = new File(mainParent.getKeyStoreFilename()); - - // save the store to a file - PFXTools.saveStore(keyStore, keyFile, mainParent.getPassword()); - - // connect using the default first user - Convex convex = mainParent.connectAsPeer(0); - // create an account - Address address = convex.createAccountSync(keyPair.getAccountKey()); - convex.transferSync(address, peerStake); - - convex = mainParent.connectToSessionPeer(hostname, port, address, keyPair); - long stakeBalance = convex.getBalance(address); - String accountKeyString = keyPair.getAccountKey().toHexString(); - long stakeAmount = (long) (stakeBalance * 0.98); - - String transactionCommand = String.format("(create-peer 0x%s %d)", accountKeyString, stakeAmount); - ACell message = Reader.read(transactionCommand); - ATransaction transaction = Invoke.create(address, -1, message); - Result result = convex.transactSync(transaction, timeout); - if (result.isError()) { - mainParent.output.setResult(result); - return; - } - long currentBalance = convex.getBalance(address); - - mainParent.output.setField("Public Peer Key", keyPair.getAccountKey().toString()); - mainParent.output.setField("Address", address.longValue()); - mainParent.output.setField("Balance", currentBalance); - mainParent.output.setField("Inital stake amount", stakeAmount); - String shortAccountKey = accountKeyString.substring(0, 6); - // System.out.println("You can now start this peer by executing the following line:\n"); - - // WARNING not sure about showing the users password.. - // to make the starting of peers easier, I have left it in for a simple copy/paste - - mainParent.output.setField("Peer start line", - String.format( - "./convex peer start --password=%s --address=%d --public-key=%s", - mainParent.getPassword(), - address.toLong(), - shortAccountKey - ) - ); - } catch (Throwable t) { - mainParent.showError(t); - } - } -} diff --git a/convex-cli/src/main/java/convex/cli/PeerStart.java b/convex-cli/src/main/java/convex/cli/PeerStart.java deleted file mode 100644 index b7935b889..000000000 --- a/convex-cli/src/main/java/convex/cli/PeerStart.java +++ /dev/null @@ -1,142 +0,0 @@ -package convex.cli; - -import java.io.File; -import java.io.IOException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import convex.cli.peer.PeerManager; -import convex.cli.peer.SessionItem; -import convex.core.crypto.AKeyPair; -import convex.core.data.Address; -import convex.core.store.AStore; -import convex.core.store.Stores; -import etch.EtchStore; -import picocli.CommandLine.Command; -import picocli.CommandLine.Model.CommandSpec; -import picocli.CommandLine.Option; -import picocli.CommandLine.ParentCommand; -import picocli.CommandLine.Spec; - -/** - * peer start command - * - * convex.peer.start - * - */ - -@Command(name = "start", aliases = { "st" }, mixinStandardHelpOptions = true, description = "Starts a local peer.") -public class PeerStart implements Runnable { - - private static final Logger log = LoggerFactory.getLogger(PeerStart.class); - - @ParentCommand - private Peer peerParent; - - @Spec - CommandSpec spec; - - @Option(names={"-i", "--index-key"}, - defaultValue="0", - description="Keystore index of the public/private key to use for the peer.") - - private int keystoreIndex; - - @Option(names = { - "--public-key" }, defaultValue = "", description = "Hex string of the public key in the Keystore to use for the peer.%n" - + "You only need to enter in the first distinct hex values of the public key.%n" - + "For example: 0xf0234 or f0234") - private String keystorePublicKey; - - @Option(names = { "-r", - "--reset" }, description = "Reset and delete the etch database if it exists. Default: ${DEFAULT-VALUE}") - private boolean isReset; - - @Option(names = { "--port" }, description = "Port number of this local peer.") - private int port = 0; - - @Option(names = { - "--host" }, defaultValue=Constants.HOSTNAME_PEER, description = "Hostname of this peer. Default: ${DEFAULT-VALUE}") - private String hostname = Constants.HOSTNAME_PEER; - - @Option(names = { - "--peer" }, description = "Hostname and port number of remote peer. If not provided then try to connect to a local peer") - private String remotePeerHostname; - - @Option(names = { "-a", "--address" }, description = "Account address to use for the peer.") - private long addressNumber; - - @Override - public void run() { - - Main mainParent = peerParent.mainParent; - PeerManager peerManager = null; - - AKeyPair keyPair = null; - try { - keyPair = mainParent.loadKeyFromStore(keystorePublicKey, keystoreIndex); - } catch (Error e) { - mainParent.showError(e); - return; - } - - if (keyPair == null) { - log.warn("cannot load a valid key pair to perform peer start"); - return; - } - - if (port != 0) { - port = Math.abs(port); - } - if ( addressNumber == 0) { - log.warn("please provide an account address to run the peer from."); - return; - } - Address peerAddress = Address.create(addressNumber); - - if (remotePeerHostname == null) { - try { - SessionItem item = Helpers.getSessionItem(mainParent.getSessionFilename()); - if (item != null) { - remotePeerHostname = item.getHostname(); - } - else { - log.warn("Cannot find a local peer to connect too"); - return; - } - } catch (IOException e) { - log.warn("Cannot load the session control file"); - return; - } - } - else { - remotePeerHostname = remotePeerHostname.strip(); - } - if (hostname == null) { - log.warn("you need to provide a host name for this peer"); - return; - } - hostname = hostname.strip(); - - try { - AStore store = null; - String etchStoreFilename = mainParent.getEtchStoreFilename(); - if (etchStoreFilename != null && !etchStoreFilename.isEmpty()) { - File etchFile = new File(etchStoreFilename); - if (isReset && etchFile.exists()) { - log.info("reset: removing old etch storage file {}", etchStoreFilename); - etchFile.delete(); - } - store = EtchStore.create(etchFile); - } else { - store = Stores.getGlobalStore(); - } - peerManager = PeerManager.create(mainParent.getSessionFilename(), keyPair, peerAddress, store); - peerManager.launchPeer(hostname, port, remotePeerHostname); - peerManager.showPeerEvents(); - } catch (Throwable t) { - mainParent.showError(t); - } - } -} diff --git a/convex-cli/src/main/java/convex/cli/Query.java b/convex-cli/src/main/java/convex/cli/Query.java deleted file mode 100644 index 6832122b4..000000000 --- a/convex-cli/src/main/java/convex/cli/Query.java +++ /dev/null @@ -1,82 +0,0 @@ -package convex.cli; - -import java.io.IOException; -import java.util.concurrent.TimeoutException; - -import convex.api.Convex; -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.lang.Reader; -import convex.core.Result; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine.Command; -import picocli.CommandLine.Option; -import picocli.CommandLine.Parameters; -import picocli.CommandLine.ParentCommand; - -/** - * - * Convex Query sub command - * - * convex.query - * - */ -@Command(name="query", - aliases={"qu"}, - mixinStandardHelpOptions=true, - description="Execute a query on the current peer.") -public class Query implements Runnable { - - private static final Logger log = LoggerFactory.getLogger(Query.class); - - @ParentCommand - protected Main mainParent; - - - @Option(names={"--port"}, - description="Port number to connect to a peer.") - private int port = 0; - - @Option(names={"--host"}, - defaultValue=Constants.HOSTNAME_PEER, - description="Hostname to connect to a peer. Default: ${DEFAULT-VALUE}") - private String hostname; - - @Option(names={"-t", "--timeout"}, - description="Timeout in miliseconds.") - private long timeout = Constants.DEFAULT_TIMEOUT_MILLIS; - - @Option(names={"-a", "--address"}, - description = "Address to make the query from. Default: First peer address.") - private long address = 11; - - @Parameters(paramLabel="queryCommand", description="Query Command") - private String queryCommand; - - - @Override - public void run() { - // sub command run with no command provided - log.info("query command: {}", queryCommand); - - Convex convex = null; - - try { - convex = mainParent.connectToSessionPeer(hostname, port, Address.create(address), null); - } catch (Error e) { - mainParent.showError(e); - return; - } - try { - log.info("Executing query: %s\n", queryCommand); - ACell message = Reader.read(queryCommand); - Result result = convex.querySync(message, timeout); - mainParent.output.setResult(result); - } catch (IOException | TimeoutException e) { - mainParent.showError(e); - } - } - -} diff --git a/convex-cli/src/main/java/convex/cli/Status.java b/convex-cli/src/main/java/convex/cli/Status.java deleted file mode 100644 index b5b046a9a..000000000 --- a/convex-cli/src/main/java/convex/cli/Status.java +++ /dev/null @@ -1,108 +0,0 @@ -package convex.cli; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -import convex.api.Convex; -import convex.cli.peer.SessionItem; -import convex.core.Result; -import convex.core.State; -import convex.core.data.ABlob; -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.AccountStatus; -import convex.core.data.BlobMap; -import convex.core.data.Hash; -import convex.core.data.PeerStatus; -import convex.core.store.Stores; -import convex.core.util.Text; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine.Command; -import picocli.CommandLine.Option; -import picocli.CommandLine.ParentCommand; - -/** - * - * Convex Status sub command - * - * convex.status - * - */ -@Command(name="status", - aliases={"st"}, - mixinStandardHelpOptions=true, - description="Reports on the current status of the network.") -public class Status implements Runnable { - - private static final Logger log = LoggerFactory.getLogger(Status.class); - - @ParentCommand - protected Main mainParent; - - @Option(names={"--port"}, - description="Port number to connect or create a peer.") - private int port = 0; - - @Option(names={"--host"}, - defaultValue=Constants.HOSTNAME_PEER, - description="Hostname to connect to a peer. Default: ${DEFAULT-VALUE}") - private String hostname; - - @Option(names={"-t", "--timeout"}, - description="Timeout in miliseconds.") - private long timeout = Constants.DEFAULT_TIMEOUT_MILLIS; - - @SuppressWarnings("unchecked") - @Override - public void run() { - - if (port == 0) { - try { - SessionItem item = Helpers.getSessionItem(mainParent.getSessionFilename()); - port = item.getPort(); - } catch (IOException e) { - log.warn("Cannot load the session control file"); - } - } - if (port == 0) { - log.warn("Cannot find a local port or you have not set a valid port number"); - return; - } - - Convex convex = null; - try { - convex = mainParent.connectAsPeer(0); - } catch (Throwable t) { - mainParent.showError(t); - return; - } - - try { - Result result = convex.requestStatus().get(timeout, TimeUnit.MILLISECONDS); - AVector resultVector = (AVector) result.getValue(); - ABlob stateHash = (ABlob) resultVector.get(1); - Hash hash = Hash.wrap(stateHash.getBytes()); - AVector stateWrapper = (AVector) convex.acquire(hash, Stores.current()).get(3000,TimeUnit.MILLISECONDS); - State state = (State) stateWrapper.get(0); - - state.validate(); - AVector accountList = state.getAccounts(); - BlobMap peerList = state.getPeers(); - - mainParent.output.setField("State hash", stateHash.toString()); - mainParent.output.setField("Timestamp",state.getTimeStamp().toString()); - mainParent.output.setField("Timestamp value", Text.dateFormat(state.getTimeStamp().longValue())); - mainParent.output.setField("Global Fees", Text.toFriendlyBalance(state.getGlobalFees().longValue())); - mainParent.output.setField("Juice Price", Text.toFriendlyBalance(state.getJuicePrice().longValue())); - mainParent.output.setField("Total Funds", Text.toFriendlyBalance(state.computeTotalFunds())); - mainParent.output.setField("Number of accounts", accountList.size()); - mainParent.output.setField("Number of peers", peerList.size()); - } catch (Throwable t) { - mainParent.showError(t); - } - } - -} diff --git a/convex-cli/src/main/java/convex/cli/Transaction.java b/convex-cli/src/main/java/convex/cli/Transaction.java deleted file mode 100644 index 40365d8d0..000000000 --- a/convex-cli/src/main/java/convex/cli/Transaction.java +++ /dev/null @@ -1,107 +0,0 @@ -package convex.cli; - -import convex.api.Convex; -import convex.core.crypto.AKeyPair; -import convex.core.data.Address; -import convex.core.data.ACell; -import convex.core.lang.Reader; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Invoke; -import convex.core.Result; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine.Command; -import picocli.CommandLine.Option; -import picocli.CommandLine.Parameters; -import picocli.CommandLine.ParentCommand; - -/** - * - * Convex Transaction sub command - * - * convex.transaction - * - */ -@Command(name="transaction", - aliases={"transact", "tr"}, - mixinStandardHelpOptions=true, - description="Execute a transaction on the network via a peer.") -public class Transaction implements Runnable { - - @ParentCommand - protected Main mainParent; - - private static final Logger log = LoggerFactory.getLogger(Transaction.class); - - @Option(names={"-i", "--index-key"}, - defaultValue="0", - description="Keystore index of the public/private key to use to run the transaction.") - private int keystoreIndex; - - @Option(names={"--public-key"}, - defaultValue="", - description="Hex string of the public key in the Keystore to use to run the transaction.%n" - + "You only need to enter in the first distinct hex values of the public key.%n" - + "For example: 0xf0234 or f0234") - private String keystorePublicKey; - - @Option(names={"--port"}, - description="Port number to connect or create a peer.") - private int port = 0; - - @Option(names={"--host"}, - defaultValue=Constants.HOSTNAME_PEER, - description="Hostname to connect to a peer. Default: ${DEFAULT-VALUE}") - private String hostname; - - @Option(names={"-a", "--address"}, - description="Account address to use for the transaction request.") - private long addressNumber; - - @Option(names={"-t", "--timeout"}, - description="Timeout in miliseconds.") - private long timeout = Constants.DEFAULT_TIMEOUT_MILLIS; - - @Parameters(paramLabel="transactionCommand", - description="Transaction Command") - private String transactionCommand; - - @Override - public void run() { - - AKeyPair keyPair = null; - try { - keyPair = mainParent.loadKeyFromStore(keystorePublicKey, keystoreIndex); - } catch (Error e) { - mainParent.showError(e); - return; - } - - if (keyPair == null) { - log.warn("cannot load a valid key pair to perform this transaction"); - return; - } - - if (addressNumber == 0) { - log.warn("--address. You need to provide a valid address number"); - return; - } - - Address address = Address.create(addressNumber); - - Convex convex = null; - try { - convex = mainParent.connectToSessionPeer(hostname, port, address, keyPair); - log.info("Executing transaction: %s\n", transactionCommand); - ACell message = Reader.read(transactionCommand); - ATransaction transaction = Invoke.create(address, -1, message); - - Result result = convex.transactSync(transaction, timeout); - mainParent.output.setResult(result); - } catch (Throwable t) { - mainParent.showError(t); - } - } - -} diff --git a/convex-cli/src/main/java/convex/cli/output/Output.java b/convex-cli/src/main/java/convex/cli/output/Output.java deleted file mode 100644 index 9046ba3d2..000000000 --- a/convex-cli/src/main/java/convex/cli/output/Output.java +++ /dev/null @@ -1,70 +0,0 @@ -package convex.cli.output; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import convex.core.data.ACell; -import convex.core.Result; - -/* - * Output class to show results from the CLI - * - */ - - - -public class Output { - - protected List fieldList = new ArrayList(); - - protected List> rowList = new ArrayList>(); - - public OutputField setField(String name, long value) { - return setField(name, String.valueOf(value)); - } - - public OutputField setField(String name, ACell value) { - return setField(name, value.toString()); - } - - public OutputField setField(String name, String value) { - OutputField field = OutputField.create(name, value); - fieldList.add(field); - return field; - } - - public void setResult(Result result) { - ACell value = result.getValue(); - setField("Result", value); - if (result.isError()) { - setField("Error code", result.getErrorCode()); - if (result.getTrace() != null) { - setField("Trace", result.getTrace()); - } - return; - } - setField("Data type", value.getType().toString()); - } - - public void addRow() { - rowList.add(fieldList); - fieldList = new ArrayList(); - } - - public void writeToStream(PrintWriter out) { - if (rowList.size() > 0) { - List firstRow = rowList.get(0); - out.println(firstRow.stream().map(OutputField::getDescription).collect(Collectors.joining(" "))); - for ( ListfieldList : rowList) { - out.println(fieldList.stream().map(OutputField::getValue).collect(Collectors.joining(" "))); - } - } - else { - for (OutputField field : fieldList) { - out.println(String.format("%s: %s", field.getDescription(), field.getValue())); - } - } - } -} diff --git a/convex-cli/src/main/java/convex/cli/output/OutputField.java b/convex-cli/src/main/java/convex/cli/output/OutputField.java deleted file mode 100644 index 9330c5c49..000000000 --- a/convex-cli/src/main/java/convex/cli/output/OutputField.java +++ /dev/null @@ -1,25 +0,0 @@ -package convex.cli.output; - - -public class OutputField { - - protected String description; - protected String value; - - private OutputField(String description, String value) { - this.description = description; - this.value = value; - } - - public static OutputField create(String description, String value) { - return new OutputField(description, value); - } - - public String getDescription() { - return description; - } - - public String getValue() { - return value; - } -} diff --git a/convex-cli/src/main/java/convex/cli/peer/PeerManager.java b/convex-cli/src/main/java/convex/cli/peer/PeerManager.java deleted file mode 100644 index e4c46da9c..000000000 --- a/convex-cli/src/main/java/convex/cli/peer/PeerManager.java +++ /dev/null @@ -1,349 +0,0 @@ -package convex.cli.peer; - -import java.io.File; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.stream.Collectors; - -import convex.api.Convex; -import convex.core.util.Shutdown; -import convex.cli.Helpers; -import convex.core.Belief; -import convex.core.Result; -import convex.core.State; -import convex.core.crypto.AKeyPair; -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.Address; -import convex.core.data.Hash; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.SignedData; -import convex.core.init.Init; -import convex.core.lang.RT; -import convex.core.store.AStore; -import convex.core.util.Utils; -import convex.peer.API; -import convex.peer.IServerEvent; -import convex.peer.Server; -import convex.peer.ServerEvent; -import convex.peer.ServerInformation; -import etch.EtchStore; - - -/** -* -* Convex CLI PeerManager -* -*/ - -public class PeerManager implements IServerEvent { - - private static final Logger log = LoggerFactory.getLogger(PeerManager.class.getName()); - - private static final long TRANSACTION_TIMEOUT_MILLIS = 50000; - private static final int FRIENDLY_HEX_STRING_SIZE = 6; - - protected List peerServerList = new ArrayList(); - - protected Session session = new Session(); - - protected String sessionFilename; - - protected AKeyPair keyPair; - - protected Address address; - - protected AStore store; - - protected BlockingQueue serverEventQueue = new ArrayBlockingQueue(1024); - - - private PeerManager(String sessionFilename, AKeyPair keyPair, Address address, AStore store) { - this.sessionFilename = sessionFilename; - this.keyPair = keyPair; - this.address = address; - this.store = store; - } - - public static PeerManager create(String sessionFilename) { - return new PeerManager(sessionFilename, null, null, null); - } - - public static PeerManager create(String sessionFilename, AKeyPair keyPair, Address address, AStore store) { - return new PeerManager(sessionFilename, keyPair, address, store); - } - - public void launchLocalPeers(List keyPairList, int peerPorts[]) { - List keyList=keyPairList.stream().map(kp->kp.getAccountKey()).collect(Collectors.toList()); - - State genesisState=Init.createState(keyList); - peerServerList = API.launchLocalPeers(keyPairList,genesisState, peerPorts, this); - } - - public List getNetworkHashList(String remotePeerHostname) { - InetSocketAddress remotePeerAddress = Utils.toInetSocketAddress(remotePeerHostname); - int retryCount = 5; - Convex convex = null; - Result result = null; - while (retryCount > 0) { - try { - convex = Convex.connect(remotePeerAddress, address, keyPair); - Future cf = convex.requestStatus(); - result = cf.get(TRANSACTION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - retryCount = 0; - } catch (IOException | InterruptedException | ExecutionException | TimeoutException e ) { - // raiseServerMessage("unable to connect to remote peer at " + remoteHostname + ". Retrying " + e); - retryCount --; - } - } - if ((convex==null)||(result == null)) { - throw new Error("Failed to join network: Cannot connect to remote peer at "+remotePeerHostname); - } - convex.close(); - List hashList = new ArrayList(5); - AVector values = result.getValue(); - hashList.add(RT.ensureHash(values.get(0))); // beliefHash - hashList.add(RT.ensureHash(values.get(1))); // stateHash - hashList.add(RT.ensureHash(values.get(2))); // netwokIdHash - hashList.add(RT.ensureHash(values.get(4))); // consensusHash - - return hashList; - } - - public State aquireState(String remotePeerHostname, Hash stateHash) { - InetSocketAddress remotePeerAddress = Utils.toInetSocketAddress(remotePeerHostname); - Convex convex = null; - State state = null; - try { - convex = Convex.connect(remotePeerAddress, address, keyPair); - Future bf = convex.acquire(stateHash, store); - state = bf.get(TRANSACTION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - convex.close(); - } catch (IOException | InterruptedException | ExecutionException | TimeoutException e) { - throw new Error("cannot aquire network state: " + e); - } - return state; - - } - public SignedData aquireBelief(String remotePeerHostname, Hash beliefHash) { - // sync the etch db with the network state - Convex convex = null; - SignedData signedBelief = null; - InetSocketAddress remotePeerAddress = Utils.toInetSocketAddress(remotePeerHostname); - try { - convex = Convex.connect(remotePeerAddress, address, keyPair); - Future> cf = convex.acquire(beliefHash, store); - signedBelief = cf.get(TRANSACTION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - convex.close(); - } catch (IOException | InterruptedException | ExecutionException | TimeoutException e) { - throw new Error("cannot acquire belief: " + e); - } - - return signedBelief; - } - - public void launchPeer( - String hostname, - int port, - String remotePeerHostname - ) { - Map config = new HashMap<>(); - if (port > 0 ) { - config.put(Keywords.PORT, port); - } - config.put(Keywords.STORE, store); - config.put(Keywords.SOURCE, remotePeerHostname); - config.put(Keywords.KEYPAIR, keyPair); - config.put(Keywords.EVENT_HOOK, this); // Add this as IServerEvent hook - Server server = API.launchPeer(config); - - peerServerList.add(server); - } - - /** - * Load in a session from a session file. - * - * @param sessionFilename Filename to load. - * - */ - protected void loadSession() { - File sessionFile = new File(sessionFilename); - try { - session.load(sessionFile); - } catch (IOException e) { - log.error("Cannot load the session control file"); - } - } - - /** - * Add a peer to the session list of peers. - * - * @param peerServer Add the peerServer to the list of peers for this session. - * - */ - protected void addToSession(Server peerServer) { - EtchStore store = (EtchStore) peerServer.getStore(); - - session.addPeer( - peerServer.getPeerKey(), - peerServer.getHostname(), - store.getFileName() - ); - } - - /** - * Add all peers started in this session to the session list. - * - */ - protected void addAllToSession() { - for (Server peerServer: peerServerList) { - addToSession(peerServer); - } - } - - /** - * Remove all peers added by this manager from the session list of peers. - * - */ - protected void removeAllFromSession() { - for (Server peerServer: peerServerList) { - session.removePeer(peerServer.getPeerKey()); - } - } - - /** - * Store the session details to file. - * - * @param sessionFilename Fileneame to save the session. - * - */ - protected void storeSession() { - File sessionFile = new File(sessionFilename); - try { - Helpers.createPath(sessionFile); - if (session.getSize() > 0) { - session.store(sessionFile); - } - else { - sessionFile.delete(); - } - } catch (IOException e) { - log.error("Cannot store the session control data"); - } - } - - /** - * Once the manager has launched 1 or more peers. The manager now needs too loop and show any events generated by the peers - * - */ - public void showPeerEvents() { - - loadSession(); - addAllToSession(); - storeSession(); - - /* - Go through each started peer server connection and make sure - that each peer is connected to the other peer. - */ - /* - for (Server peerServer: peerServerList) { - connectToPeers(peerServer, session.getPeerAddressList()); - } - */ - - // shutdown hook to remove/update the session file - Shutdown.addHook(Shutdown.CLI,new Runnable() { - public void run() { - // System.out.println("peers stopping"); - // remove session file - loadSession(); - removeAllFromSession(); - storeSession(); - } - }); - - Server firstServer = peerServerList.get(0); - System.out.println("Starting network Id: "+ firstServer.getPeer().getNetworkID().toString()); - while (true) { - try { - ServerEvent event = serverEventQueue.take(); - ServerInformation information = event.getInformation(); - int index = getServerIndex(information.getPeerKey()); - if (index >=0) { - String item = toServerInformationText(information); - System.out.println(String.format("#%d: %s Msg: %s", index + 1, item, event.getReason())); - } - } catch (InterruptedException e) { - System.out.println("Peer manager interrupted!"); - return; - } - } - } - - protected String toServerInformationText(ServerInformation serverInformation) { - String shortName = Utils.toFriendlyHexString(serverInformation.getPeerKey().toHexString(), FRIENDLY_HEX_STRING_SIZE); - String hostname = serverInformation.getHostname(); - String joined = "NJ"; - String synced = "NS"; - if (serverInformation.isJoined()) { - joined = " J"; - } - if (serverInformation.isSynced()) { - synced = " S"; - } - String stateHash = Utils.toFriendlyHexString(serverInformation.getStateHash().toHexString(), FRIENDLY_HEX_STRING_SIZE); - String beliefHash = Utils.toFriendlyHexString(serverInformation.getBeliefHash().toHexString(), FRIENDLY_HEX_STRING_SIZE); - int connectionCount = serverInformation.getConnectionCount(); - int trustedConnectionCount = serverInformation.getTrustedConnectionCount(); - long consensusPoint = serverInformation.getConsensusPoint(); - String item = String.format("Peer:%s URL: %s Status:%s %s Connections:%2d/%2d Consensus:%4d State:%s Belief:%s", - shortName, - hostname, - joined, - synced, - connectionCount, - trustedConnectionCount, - consensusPoint, - stateHash, - beliefHash - ); - - return item; - } - - protected int getServerIndex(AccountKey peerKey) { - for (int index = 0; index < peerServerList.size(); index ++) { - Server server = peerServerList.get(index); - if (server.getPeer().getPeerKey().equals(peerKey)) { - return index; - } - } - return -1; - } - - /** - * Implements for IServerEvent - * - */ - - public void onServerChange(ServerEvent serverEvent) { - // add in queue if space available - serverEventQueue.offer(serverEvent); - } -} diff --git a/convex-cli/src/main/java/convex/cli/peer/Session.java b/convex-cli/src/main/java/convex/cli/peer/Session.java deleted file mode 100644 index 1dd76aff7..000000000 --- a/convex-cli/src/main/java/convex/cli/peer/Session.java +++ /dev/null @@ -1,147 +0,0 @@ -package convex.cli.peer; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -import convex.core.data.AccountKey; - -public class Session { - - - protected List items = new ArrayList(); - /** - * Load a session data from file. - * - * @param filename Filename of the session file to load. - * - * @throws IOException - */ - public void load(File filename) throws IOException { - items.clear(); - if (filename.exists()) { - FileInputStream stream = new FileInputStream(filename); - Properties values = new Properties(); - values.load(stream); - for (String name: values.stringPropertyNames()) { - String line = values.getProperty(name, ""); - SessionItem item = SessionItem.createFromString(line); - items.add(item); - } - } - } - - /** - * Add a peer to the list of peers kept in the session data. - * - * @param accountKey Public key of Peer - * @param hostname Hostname of the peer. This includes the port number. - * @param etchFilename Filename that the peer is using to store the peer's state. - * - */ - public void addPeer(AccountKey accountKey, String hostname, String etchFilename){ - SessionItem item = SessionItem.create(accountKey, hostname, etchFilename); - items.add(item); - } - - /** - * Remove a peer from the list of peers held by this session. - * - * @param accountKey Address of the peer, this is the public key used by the peer. - * - */ - public void removePeer(AccountKey accountKey) { - for (SessionItem item: items) { - if (item.getAccountKey().equals(accountKey)) { - items.remove(item); - return; - } - } - } - - /** - * Store the session list to a file. - * - * @param filename Filename to save the session too. - * @throws IOException if the file data cannot be writtern. - * - */ - public void store(File filename) throws IOException { - FileOutputStream stream = new FileOutputStream(filename); - Properties values = new Properties(); - int index = 0; - for (SessionItem item: items) { - values.setProperty(String.valueOf(index), item.toString()); - index ++; - } - values.store(stream, "Convex Session"); - } - - /** - * Return the number of session items added to this session. - * - * @return number of items found for this session. - * - */ - public int getSize() { - return items.size(); - } - - /** - * Return true of false if the peer name exists in the list of peers for this session - * - * @param accountKey Public Key of the peer to check to see if it exists. - * @return true if the peer name exists, flase otherwise - */ - public boolean isPeer(AccountKey accountKey) { - SessionItem item = getItemFromAccountKey(accountKey); - return item != null; - } - - /** - * Return a session item based on the peer index. - * - * @param index The index of the peer in the list, starting from 0. - * @return Session Item if the item is found at the index, if not then return null. - */ - public SessionItem getItemFromIndex(int index) { - return items.get(index); - } - - /** - * Get a session item based on the peers AccountKey. - * - * @param accountKey AccountKey of the peer to get the session item for. - * - * @return SessionItem object or null if the peer account key cannot be found - * - */ - public SessionItem getItemFromAccountKey(AccountKey accountKey) { - for (SessionItem item: items) { - if (item.getAccountKey().equals(accountKey)) { - return item; - } - } - return null; - } - - /** - * Get a list of peer hostnames. - * - * @return List hostname item for each stored peer. - * - */ - public String[] getPeerHostnameList() { - String[] result = new String[items.size()]; - int index = 0; - for (SessionItem item: items) { - result[index] = item.getHostname(); - index ++; - } - return result; - } -} diff --git a/convex-cli/src/main/java/convex/cli/peer/SessionItem.java b/convex-cli/src/main/java/convex/cli/peer/SessionItem.java deleted file mode 100644 index 7f522967b..000000000 --- a/convex-cli/src/main/java/convex/cli/peer/SessionItem.java +++ /dev/null @@ -1,106 +0,0 @@ -package convex.cli.peer; - -import java.net.InetSocketAddress; - -import convex.core.data.AccountKey; -import convex.core.util.Utils; - -public class SessionItem { - - protected static final String DELIMITER = ","; - - protected AccountKey accountKey; - protected String hostname; - protected String etchFilename; - - /** - * Creates a new SessionItem object - */ - private SessionItem(AccountKey accountKey, String hostname, String etchFilename) { - this.accountKey = accountKey; - this.hostname = hostname; - this.etchFilename = etchFilename; - } - - /** - * Create a new SessionItem object using the following fields: - * - * @param accountKey AccountKey of the peer. - * - * @param hostname Hostname and port of the peer. - * - * @param etchFilename Etch filename the peer is using. - * - * @return a new SessionItem object. - * - */ - public static SessionItem create(AccountKey accountKey, String hostname, String etchFilename) { - return new SessionItem(accountKey, hostname, etchFilename); - } - - /** - * Create a new SessionItem from a comma delimited string. - * - * @param value String that contain the session item data. - * - * @return a new SessionItem object. - * - */ - public static SessionItem createFromString(String value) { - String[] values = value.split(DELIMITER); - return create(AccountKey.fromChecksumHex(values[0]), values[1], values[2]); - } - - /** - * @return the peer AccountKey. - * - */ - public AccountKey getAccountKey() { - return accountKey; - } - - /** - * @return the string name to use for this session item. - * - */ - public String getName() { - return accountKey.toChecksumHex(); - } - - /** - * @return the peers hostname and port number. - * - */ - public String getHostname() { - return hostname; - } - - public int getPort() { - InetSocketAddress address = Utils.toInetSocketAddress(hostname); - return address.getPort(); - } - - public InetSocketAddress getHostAddress() { - return Utils.toInetSocketAddress(hostname); - } - - /** - * @return the used Etch Filename for this peer. - * - */ - public String getEtchFilename() { - return etchFilename; - } - - /** - * @return the encoded data for this session as a commar delimited string. - * - */ - public String toString() { - String[] values = new String[3]; - values[0] = accountKey.toChecksumHex(); - values[1] = hostname; - values[2] = etchFilename; - return String.join(DELIMITER, values); - } -} diff --git a/convex-cli/src/test/java/convex/cli/CLICommandKeyExportTest.java b/convex-cli/src/test/java/convex/cli/CLICommandKeyExportTest.java deleted file mode 100644 index cdd5ce4e7..000000000 --- a/convex-cli/src/test/java/convex/cli/CLICommandKeyExportTest.java +++ /dev/null @@ -1,68 +0,0 @@ -package convex.cli; - -import java.io.File; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.Test; - -public class CLICommandKeyExportTest { - - private static final String KEYSTORE_FILENAME = "/tmp/tempKeystore.dat"; - private static final String KEYSTORE_PASSWORD = "testPassword"; - private static final String EXPORT_PASSWORD = "testExportPassword"; - - @Test - public void testKeyGenerateList() { - - // command key.generate - CommandLineTester tester = new CommandLineTester( - "key", "generate", - "--password", KEYSTORE_PASSWORD, - "--keystore", KEYSTORE_FILENAME - ); - tester.assertOutputMatch("^Index Public Key\\s+0"); - String publicKey = tester.getField("0 "); - assertFalse(publicKey.isEmpty()); - publicKey = publicKey.stripLeading(); - - File fp = new File(KEYSTORE_FILENAME); - assertTrue(fp.exists()); - - // command key.export index - tester = new CommandLineTester( - "key", - "export", - "--password", KEYSTORE_PASSWORD, - "--keystore", KEYSTORE_FILENAME, - "--index-key", "1", - "--export-password", EXPORT_PASSWORD - ); - tester.assertOutputMatch("ENCRYPTED PRIVATE KEY"); - - // command key.export publicKey - tester = new CommandLineTester( - "key", - "export", - "--password", KEYSTORE_PASSWORD, - "--keystore", KEYSTORE_FILENAME, - "--public-key", publicKey, - "--export-password", EXPORT_PASSWORD - ); - tester.assertOutputMatch("ENCRYPTED PRIVATE KEY"); - - // command key.export publicKey with leading 0x - tester = new CommandLineTester( - "key", - "export", - "--password", KEYSTORE_PASSWORD, - "--keystore", KEYSTORE_FILENAME, - "--public-key", "0x" + publicKey, - "--export-password", EXPORT_PASSWORD - ); - tester.assertOutputMatch("ENCRYPTED PRIVATE KEY"); - - - } -} - diff --git a/convex-cli/src/test/java/convex/cli/CLICommandKeyImportTest.java b/convex-cli/src/test/java/convex/cli/CLICommandKeyImportTest.java deleted file mode 100644 index 0e323fac4..000000000 --- a/convex-cli/src/test/java/convex/cli/CLICommandKeyImportTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package convex.cli; - -import org.junit.jupiter.api.Test; - -import convex.core.crypto.AKeyPair; -import convex.core.crypto.Ed25519KeyPair; -import convex.core.crypto.PEMTools; - -public class CLICommandKeyImportTest { - - private static final String KEYSTORE_FILENAME = "/tmp/tempKeystore.dat"; - private static final String KEYSTORE_PASSWORD = "testPassword"; - private static final String IMPORT_PASSWORD = "testImportPassword"; - - @Test - public void testKeyImport() { - - AKeyPair keyPair = Ed25519KeyPair.generate(); - String pemText = PEMTools.encryptPrivateKeyToPEM(keyPair.getPrivate(), IMPORT_PASSWORD.toCharArray()); - - // command key.list - CommandLineTester tester = new CommandLineTester( - "key", - "import", - "--password", KEYSTORE_PASSWORD, - "--keystore", KEYSTORE_FILENAME, - "--import-text", pemText, - "--import-password", IMPORT_PASSWORD - ); - - tester.assertOutputMatch("public key: " + keyPair.getAccountKey().toHexString()); - - } -} diff --git a/convex-cli/src/test/java/convex/cli/CLICommandKeyTest.java b/convex-cli/src/test/java/convex/cli/CLICommandKeyTest.java deleted file mode 100644 index 42821e377..000000000 --- a/convex-cli/src/test/java/convex/cli/CLICommandKeyTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package convex.cli; - -import java.io.File; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.Test; - -public class CLICommandKeyTest { - - private static final String KEYSTORE_FILENAME = "/tmp/tempKeystore.dat"; - private static final String KEYSTORE_PASSWORD = "testPassword"; - - @Test - public void testKeyGenerateList() { - - // command key.generate - CommandLineTester tester = new CommandLineTester("key", "generate", "--password", KEYSTORE_PASSWORD, "--keystore", KEYSTORE_FILENAME); - tester.assertOutputMatch("^Index Public Key\\s+0"); - - File fp = new File(KEYSTORE_FILENAME); - assertTrue(fp.exists()); - - // command key.list - tester = new CommandLineTester("key", "list", "--password", KEYSTORE_PASSWORD, "--keystore", KEYSTORE_FILENAME); - tester.assertOutputMatch("^Index Public Key\\s+1"); - - } -} diff --git a/convex-cli/src/test/java/convex/cli/CLIHelpTest.java b/convex-cli/src/test/java/convex/cli/CLIHelpTest.java deleted file mode 100644 index 67682569b..000000000 --- a/convex-cli/src/test/java/convex/cli/CLIHelpTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package convex.cli; - -import org.junit.jupiter.api.Test; - -import static convex.cli.Helper.assertExecuteCommandLineResult; - -public class CLIHelpTest { - - @Test - public void testHelp() { - assertExecuteCommandLineResult(0, "^Usage: convex \\[-hVv\\]", "--help"); - assertExecuteCommandLineResult(0, "^Usage: convex \\[-hVv\\]", "-h"); - assertExecuteCommandLineResult(0, "^Usage: convex \\[-hVv\\]", "help"); - assertExecuteCommandLineResult(0, "^Usage: convex account \\[-hVv\\]", "account", "help"); - assertExecuteCommandLineResult(0, "^Usage: convex account balance \\[-hVv\\]", "account", "balance", "--help"); - assertExecuteCommandLineResult(0, "^Usage: convex account create \\[-fhVv\\]", "account", "create", "--help"); - assertExecuteCommandLineResult(0, "^Usage: convex account information \\[-hVv\\]", "account", "information", "--help"); - assertExecuteCommandLineResult(0, "^Usage: convex account fund \\[-hVv\\]", "account", "fund", "--help"); - assertExecuteCommandLineResult(0, "^Usage: convex key \\[-hVv\\]", "key", "help"); - assertExecuteCommandLineResult(0, "^Usage: convex key generate \\[-hVv\\]", "key", "generate", "--help"); - assertExecuteCommandLineResult(0, "^Usage: convex key list \\[-hVv\\]", "key", "list", "--help"); - assertExecuteCommandLineResult(0, "^Usage: convex peer \\[-hVv\\]", "peer", "help"); - assertExecuteCommandLineResult(0, "^Usage: convex peer start \\[-hrVv\\]", "peer", "start", "--help"); - assertExecuteCommandLineResult(0, "^Usage: convex local \\[-hVv\\]", "local", "--help"); - assertExecuteCommandLineResult(0, "^Usage: convex local start \\[-hVv\\]", "local", "start", "--help"); - assertExecuteCommandLineResult(0, "^Usage: convex local gui \\[-hVv\\]", "local", "gui", "--help"); - assertExecuteCommandLineResult(0, "^Usage: convex query \\[-hVv\\]", "query", "--help"); - assertExecuteCommandLineResult(0, "^Usage: convex status \\[-hVv\\]", "status", "--help"); - assertExecuteCommandLineResult(0, "^Usage: convex transaction \\[-hVv\\]", "transaction", "--help"); - assertExecuteCommandLineResult(0, "^Usage: convex transaction \\[-hVv\\]", "transact", "--help"); - } - -} diff --git a/convex-cli/src/test/java/convex/cli/CLIMainTest.java b/convex-cli/src/test/java/convex/cli/CLIMainTest.java deleted file mode 100644 index 4f99a3482..000000000 --- a/convex-cli/src/test/java/convex/cli/CLIMainTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package convex.cli; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; - -public class CLIMainTest { - - @Test - public void testMainGetPortList() { - Main mainApp = new Main(); - String basicList[] = {"80", "90", "100", "101", "200"}; - int result[] = mainApp.getPortList(basicList, 4); - assertArrayEquals(new int[]{80, 90, 100, 101}, result); - - String commaList[] = {"80,90,100,101,200"}; - result = mainApp.getPortList(commaList, 4); - assertArrayEquals(new int[]{80, 90, 100, 101}, result); - - String closedRange[] = {"100-200"}; - result = mainApp.getPortList(closedRange, 4); - assertArrayEquals(new int[]{100, 101, 102, 103}, result); - - String openRange[] = {"100-"}; - result = mainApp.getPortList(openRange, 6); - assertArrayEquals(new int[]{100, 101, 102, 103, 104, 105}, result); - - String combinedClosedRange[] = {"80", "100-103", "200"}; - result = mainApp.getPortList(combinedClosedRange, 6); - assertArrayEquals(new int[]{80, 100, 101, 102, 103, 200}, result); - - String combinedOpenRange[] = {"80", "100-", "200", "300"}; - result = mainApp.getPortList(combinedOpenRange, 6); - assertArrayEquals(new int[]{80, 100, 101, 102, 103, 104}, result); - - String combinedCommaClosedRange[] = {"80,100-103,200"}; - result = mainApp.getPortList(combinedCommaClosedRange, 6); - assertArrayEquals(new int[]{80, 100, 101, 102, 103, 200}, result); - - String combinedCommaOpenRange[] = {"80,100-,200,300"}; - result = mainApp.getPortList(combinedCommaOpenRange, 6); - assertArrayEquals(new int[]{80, 100, 101, 102, 103, 104}, result); - - assertThrows(NumberFormatException.class, () -> { - String badNumberValue[] = {"80,100+,200,300"}; - mainApp.getPortList(badNumberValue, 6); - }); - } - -} diff --git a/convex-cli/src/test/java/convex/cli/CommandLineTester.java b/convex-cli/src/test/java/convex/cli/CommandLineTester.java deleted file mode 100644 index 3f1e5397c..000000000 --- a/convex-cli/src/test/java/convex/cli/CommandLineTester.java +++ /dev/null @@ -1,69 +0,0 @@ - -package convex.cli; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import picocli.CommandLine; - - -public class CommandLineTester { - protected String output; - protected int result; - protected String args[]; - - public CommandLineTester(String ... args) { - this.args = args; - StringWriter outputWriter = new StringWriter(); - PrintWriter printWriter = new PrintWriter(outputWriter); - Main mainApp = new Main(); - - CommandLine commandLine = new CommandLine(mainApp); - - commandLine.setOut(printWriter); - - this.result = commandLine.execute(args); - mainApp.output.writeToStream(printWriter); - this.output = new String(outputWriter.toString()); - } - - public void assertOutputMatch(String patternText) { - Pattern regex = Pattern.compile(patternText, Pattern.MULTILINE + Pattern.DOTALL); - Matcher matcher = regex.matcher(output); - - String assertText = "\nCommand: convex " + String.join(" ", args) + - "\nMatch: '" + patternText + "'" + - "\nOutput: '" + output.substring(0, Math.min(132, output.length())) + "'" + - "\n"; - assertEquals(true, matcher.find(), assertText); - } - - - public String getField(String name) { - String lines[] = output.split("\\r?\\n"); - for (int index = 0; index < lines.length; index ++) { - String line = lines[index]; - int position = line.indexOf(name); - if (position > 0) { - return line.substring(position + name.length()); - } - } - return ""; - } - - public String getOutput() { - return output; - } - - public int getResult() { - return result; - } - - public String[] getArgs() { - return args; - } -} diff --git a/convex-cli/src/test/java/convex/cli/Helper.java b/convex-cli/src/test/java/convex/cli/Helper.java deleted file mode 100644 index f72216de2..000000000 --- a/convex-cli/src/test/java/convex/cli/Helper.java +++ /dev/null @@ -1,19 +0,0 @@ -package convex.cli; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class Helper { - - - public static void assertExecuteCommandLineResult(int returnCode, String patternText, String ... args) { - CommandLineTester tester = new CommandLineTester(args); - assertEquals(returnCode, tester.getResult()); - tester.assertOutputMatch(patternText); - } - - public static void assertCommandLineResult(int returnCode, String patternText, CommandLineTester tester) { - assertEquals(returnCode, tester.getResult()); - tester.assertOutputMatch(patternText); - } -} - diff --git a/convex-cli/src/test/java/convex/cli/output/OutputTest.java b/convex-cli/src/test/java/convex/cli/output/OutputTest.java deleted file mode 100644 index f6ea30d59..000000000 --- a/convex-cli/src/test/java/convex/cli/output/OutputTest.java +++ /dev/null @@ -1,68 +0,0 @@ -package convex.cli.output; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.io.PrintWriter; -import java.io.StringWriter; - -import org.junit.jupiter.api.Test; - -import convex.core.Result; -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.data.prim.CVMLong; - -public class OutputTest { - - public static final long TEST_LONG_VALUE = 123456789L; - public static final String TEST_STRING_VALUE = "TEST_VALUE"; - public static final long TEST_ERROR_CODE = 98989898L; - - @Test - public void testSetField() { - - StringWriter outputWriter = new StringWriter(); - PrintWriter printWriter = new PrintWriter(outputWriter); - ACell cellValue = Address.create(TEST_LONG_VALUE); - Output output = new Output(); - output.setField("Test-Long", TEST_LONG_VALUE); - output.setField("Test-Cell", cellValue); - output.setField("Test-String", TEST_STRING_VALUE); - output.writeToStream(printWriter); - String outputResult = outputWriter.toString(); - String fullText = String.format("Test-Long: %d\nTest-Cell: #%d\nTest-String: %s\n", TEST_LONG_VALUE, TEST_LONG_VALUE, TEST_STRING_VALUE); - assertEquals(fullText, outputResult.replaceAll("\r\n", "\n")); - } - - @Test - public void testSetResult() { - StringWriter outputWriter = new StringWriter(); - PrintWriter printWriter = new PrintWriter(outputWriter); - ACell cellValue = Address.create(TEST_LONG_VALUE); - - Output output = new Output(); - Result result = Result.create(CVMLong.create(1), cellValue, null); - output.setResult(result); - output.writeToStream(printWriter); - String outputResult = outputWriter.toString(); - String fullText = String.format("Result: #%d\nData type: Address\n", TEST_LONG_VALUE); - assertEquals(fullText, outputResult.replaceAll("\r\n", "\n")); - } - - @Test - public void testSetResultError() { - StringWriter outputWriter = new StringWriter(); - PrintWriter printWriter = new PrintWriter(outputWriter); - ACell cellValue = Address.create(TEST_LONG_VALUE); - ACell errorCode = CVMLong.create(TEST_ERROR_CODE); - Output output = new Output(); - Result result = Result.create(CVMLong.create(1), cellValue, errorCode); - output.setResult(result); - output.writeToStream(printWriter); - String outputResult = outputWriter.toString(); - String fullText = String.format("Result: #%d\nError code: %d\n", TEST_LONG_VALUE, TEST_ERROR_CODE); - assertEquals(fullText, outputResult.replaceAll("\r\n", "\n")); - } - - -} diff --git a/convex-cli/src/test/java/convex/cli/peer/SessionTest.java b/convex-cli/src/test/java/convex/cli/peer/SessionTest.java deleted file mode 100644 index 30bc2e3e5..000000000 --- a/convex-cli/src/test/java/convex/cli/peer/SessionTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package convex.cli.peer; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.junit.jupiter.api.Test; - -import convex.core.crypto.AKeyPair; - - -public class SessionTest { - - private static final String SESSION_FILENAME = "/tmp/session.dat"; - private static final String KEYSTORE_FILENAME = "/tmp/keystore.dat"; - - public List generateSessionList(Session session, int itemCount) { - List keyPairList = new ArrayList(itemCount); - for (int index = 0; index < 10; index ++ ) { - String hostname = String.format("testhostname_%d.com", index); - AKeyPair keyPair = AKeyPair.generate(); - keyPairList.add(keyPair); - session.addPeer(keyPair.getAccountKey(), hostname, KEYSTORE_FILENAME); - } - return keyPairList; - } - - @Test - public void sessionCreate() { - Session session = new Session(); - int itemCount = 10; - List keyPairList = generateSessionList(session, itemCount); - assertEquals(session.getSize(), itemCount); - for (int index = 0; index < itemCount; index ++ ) { - AKeyPair keyPair = keyPairList.get(index); - assertTrue(session.isPeer(keyPair.getAccountKey())); - SessionItem item = session.getItemFromIndex(index); - assertTrue(item.getAccountKey().equals(keyPair.getAccountKey())); - item = session.getItemFromAccountKey(keyPair.getAccountKey()); - assertTrue(item.getAccountKey().equals(keyPair.getAccountKey())); - } - } - - @Test - public void sessionGetHostNameList() { - Session session = new Session(); - int itemCount = 10; - generateSessionList(session, itemCount); - String[] hostnameList = session.getPeerHostnameList(); - for (int index = 0; index < hostnameList.length; index ++ ) { - String expectedHostname = String.format("testhostname_%d.com", index); - assertEquals(hostnameList[index], expectedHostname); - } - } - - @Test - public void sessionStoreAndLoad() { - File fp = new File(SESSION_FILENAME); - if (fp.exists()) { - fp.delete(); - } - Session session = new Session(); - int itemCount = 10; - generateSessionList(session, itemCount); - - try { - session.store(fp); - - } catch (IOException e) { - fail(e); - } - assertTrue(fp.exists()); - - Session savedSession = new Session(); - try { - savedSession.load(fp); - } catch (IOException e) { - fail(e); - } - assertEquals(session.getSize(), savedSession.getSize()); - for (int index = 0; index < savedSession.getSize(); index ++) { - SessionItem item = session.getItemFromIndex(index); - SessionItem savedItem = savedSession.getItemFromIndex(index); - assertTrue(item.getAccountKey().equals(savedItem.getAccountKey())); - } - fp.delete(); - } - - @Test - public void sessionRemovePeer() { - Session session = new Session(); - int itemCount = 10; - List keyPairList = generateSessionList(session, itemCount); - - for (int index = 0; index < keyPairList.size(); index ++ ) { - AKeyPair keyPair = keyPairList.get(index); - session.removePeer(keyPair.getAccountKey()); - } - assertEquals(session.getSize(), 0); - } - -} diff --git a/convex-core/.gitignore b/convex-core/.gitignore deleted file mode 100644 index e450bc81d..000000000 --- a/convex-core/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -/target/ -/.project -/.classpath -/.settings/ -/pom.xml.versionsBackup diff --git a/convex-core/pom.xml b/convex-core/pom.xml deleted file mode 100644 index ddddc7b83..000000000 --- a/convex-core/pom.xml +++ /dev/null @@ -1,125 +0,0 @@ - - - world.convex - convex - 0.7.0-rc3 - - - 4.0.0 - - convex-core - - Convex Core - Convex core libraries and common utilities - https://convex.world - - - - - org.antlr - antlr4-maven-plugin - 4.9.2 - - ${basedir}/lib/src/main/antlr4/convex/core/lang/reader/antlr - - - - - antlr4 - - - - - - - - src/main/cvx - - - src/main/antlr4 - - - - - - - - org.bouncycastle - bcprov-jdk15on - 1.69 - - - org.bouncycastle - bcpkix-jdk15on - 1.69 - - - - com.goterl - lazysodium-java - 5.1.1 - - - - net.java.dev.jna - jna - 5.8.0 - - - - org.apache.commons - commons-text - 1.9 - - - com.pholser - junit-quickcheck-core - 1.0 - test - - - com.pholser - junit-quickcheck-generators - 1.0 - test - - - org.junit.jupiter - junit-jupiter-engine - ${junit.version} - test - - - org.junit.vintage - junit-vintage-engine - ${junit.version} - test - - - org.junit.jupiter - junit-jupiter-params - ${junit.version} - test - - - - org.antlr - antlr4-runtime - 4.9.2 - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.slf4j - slf4j-jdk14 - ${slf4j.version} - test - - - - diff --git a/convex-core/src/main/antlr4/convex/core/lang/reader/antlr/Convex.g4 b/convex-core/src/main/antlr4/convex/core/lang/reader/antlr/Convex.g4 deleted file mode 100644 index 1815dacb4..000000000 --- a/convex-core/src/main/antlr4/convex/core/lang/reader/antlr/Convex.g4 +++ /dev/null @@ -1,189 +0,0 @@ -grammar Convex; - -form - : literal - | symbol - | pathSymbol - | dataStructure - | syntax - | quoted - ; - -singleForm: form EOF; - -forms: (form | commented) * ; - -dataStructure: - list | vector | set | map; - -list : '(' forms ')'; - -vector : '[' forms ']'; - -set : HASH '{' forms '}'; - -map : '{' forms '}'; - -literal - : nil - | bool - | blob - | character - | keyword - | symbol - | address - | string - | longValue - | doubleValue - | specialLiteral - ; - -longValue: - DIGITS | SIGNED_DIGITS; - -doubleValue: - DOUBLE; - -specialLiteral: HASH HASH symbol; - -address: HASH DIGITS; - -nil: NIL; - -blob: BLOB; - -bool: BOOL; - -character: CHARACTER; - -keyword: KEYWORD; - -symbol: SYMBOL; - -pathSymbol: SYMBOL_PATH; - -syntax: META form form; - -quoted: QUOTING form; - -string: STRING; - -commented: COMMENTED form; - -/* ========================================= - * Lexer stuff below here - * ========================================= - */ - -SYMBOL_PATH: - (NAME | HASH DIGITS) ('/' NAME)+; - -COMMENTED: '#_'; - -HASH: '#'; - -META: '^'; - -NIL: 'nil'; - -BOOL : 'true' | 'false' ; - -// Number. Needs to go before Symbols! - -DOUBLE: - (DIGITS | SIGNED_DIGITS) DOUBLE_TAIL; - -fragment -DOUBLE_TAIL: - DECIMAL EPART | DECIMAL | EPART; - -fragment -DECIMAL: - '.' DIGITS; - -fragment -EPART: - [eE] (DIGITS | SIGNED_DIGITS); - -DIGITS: - [0-9]+; - -SIGNED_DIGITS: - '-' DIGITS; - -BLOB: '0x' HEX_DIGIT*; - -fragment -HEX_BYTE: HEX_DIGIT HEX_DIGIT; - -fragment -HEX_DIGIT: [0-9a-fA-F]; - -STRING : '"' ( ~'"' | '\\' '"' )* '"' ; - -// Quoting - -QUOTING: '\'' | '`' | '~' | '~@'; - -// Symbols and Keywords - - -KEYWORD: - ':' NAME; - -SYMBOL - : NAME - ; - -fragment -NAME - : '/' - | SYMBOL_FIRST SYMBOL_FOLLOWING*; - -CHARACTER - : '\\u' HEX_BYTE HEX_BYTE - | '\\' . - | SPECIAL_CHARACTER; - -fragment -SPECIAL_CHARACTER - : '\\' ( 'newline' - | 'return' - | 'space' - | 'tab' - | 'formfeed' - | 'backspace' ) ; - - -// Test case "a*+!-_?<>=!" should be a symbol - -fragment -SYMBOL_FIRST - : ALPHA - | '.' | '*' | '+' | '!' | '-' | '_' | '?' | '$' | '%' | '&' | '=' | '<' | '>' - ; - -fragment -SYMBOL_FOLLOWING - : SYMBOL_FIRST - | [0-9] - | ':' | '#' - ; - -fragment -ALPHA: [a-z] | [A-Z]; - -/* - * Whitespace and comments - */ - -fragment -WS : [ \n\r\t,] ; - -fragment -COMMENT: ';' ~[\r\n]* ; - -TRASH - : ( WS | COMMENT ) -> channel(HIDDEN) - ; - diff --git a/convex-core/src/main/assembly/full.xml b/convex-core/src/main/assembly/full.xml deleted file mode 100644 index e8be999d2..000000000 --- a/convex-core/src/main/assembly/full.xml +++ /dev/null @@ -1,34 +0,0 @@ - - full - - jar - - - - ${project.basedir} - / - - README* - LICENSE* - NOTICE* - - - - ${project.build.directory} - / - - *.jar - - - - - - / - true - true - true - test - - - \ No newline at end of file diff --git a/convex-core/src/main/assembly/testing.xml b/convex-core/src/main/assembly/testing.xml deleted file mode 100644 index cf7092fbb..000000000 --- a/convex-core/src/main/assembly/testing.xml +++ /dev/null @@ -1,30 +0,0 @@ - - testing - - jar - - false - - - / - true - true - true - test - - - - - - ${project.build.directory}/test-classes - / - - **/*.class - - true - - - \ No newline at end of file diff --git a/convex-core/src/main/cvx/asset/box.cvx b/convex-core/src/main/cvx/asset/box.cvx deleted file mode 100644 index 6e1f17f5e..000000000 --- a/convex-core/src/main/cvx/asset/box.cvx +++ /dev/null @@ -1,100 +0,0 @@ -'asset.box - -(call *registry* - (register {:description ["A box acts as a holder of arbitrary assets, bundling them together." - "Assets are described in `convex.asset` and created using accounts such as `convex.fungible`." - "The owner of the box has exclusive rights to put assets in or take assets out." - "The box itself is an asset with the designator `[box-actor id]`." - "This library uses `asset.box.actor` as default actor. An alternative implementation can be provided if required."] - :name "Asset box API."})) - - -;;;;;;;;;; Setup - - -(import asset.box.actor :as box.actor) -(import convex.asset :as asset-lib) - - -;;;;;;;;;; Public API - - -(defn burn - - ^{:doc {:description "Destroys a set of box ids which must be owned and empty." - :signature [{:params [set-box-ids]} - {:params [actor set-box-ids]}]}} - - - ([set-box-ids] - - (burn box.actor - set-box-ids)) - - - ([actor set-box-ids] - - (call actor - (burn set-box-ids)))) - - - -(defn create - - ^{:doc {:description "Creates a new box and returns its id." - :signature [{:params []} - {:params [actor]}]}} - - - ([] - - (create box.actor)) - - - ([actor] - - (call actor - (create)))) - - - -(defn insert - - ^{:doc {:description "Inserts an asset into a box." - :signature [{:params [box-id asset]} - {:params [actor box-id asset]}]}} - - - ([box-id asset] - - (insert box.actor - box-id - asset)) - - - ([actor box-id asset] - - (asset-lib/transfer actor - asset - box-id))) - - - -(defn remove - - ^{:doc {:description "Removes an asset from the given box." - :signature [{:params [box-id]} - {:params [actor box-id asset]}]}} - - ([box-id asset] - - (remove box.actor - box-id - asset)) - - - ([actor box-id asset] - - (call actor - (remove box-id - asset)))) diff --git a/convex-core/src/main/cvx/asset/box/actor.cvx b/convex-core/src/main/cvx/asset/box/actor.cvx deleted file mode 100644 index ca81b0235..000000000 --- a/convex-core/src/main/cvx/asset/box/actor.cvx +++ /dev/null @@ -1,301 +0,0 @@ -'asset.box.actor - -(call *registry* - (register {:description ["Default actor for `asset.box`." - "Implements callable functions for `asset.box` and `convex.asset`."] - :name "Asset box actor."})) - - -;;;;;;;;;; Setup - - -(import convex.asset :as asset-lib) - - -;;;;;;;;;; Values - - -(def boxes - - ^{:doc {:description "Map of `box id` -> `asset quantity`."}} - - {}) - - - -(def counter - - ^{:doc {:description "Used for creating box ids."}} - - 0) - - - -(def offers - - ^{:doc {:description "Map of `owner` -> (Map of `recipient address` -> `set of box ids`."}} - - {}) - - - -(def ownership - - ^{:doc {:descrption "Map of `owner` -> `set of box ids`."}} - - {}) - - -;;;;;;;;;; Private helpers - - -(defn -direct-transfer - - ^{:private? true} - - ;; Internal implementation for executing a direct transfer. - - [sender receiver quantity] - - (let [receiver (address receiver) - sender-balance (get ownership - sender - #{}) - _ (assert (subset? quantity - sender-balance)) ;; TODO. Replace with `fail` for better error messages? - receiver-balance (get ownership - receiver - #{}) - new-sender-balance (difference sender-balance - quantity) - new-receiver-balance (union receiver-balance - quantity)] - (def ownership - (assoc ownership - sender new-sender-balance - receiver new-receiver-balance)) - quantity)) - - -;;;;;;;;;; Implementation of `convex.asset` interface - - -(defn accept - - ^{:callable? true - :private? true} - - [sender quantity] - - (let [sender (address sender) - sender-offers (get offers - sender - {}) - offer (or (get-in offers - [sender *caller*]) - #{}) - _ (assert (subset? quantity - offer)) - receiver-balance (get ownership - *caller* - #{}) - new-offer (difference offer - quantity)] - (def offers - (assoc offers sender - (assoc sender-offers - *caller* - new-offer))) - (-direct-transfer sender - *caller* - quantity))) - - - -(defn balance - - ^{:callable? true - :private? true} - - [owner] - - (or (get ownership - owner) - #{})) - - - -(defn direct-transfer - - ^{:callable? true - :private? true} - - [receiver quantity] - - (-direct-transfer *caller* - receiver - quantity)) - - - -(defn offer - - ^{:callable? true - :private? true} - - [receiver quantity] - - (let [caller-offers (get offers - *caller* - {})] - (def offers - (assoc offers - *caller* - (assoc caller-offers - receiver - quantity))))) - - - -(defn receive-asset - - ^{:callable? true - :private? true} - - [asset box-id] - - (let [box-id (long box-id)] - ;; Accepting first solves the problem of putting a box into itself. - ;; - (asset-lib/accept *caller* - asset) - (cond - (not (contains-key? boxes - box-id)) - (fail :STATE - "Target box does not exist") - - (not (contains-key? (get ownership - *caller*) - box-id)) - (fail :TRUST - (str "Box " box-id " not owned"))) - (def boxes - (assoc boxes - box-id - (asset-lib/quantity-add (get boxes - box-id) - asset))))) - - - -(def quantity-add - - ^{:callable? true - :private? true} - - union) - - - -(def quantity-sub - - ^{:callable? true - :private? true} - - difference) - - - -(def quantity-subset? - - ^{:callable? true - :private? true} - - subset?) - - -;;;;;;;;;; Implementation of `asset.box` interface - - -(defn burn - - ^{:callable? true - :private? true} - - [set-box-ids] - - (let [owned-boxes (ownership *caller*)] - (when-not (subset? set-box-ids - owned-boxes) - (fail :TRUST - "Burning boxes requires ownership")) - (for [id set-box-ids] - (let [contents (boxes id)] - (if (empty? contents) - (def boxes - (dissoc boxes - id)) - (fail :STATE - (str "Trying to delete non-empty box: " id))))) - (def ownership - (assoc ownership - *caller* - (difference owned-boxes - set-box-ids))) - nil)) - - - -(defn create - - ^{:callable? true - :private? true} - - [] - - (let [id counter - owner *caller* - owned-boxes (or (get ownership - owner) - #{})] - (def ownership - (assoc ownership - owner - (conj owned-boxes - id))) - (def boxes - (assoc boxes - id - {})) ;; New box contains no assets - (def counter - (inc counter)) - id)) - - - -(defn remove - - ^{:callable? true - :private? true} - - [box-id asset] - - (let [current-asset (get boxes - box-id)] - (when-not (asset-lib/quantity-contains? current-asset - asset) - (fail "Box does not contain quantity of asset specified for removal")) - (when-not (contains-key? (ownership *caller*) - box-id) - (fail :TRUST - (str "Box not owned: " box-id))) - (def boxes - (assoc boxes - box-id - (asset-lib/quantity-sub current-asset - asset))) - ;; Delivers the asset to the caller. - ;; - (asset-lib/transfer *caller* - asset))) diff --git a/convex-core/src/main/cvx/asset/nft/simple.cvx b/convex-core/src/main/cvx/asset/nft/simple.cvx deleted file mode 100644 index 71ca90c17..000000000 --- a/convex-core/src/main/cvx/asset/nft/simple.cvx +++ /dev/null @@ -1,208 +0,0 @@ -'asset.nft.simple - - -(call *registry* - (register {:description ["Enables the creation of minimal NFT tokens." - "An NFT is merely a long. Users can build an additional layer so that this id points to anything." - "Follows the interface described in `convex.asset`."] - :name "Simple NFT creation and management"})) - - -;;;;;;;;;; Values - - -(def counter - - ^{:doc {:description "Used for creating NFT ids."}} - - 0) - - - -(def offers - - ^{:doc {:description "Map of `owner` -> map of `recipient address` -> `set of NFT ids`"}} - - {}) - - -;;;;;;;;;; Implementation of `convex.asset` interface - - -(defn -direct-transfer - - ^{:private? true} - - ;; Used internally by [[accept]] and [[direct-transfer]]. - - [sender receiver quantity] - - (let [receiver (address receiver) - sender-balance (or (get-holding sender) - #{}) - _ (assert (subset? quantity - sender-balance)) - receiver-balance (or (get-holding receiver) - #{}) - new-sender-balance (difference sender-balance - quantity) - new-receiver-balance (union receiver-balance - quantity)] - (set-holding sender - new-sender-balance) - (set-holding receiver - new-receiver-balance)) - quantity) - - - -(defn accept - - ^{:callable? true - :private? true} - - [sender quantity] - - (let [sender (address sender) - sender-offers (or (get offers - sender) - {}) - offer (or (get-in offers - [sender - *caller*]) - #{}) - _ (assert (subset? quantity - offer)) - receiver-balance (or (get-holding *caller*) - #{}) - new-offer (difference offer - quantity)] - - (def offers - (assoc offers - sender - (assoc sender-offers - *caller* - new-offer))) - - (-direct-transfer sender - *caller* - quantity))) - - - -(defn balance - - ^{:callable? true - :private? true} - - [owner] - - (or (get-holding owner) - #{})) - - - -(defn direct-transfer - - ^{:callable? true - :private? true} - - [receiver quantity] - - (-direct-transfer *caller* - receiver - quantity)) - - - -(defn offer - - ^{:callable? true - :private? true} - - [receiver quantity] - - (let [caller-offers (get offers - *caller* - {})] - (def offers - (assoc offers - *caller* - (assoc caller-offers - receiver - quantity))))) - - - -(def quantity-add - - ^{:callable? true - :private? true} - - union) - - - -(def quantity-sub - - ^{:callable? true - :private? true} - - difference) - - - -(def quantity-subset? - - ^{:callable? true - :private? true} - - subset?) - - -;;;;;;;;;; Callable functions - - -(defn burn - - ^{:callable? true - :doc {:description "Destroys a set of NFTs. NFTs must be owned by the caller." - :signature [{:params [nft-set]}]}} - - [nft-set] - - (let [owned-nfts (get-holding *caller*) - nft-set (cond - (long? nft-set) #{nft-set} - (set? nft-set) nft-set - :else (set nft-set))] - (when-not (subset? nft-set - owned-nfts) - (fail :TRUST - "Can only burn owned NFTs")) - (set-holding *caller* - (difference owned-nfts - nft-set)) - nft-set)) - - - -(defn create - - ^{:callable? true - :doc {:description "Creates a new NFT with a fresh ID and arbitrary metadata." - :signature [{:params []}]}} - - [] - - (let [id counter - owner *caller* - owned-nfts (or (get-holding owner) - #{})] - (set-holding owner - (conj owned-nfts - id)) - (def counter - (inc counter)) - id)) diff --git a/convex-core/src/main/cvx/asset/nft/tokens.cvx b/convex-core/src/main/cvx/asset/nft/tokens.cvx deleted file mode 100644 index 27e938750..000000000 --- a/convex-core/src/main/cvx/asset/nft/tokens.cvx +++ /dev/null @@ -1,747 +0,0 @@ -'asset.nft.tokens - - -(call *registry* - (register {:description ["Actor for storing and handling Non-Fungible Tokens, where a token is an map containing:" - "- `:creator`, address of the creator" - "- `:data`, arbitrary value describing the NFT (eg. a URL)" - "- `:owner`, current owner" - "At creation, rights can be specified as to who can transfer, destroy, or update the NFT data in anyway (see `create-token`)." - "Each NFT implements the asset interface described in `convex.asset`." - "Hence, while this account offers specific functions such as `destroy-token`, all other matters are resolved using `convex.asset`."] - :name "Advanced NFT tokens creation and management"})) - - -;; -;; -;; The primary notion of "quantity" for this contract is a set of token ids. -;; As a convenience, we also recognize a single id as shorthand for the set of that id. -;; Therefore, the contract recognizes the following two types of asset descriptions: -;; -;; `[nft-contract-address id-set]` -;; `[nft-contract-address id]`, which is shorthand for `[nft-contract-address #{id}]` -;; -;; This smart contract is designed to be exceptionally simple to use for one-off "singleton tokens", -;; while also providing numerous extension points for creating a class of tokens with shared custom behavior. -;; The goal is that most custom classes of nft tokens will not require building an entire contract -;; from scratch, and instead can be implemented by providing a "class actor" that contains the custom logic. -;; This makes it easier to verify that custom nfts are correct and trustworthy. -;; -;; Each NFT token has a creator, a current owner, and a data map. -;; Note that the owner doesn't necessarily have rights to do anything with the token other than to assert ownership. -;; Singleton tokens utilize a "policy map" to determine who has the right to destroy token, -;; transfer token, or update the data map. By default, these rights reside with the token owner, but -;; the policy map may assign these rights to someone other than the owner, or possibly to no one. -;; One particularly interesting thing you can do with the policy map is to assign a right to -;; the holder of some other token id or asset, thus turning a right into something that is itself transferable. - -;; If you desire more complex policy behavior than what the policy map provide, you can implement a -;; "class actor" defining a new class of non-fungible-tokens with some shared behavior. -;; A class actor must implement `check-trusted?`, similarly to what is defined here. -;; -;; As part of the class actor, you may also implement the following "hooks": -;; (create-token caller id initial-data) - Called when token is created -;; (destroy-token caller id) - Called when token is destroyed -;; (set-token-data caller id data) - Called when token's data is replaced with a new data map -;; (merge-token-data caller id data) - Called when data is merged in with token's existing data. -;; (get-uri id) - If implemented, this is preferred over :uri field in token's data map. -;; (check-transfer caller sender receiver id-set) - Additional restrictions beyond basic permission testing. -;; (perform-transfer caller sender receiver id-set) - Called when transfer takes place -;; -;; Note that a class actor's check-transfer and perform-transfer functions -;; are called with the set of all token ids involved in the transfer that have this class actor. -;; So when transferring a mixed set of tokens from several classes, they are grouped by class, -;; and then each class actor gets called with the set of ids relevant to it. -;; -;; Transfer is implemented using the offer/accept model, following `convex.asset`. -;; So, sender `offer`s a set of ids to the receiver. -;; The receiver's `receive-asset` function is then called to notify them of the offer, -;; and if they want to accept the offer, they call the asset's `accept` function, accepting -;; whatever subset of the offered tokens they want. -;; If an account doesn't implement receive-asset, default behavior is to go ahead and perform the transfer. -;; If an actor doesn't implement receive-asset, default behavior is to fail. -;; -;; - - -(import convex.asset :as asset) - - -;;;;;;;;;; Values - - -(def next-token-id - - ^{:private? true} - - 0) - - - -(def token-records - - ^{:private? true} - - ;; {id {:creator, :owner, :data, :policies, :class}} - - {}) - - - -(def offers - - ^{:private? true} - - ;; {sender {receiver id-set}} - - {}) - - -;;;;;;;;;; Private - Generic - - -(defn empty->nil - - ^{:private? true} - - [s] - - (if (empty? s) - nil - s)) - - - -(defn every? - - ^{:private? true} - - [f coll] - - (reduce (fn [_ x] - (if (f x) - true - (reduced false))) - true - coll)) - - -(defn group-by-class - - ^{:private? true} - - ;; Groups ids by class, only including them if they export a given symbol. - - [id-set required-export] - - (reduce (fn [m id] - (let [class (get-token-class id)] - (if (and class - (callable? class - required-export)) - (assoc m - class - (conj (get m - class - #{}) - id)) - m))) - {} - id-set)) - - - -(defn num->set - - ^{:private? true} - - ;; For convenience, our asset path understands a long to be a singleton set. - ;; Here's a helper function to do that conversion - - [n] - - (if (long? n) - #{n} - n)) - - -;;;;;;;;;; Private - API high-level helpers - - -(defn cancel-offer - - ^{:private? true} - - ;; Optimizes memory use in offers. - - [sender receiver] - - (def offers - (if-let [my-offers (empty->nil (dissoc (get offers - sender) - receiver))] - (assoc offers - sender - my-offers) - (dissoc offers - sender)))) - - - -(defn perform-transfer - - ^{:private? true} - - [sender receiver id-set] - - (when-let [msg (check-transfer sender - receiver - id-set)] - (fail (str msg))) - (def token-records - (reduce (fn [records id] - (assoc-in records - [id - :owner] - receiver)) - token-records - id-set)) - (set-holding sender - (empty->nil (difference (get-holding sender) - id-set))) - (set-holding receiver - (union (get-holding receiver) - id-set)) - (let [perform-transfer-map (group-by-class id-set - 'perform-transfer)] - (reduce (fn [_ [class id-set]] - (call class - (perform-transfer *caller* - sender - receiver - id-set))) - nil - perform-transfer-map) - [*address* - id-set])) - - -;;;;;;;;;; Callable API - - -(defn create-token - - ^{:callable? true - :doc {:description ["Creates token with initial data map." - "A policy map can provided (see `check-trusted?`) or the address of an account acting as a \"class\"." - "A class implements a version of `check-trusted?` and optionally any of the functions found it this account." - "Eg. `destroy-token`, `set-token-data`, ..." - "Classes are meant for advanced use cases, most of the time a policy map or nil will be sufficient." - "Nil means that only the owner can any perform operation on that token (eg. destruction, transfer)"] - :examples [{:code "(create-token {:name \"My Amazing Artwork\"} nil)"} - {:code "(create-token {:name \"Concert ticket\", :redeemed? false} {:destroy :owner, :update :creator, :transfer :creator})"} - {:code "(create-token {:name \"House\"} real-estate-class-actor)"}] - :signature [{:params [initial-data policy-map-or-class]}]}} - - [initial-data policy-map-or-class] - - (assert (or (nil? initial-data) - (map? initial-data)) - (or (nil? policy-map-or-class) - (map? policy-map-or-class) - (and (actor? policy-map-or-class) - (callable? policy-map-or-class - 'check-trusted?)))) - (let [holdings (or (get-holding *caller*) - #{}) - id next-token-id - token-record {:creator *caller* - :owner *caller*} - token-record (if initial-data - (assoc token-record - :data - initial-data) - token-record) - token-record (cond - (nil? policy-map-or-class) token-record - (map? policy-map-or-class) (assoc token-record - :policies - policy-map-or-class) - :else (assoc token-record - :class - policy-map-or-class)) - class (:class token-record)] - (set-holding *caller* - (conj holdings - id)) - (when (and class - (callable? class - 'create-token)) - (call class - (create-token *caller* - id - initial-data))) - (def next-token-id - (inc next-token-id)) - (def token-records - (assoc token-records - id - token-record)) - id)) - - - -(defn destroy-token - - ^{:callable? true - :doc {:description "Destroys token if `(check-trusted? *caller* :destroy id)` returns true." - :examples [{:code "(destroy-token 1234)"}] - :signature [{:params [id]}]}} - - [id] - - (when-not (check-trusted? *caller* - :destroy - id) - (fail "No right to destroy token")) - (let [record (get token-records - id) - class (:class record) - owner (:owner record)] - (set-holding owner (empty->nil (disj (get-holding owner) - id))) - (def token-records - (dissoc token-records - id)) - (when (and class - (callable? class - 'destroy-token)) - (call class - (destroy-token *caller* - id))) - true)) - - - -(defn get-offer - - ^{:callable? true - :doc {:description "Gets the offer from a given sender to a given receiver." - :examples [{:code "(get-offer sender receiver)"}] - :signature [{:params [sender receiver]}]}} - - [sender receiver] - - (get-in offers - [sender - receiver])) - - - -(defn get-offers - - ^{:callable? true - :doc {:description "Gets all the offers from a given sender." - :examples [{:code "(get-offers sender)"}] - :signature [{:params [sender]}]}} - [sender] - - (get offers - sender)) - - - -(defn get-token-class - - ^{:callable? true - :doc {:description "Gets class actor for token, returns nil if it is a singleton token with policy map.", - :examples [{:code "(get-token-class 1234)"}] - :signature [{:params [id]}]}} - - [id] - - (get-in token-records - [id - :class])) - - - -(defn get-token-creator - - ^{:callable? true - :doc {:description "Gets creator of token.", - :examples [{:code "(get-token-creator 1234)"}] - :signature [{:params [id]}]}} - - [id] - - (get-in token-records - [id - :creator])) - - - -(defn get-token-data - - ^{:callable? true - :doc {:description "Gets data map associated with token." - :examples [{:code "(get-token-data 1234)"}] - :signature [{:params [id]}]}} - - [id] - - (get-in token-records - [id - :data])) - - - -(defn get-token-owner - - ^{:callable? true - :doc {:description "Gets owner of token.", - :examples [{:code "(get-token-owner 1234)"}] - :signature [{:params [id]}]}} - - [id] - - (get-in token-records - [id - :owner])) - - - -(defn get-uri - - ^{:callable? true - :doc {:description ["Gets the URI associth with token." - "Not all NFTs have a URI pointing to data off-chain but it is common."]}} - - ;; The class actor handles this call, if available, otherwise we look in the token's data for :uri. - - [id] - - (let [record (get token-records - id) - class (:class record)] - (if (and class - (callable? class - 'get-uri)) - (call class - (get-uri id)) - (:uri (:data record))))) - - - -(defn merge-token-data - - ^{:callable? true - :doc {:description ["Merges data into token's data map." - "Caller must have the right to do so. Either:" - "- `(check-trusted? *caller* :update id)` returns true" - "- `(check-trusted? *caller* [:update key] id) returns true for all key-values"] - :examples [{:code "(merge-token-data 1234 {:redeemed? true})"}] - :signature [{:params [id data]}]}} - - [id data] - - (when-not (or (check-trusted? *caller* - :update - id) - (every? (fn [k] (check-trusted? *caller* - [:update - k] - id)) - (keys data))) - (fail "No right to update token's data fields")) - (let [class (get-token-class id) - new-data (merge (get-in token-records - [id :data]) - data)] - (def token-records - (assoc-in token-records - [id - :data] - new-data)) - (when (and class - (callable? class - 'merge-token-data)) - (call class - (merge-token-data *caller* - id - data))) - new-data)) - - - -(defn set-token-data - - ^{:callable? true - :doc {:description "Replaces data map if `(check-trusted? *caller* :update id)` returns true." - :examples [{:code "(set-token-data 1234 {:name \"New name\", :redeemed? false})"}] - :signature [{:params [id data]}]}} - - [id data] - - (when-not (check-trusted? *caller* - :update - id) - (fail "No right to update token's data")) - (let [class (get-token-class id)] - (def token-records - (assoc-in token-records - [id - :data] - data)) - (when (and class - (callable? class - 'set-token-data)) - (call class - (set-token-data *caller* - id - data))) - data)) - - -;;;;;;;;;; Implementation of `convex.asset` interface -;; -;; `get-offer` is implemented as part of the public callable API. - - -(defn accept - - ^{:callable? true - :private? true} - - [sender accepted-id-set] - - (let [sender (address sender) - receiver *caller* - accepted-id-set (num->set accepted-id-set) - offered-id-set (or (get-in offers - [sender - receiver]) - #{})] - (when-not offer - (fail "Offer not found")) - ;; Assures receiver accepts a subset of offer. - ;; - (assert (subset? accepted-id-set - offered-id-set)) - (if (= accepted-id-set - offered-id-set) - (cancel-offer sender - receiver) - (def offers - (assoc-in offers - [sender - receiver] - (difference offered-id-set - accepted-id-set)))) - (perform-transfer sender - receiver - accepted-id-set))) - - - -(defn balance - - ^{:callable? true - :private? true} - - [owner] - - (or (get-holding (address owner)) - #{})) - - - -(defn direct-transfer - - ^{:callable? true - :private? true} - - ;; Interface for a sender to call the private perform-transfer function - ;; This does not use the offer/accept model, and does not check whether the receiver wants to receive it. - ;; Therefore, it is preferred to use `asset/transfer` to transfer assets. - - [receiver id-set] - - (perform-transfer *caller* - receiver - (num->set id-set))) - - - -(defn check-transfer - - ^{:callable? true - :private? true} - - ;; Returns string explaining restriction, or nil if no restriction on transfer. - - [sender receiver id-set] - - (let [id-set (num->set id-set) - sender (address sender) - msg (reduce (fn [_ id] - (when-not (check-trusted? sender - :transfer - id) - (reduced (str "No right to transfer token " - id)))) - nil - id-set)] - (or msg - (let [check-transfer-map (group-by-class id-set - 'check-transfer)] - (reduce (fn [_ [class id-set]] - (when-let [msg (call class - (check-transfer *caller* - sender - receiver - id-set))] - (reduced msg))) - nil - check-transfer-map))))) - - - - -(defn offer - - ^{:callable? true - :private? true} - - ;; Can cancel existing offer by passing in `[addr #{}]`. - - [receiver id-set] - - (let [sender *caller*, - receiver (address receiver) - id-set (num->set id-set)] - (cond - (empty? id-set) (cancel-offer sender - receiver) - :else (def offers - (assoc-in offers - [sender - receiver] - id-set))))) - - - -(def quantity-add - - ^{:callable? true - :private? true} - - union) - - - -(def quantity-sub - - ^{:callable? true - :private? true} - - difference) - - - -(defn quantity-subset? - - ^{:callable? true - :private? true} - - [a b] - - ;; Hack, treats longs as singleton set. - ;; - (if (long? a) - (contains-key? b - a) - (subset? a b))) - - -;;;;;;;;;; Private callable API - - -(defn check-trusted? - - ^{:callable? true - :doc {:description ["Determines whether a given address as certain rights.." - "A `policy-key` is either:" - "- `:destroy`, right to destroy token" - "- `:transfer`, right to transfer token" - "- `:update`, right to update token's data map" - "- `[:update key]`, right to update specific key in token's data map" - "Value for each policy key is determined at token creation (see `create-token`). Either:" - "- `:creator`, only creator" - "- `:owner`, only current owner" - "- `:none`, nobody allowed" - "- A specific address" - "- Another token id, meaning the owner of that other token has the right" - "- An asset as described in `convex.asset`"] - :signature [{:params [addr policy-key id]}]}} - - ;; For tokens that have a class actor, we hand off to the class's check-trusted? function, - ;; and the logic here is bypassed entirely. - ;; - ;; For singleton tokens, we check the token's policies (a map). - ;; The value associated with the given policy-key designates who has the rights to do that action. - ;; :creator or :owner or :none or a specific account/actor address or - ;; a token number (whoever owns that token number has the right) or - ;; asset description (whoever owns that asset/quantity has the right). - ;; [policy-key key] defaults to policy for policy-key - ;; Otherwise, a nil policy defaults to :owner - - [addr policy-key id] - - (let [token-record (get token-records - id)] - (if-let [class (:class token-record)] - (call class - (check-trusted? addr - policy-key - id)) - (let [policy (get-in token-record - [:policies - policy-key]) - policy (if (and (nil? policy) - (vector? policy-key)) - (get-in token-record - [:policies - (first policy-key)]) - policy)] - (cond - (or (nil? policy) - (= policy - :owner)) - (= addr - (get-token-owner id)) - - (= policy - :creator) - (= addr - (get-token-creator id)) - - (= policy - :none) - false - - ;; Policy is a token id. - ;; - (long? policy) - (= addr - (get-token-owner policy)) - - ;; Policy is an asset description. - ;; - (vector? policy) - (asset/owns? addr - policy) - - :else - (= addr - policy)))))) diff --git a/convex-core/src/main/cvx/convex/asset.cvx b/convex-core/src/main/cvx/convex/asset.cvx deleted file mode 100644 index b93dc19bf..000000000 --- a/convex-core/src/main/cvx/convex/asset.cvx +++ /dev/null @@ -1,392 +0,0 @@ -'convex.asset - -(call *registry* - (register {:description ["An asset is either:" - "- A vector `[asset-address quantity]` indicating an asset managed by an actor" - "- A map `asset-adress` -> `quantity` indicating assets managed by one or more actors" - "Quantities are arbitrary and asset-specific." - "For a fungible currency, quantity is the amount ; for a non-fungible token, quantity may be a set of token ids." - "Key functions from this library are `balance`, `owns?`, and `transfer`." - "Other functions such as `accept` and `offer` provide more fine-grained control when required." - "Implementing a new asset means deploying an actor which defines the following callable functions:" - "- `(accept sender quantity)`" - "- `(balance owner)`" - "- `(check-transfer sender receiver quantity)`" - "- `(direct-transfer receiver quantity)`" - "- `(get-offet sender receiver)`" - "- `(offer receiver quantity`" - "- `(quantity-add asset-address a b)`" - "- `(quantity-sub asset-address a b)`" - "- `(quantity-subset a b)` ; returns true if `a` is considered a subset of `b`" - "For more information, see those functions defined in this library whose purpose is to delegate to asset implementations in a generic way."] - :name "Asset abstraction library"})) - - -;;;;;;;;;; Private - - -(defn -make-map - - ^{:private? true} - - ;; Used by quantity functions. - - [a] - - (cond - (map? a) a - (vector? a) {(first a) (second a)} - (nil? a) {})) - - -;;;;;;;;;; Transfers - - -(defn accept - - ^{:doc {:description ["Accepts asset from sender." - "If asset contains multiple assets, accepts each in turn. MUST fail if the asset cannot be accepted."] - :examples [{:code "(accept sender [fungible-token-address 1000])"}] - :signature [{:params [sender [asset-address quantity]]}]}} - - [sender asset] - - (cond - (vector? asset) - (let [asset-address (first asset) - quantity (second asset)] - (call asset-address - (accept sender - quantity))) - - (map? asset) - (reduce (fn [m [asset-address quantity]] - (assoc m - asset-address - (call asset-address - (accept sender - quantity)))) - {} - asset))) - - - -(defn check-transfer - - ;; Independently of general transfer, you can test whether there are restrictions on transferring. - - ^{:doc {:description ["Checks whether sender can transfer this asset to receiver." - "Returns a descriptive failure message if there is a restriction prohibiting transfer, or nil if there is no restriction."] - :examples [{:code "(check-transfer sender receiver [fungible-token-address 1000])"} - {:code "(check-transfer sender receiver [non-fungible-token-address #{1 4 6}])"}] - :signature [{:params [sender receiver [asset-address quantity]]}]}} - - [sender receiver [asset-address quantity]] - - (query (call asset-address - (check-transfer sender - receiver - quantity)))) - - - -(defn get-offer - - ^{:doc {:description ["Gets the current offer from `sender` to `receiver` for a given asset." - "Returns the quantity representing the current offer. Will be the 'zero' quantity if no open offer exists."] - :examples [{:code "(get-offer asset-address sender receiver)"}] - :signature [{:params [asset-address sender receiver]}]}} - - [asset-address sender receiver] - - (query (call asset-address - (get-offer sender - receiver)))) - - - -(defn offer - - ;; For smart contract assets, you can offer and accept separately if you choose - - ^{:doc {:description ["Opens an offer of an `asset` to a `receiver`, which makes it possible for the receiver to 'accept' up to this quantity." - "May result in an error if the asset does not support open offers."] - :examples [{:code "(offer receiver [fungible-token-address 1000])"} - {:code "(offer receiver [non-fungible-token-address #{1 4 6}])"}] - :signature [{:params [receiver [asset-address quantity]]}]}} - - [receiver asset] - - (cond - (vector? asset) - (let [asset-address (first asset) - quantity (second asset)] - (call asset-address - (offer receiver - quantity))) - - (map? asset) - (reduce (fn [m [asset-address quantity]] - (assoc m - asset-address - (call asset-address - (offer receiver - quantity)))) - {} - asset))) - - - -(defn transfer - - ^{:doc {:description "Transfers asset to receiver. `data` is an arbitrary value, which will be passed to the receiver's `receive-asset` method." - :examples [{:code "(transfer receiver [fungible-token-address 1000])"} - {:code "(transfer receiver [non-fungible-token-address #{1 4 6}] optional-data)"}] - :signature [{:params [receiver asset]} - {:params [receiver asset data]}]}} - - - ([receiver asset] - - (transfer receiver - asset - nil)) - - - ([receiver asset data] - - (cond - (vector? asset) - (let [[asset-address - quantity] asset] - (cond - (callable? receiver - 'receive-asset) - (do - (call asset-address - (offer receiver - quantity)) - (call receiver - (receive-asset asset - data))) - - (actor? receiver) - (fail "Receiver does not have receive-asset function") - - (account? receiver) - (call asset-address - (direct-transfer receiver - quantity)) - - :else - (fail "Address cannot receive asset"))) - - (map? asset) - (reduce (fn [acc entry] - (assoc acc - (first entry) - (transfer receiver - entry - data))) - {} - asset) - - :else - (fail "Invalid asset")))) - - -;;;;;;;;;; Ownership - - -(defn balance - - ^{:doc {:description ["Returns asset balance for a specified owner, or the current address if not supplied." - "Return value will be in the quantity format as specified by the asset type."] - :examples [{:code "(balance asset-address owner)"}] - :signature [{:params [asset-address]} - {:params [asset-address owner]}]}} - - - ([asset-address] - - (query (call asset-address - (balance *address*)))) - - - ([asset-address owner] - - (query (call asset-address - (balance owner))))) - - - -(defn owns? - - ^{:doc {:description "Tests whether owner owns at least a given quantity of an asset", - :examples [{:code "(owns? owner [fungible-token-address 1000])"} - {:code "(owns? owner [non-fungible-token-address #{1 4 6}])"}] - :signature [{:params [owner asset]}]}} - - [owner asset] - - (query - (cond - (vector? asset) - (let [[asset-address - quantity] asset - bal (call asset-address - (balance owner))] - (call asset-address - (quantity-subset? quantity - bal))) - - (map? asset) - (reduce (fn [result [asset-address quantity]] - (if (call asset-address - (quantity-subset? quantity - (call asset-address - (balance owner)))) - true - (reduced false))) - true - asset) - - ;; Interpret nil as the 'zero' asset, which everybody owns - (nil? asset) - true))) - - -;;;;;;;;;; Quantities - - -(defn quantity-add - - ^{:doc {:description ["Adds two asset quantities. Quantities must be specified in the format required by the asset type." - "Nil may be used to indicate the 'zero' quantity."] - :examples [{:code "(quantity-add fungible-token 100 1000)"} - {:code "(quantity-add non-fungible-token #{1 2} #{3 4})"} - {:code "(quantity-add [token-a 100] [token-b 1000])"}] - :signature [{:params [asset-a asset-b]} - {:params [asset-address a b]}]}} - - - ([asset-a asset-b] - - (let [asset-a (-make-map asset-a) - asset-b (-make-map asset-b)] - (reduce (fn [m [asset-address qb]] - (let [qa (get m - asset-address)] - (assoc m - asset-address - (quantity-add asset-address - qa - qb)))) - asset-a - asset-b))) - - - ([asset-address a b] - - (query (call asset-address - (quantity-add a - b))))) - - - -(defn quantity-sub - - ^{:doc {:description ["Subracts a quantity from another quantity for a given asset. Quantities must be specified in the format required by the asset type." - "Subtracting a larger amount from a smaller amount should return 'zero' or equivalent, although the exact meaning of this operation may be asset-specific." - "Nil may be used to indicate the 'zero' quantity in inputs."] - :examples [{:code "(quantity-sub fungible-token 500 300)"} - {:code "(quantity-sub non-fungible-token #{1 2 3 4} #{2 3})"}] - :signature [{:params [asset-a asset-b]} - {:params [asset-address a b]}]}} - - - ([asset-a asset-b] - - (let [asset-a (-make-map asset-a) - asset-b (-make-map asset-b)] - (reduce (fn [m [asset-address qb]] - (let [qa (get m - asset-address)] - (if (= qa - qb) - (dissoc m - asset-address) - (assoc m - asset-address - (quantity-sub asset-address - qa - qb))))) - asset-a - asset-b))) - - - ([asset-address a b] - - (query (call asset-address - (quantity-sub a - b))))) - - - -(defn quantity-zero - - ^{:doc {:description "Returns the unique 'zero' quantity for the given asset." - :examples [{:code "(quantity-zero fungible-token)" - :result 0} - {:code "(quantity-zero non-fungible-token)" - :result #{}}] - :signature [{:params [asset-address]}]}} - - [asset-address] - - (query (call asset-address - (quantity-add nil - nil)))) - - - -(defn quantity-contains? - - ^{:doc {:description "Returns true if first quantity is >= second quantity. Any valid quantity must contain the 'zero' quantity." - :examples [{:code "(quantity-contains? fungible-token 100 60)" - :result true} - {:code "(quantity-contains? non-fungible-token #{1 2} #{2 3})" - :result false}] - :signature [{:params [asset-a asset-b]} - {:params [asset a b]}]}} - - - ([asset-a asset-b] - - (query - (let [asset-a (-make-map asset-a) - asset-b (-make-map asset-b)] - (reduce (fn [m [asset-address qb]] - (let [qa (get asset-a - asset-address)] - (cond - (= qa - qb) - true - - (call asset-address - (quantity-subset? qb - qa)) - true - - :else - (reduced false)))) - true - asset-b)))) - - - ([asset-address a b] - - (query (call asset-address - (quantity-subset? b - a))))) diff --git a/convex-core/src/main/cvx/convex/core.cvx b/convex-core/src/main/cvx/convex/core.cvx deleted file mode 100644 index 8f5e7d18f..000000000 --- a/convex-core/src/main/cvx/convex/core.cvx +++ /dev/null @@ -1,692 +0,0 @@ -;; -;; -;; Core definitions executed as part of core runtime environment bootstrap, supplementing utilities defined in Java. -;; -;; First are defined core building blocks which must be kept at the beginning of the file. Order matters! -;; Then is defined the rest of the API in alphabetical order. -;; -;; - - -;;;;;;;;;; Values - - -(def *lang* - - ^{:doc {:description ["Advanced feature. Language font-end function." - "If set to a function via `def`, will be called with the code for each transaction instead of delegating to normal `eval` behavior." - "Pre-compiled operations (see `compile`) bypass this language setting."] - :examples [{:code "(def *lang* (fn [trx] (str trx)))"}]}} - - nil) - - - -(def *registry* - - ^{:doc {:description "Address of the Convex registry actor." - :examples [{:code "(call *registry* (register {:name \"My name\"}))"}]}} - - (address 9)) - - -;;;;;;;;;; Expanders, creating macros and defining functions - - -;; TODO. Review expanders and `macro`, API is not clear. + macros cannot be used within the transaction where they are created - - -(def defexpander - - ^{:doc {:description "Advanced feature. Defines an expander in the current environment." - :examples [{:code "(defexpander expand-once [x e] (e x (fn [x e] x)))"}] - :signature [{:params [a]}]} - :expander? true} - - (fn [x e] - (let [[_ - name - & decl] x - exp (cons 'fn - decl) - form `(def ~(syntax name - {:expander? true}) - ~exp)] - (e form - e)))) - - - -(def defmacro - - ^{:doc {:description ["Like `defn` but defines a macro instead of a regular function." - "A macro is a special function that is executed at expansion, before compilation, and produces valid Convex Lisp code for subsequent execution."] - :signature [{:params [name params & body]}]} - :expander? true} - - (fn [x e] - (let [[_ - name - & decl] x - mac (cons 'fn - decl) - mmeta (meta (first decl)) - form `(def ~(syntax name - (merge mmeta - {:expander? true})) - (let [m# ~mac] - (fn [x e] - (e (apply m# - (next x)) - e))))] - (e form - e)))) - - - -(defmacro defn - - ^{:doc {:description "Defines a function in the current environment." - :examples [{:code "(defn my-square [x] (* x x))"}] - :signature [{:params [name params & body]} - {:params [name & fn-decls]}]}} - - [name & decl] - - (let [fnform (cons 'fn - decl) - _ (cond - (empty? decl) - (fail :ARITY - "`defn` requires at lest one function definition")) - fst (first decl) - name (cond - (syntax fst) - (syntax name - (meta fst)) - - name)] - `(def ~name - ~fnform))) - - - -(defmacro macro - - ^{:doc {:description "Creates an anonymous macro function, suitable for use as an expander." - :examples [{:code "(macro [x] (if x :foo :bar))"}] - :signature [{:params [params & body]}]}} - - [& decl] - - (let [mac (cons 'fn - decl) - form `(let [m# ~mac] - (fn [x e] - (e (apply m# - (next (unsyntax x))) - e)))] - form)) - - -;;;;;;;;;; Logic - - -(defmacro and - - ^{:doc {:description ["Executes expressions in sequence, returning the first falsey value (false or nil), or the last value otherwise." - "Does not evaluate later expressions, so can be used to short circuit execution." - "Returns true with no expressions present."] - :examples [{:code "(and (< 1 2) :last)"}] - :signature [{:params [& exprs]}]}} - - [& exprs] - - (let [n (count exprs)] - (cond - (== n 0) true - (== n 1) (first exprs) - :else `(let [v# ~(first exprs)] - (cond v# - ~(cons 'and - (next exprs)) - v#))))) - - - -(defmacro or - - ^{:doc {:description ["Executes expressions in sequence, returning the first truthy value, or the last value if all were falsey (false or nil)." - "Does not evaluate later expressions, so can be used to short circuit execution." - "Returns nil with no expressions present."] - :examples [{:code "(or nil 1)"}] - :signature [{:params [& exprs]}]}} - - [& exprs] - - (let [n (count exprs)] - (cond - (== n 0) nil - (== n 1) (first exprs) - :else `(let [v# ~(first exprs)] - (cond - v# - v# - ~(cons 'or - (next exprs))))))) - - -;;;;;;;;;; `cond` variants - - -(defmacro if - - ^{:doc {:description ["If `test` expression evaluates to a truthy value (anything but false or nil), executes `expr-true`. Otherwise, executes `expr-false`." - "For a more general conditional expression that can handle multiple branches, see `cond.` Also see `when`."] - :examples [{:code "(if (< 1 2) :yes :no)"}] - :signature [{:params [test expr-true]} - {:params [test expr-true expr-false]}]}} - - [test & cases] - - (cond (<= 1 - (count cases) - 2) - nil - (fail :ARITY - "`if` requires 2 or 3 arguments")) - (cons 'cond - test - cases)) - - - -(defmacro if-let - - ^{:doc {:description "Similar to `if`, but the test expression in bound to a symbol so that it can be accessed in the `expr-true` branch." - :examples [{:code "(if-let [addr (some-function)] (transfer addr 1000) (fail \"Address missing\"))"}] - :signature [{:params [[sym exp] expr-true expr-false]}]}} - - [[sym exp] & branches] - - `(let [~sym ~exp] - ~(cons 'if - sym - branches))) - - - -(defmacro when - - ^{:doc {:description "Executes body expressions in an implicit `do` block if af and only if the `test` expression evaluates to a truthy value (anything but false or nil)." - :examples [{:code "(when (some-condition) (def foo 42) (+ 2 2))"}] - :signature [{:params [test & body]}]}} - - [test & body] - - `(cond - ~test - ~(cons 'do - body) - nil)) - - - -(defmacro when-let - - ^{:doc {:description ["Executes the body with the symbol bound to the value of evaluating a given expression, if and only if the result of the expression is truthy." - "Returns nil otherwise."] - :examples [{:code "(when-let [addr (some-function)] (transfer addr 1000))"}] - :signature [{:params [[sym exp] & body]}]}} - - [[sym exp] & body] - - (let [dobody (cons 'do - body)] - `(let [~sym ~exp] - (if ~sym - ~dobody - nil)))) - - - -(defmacro when-not - - ^{:doc {:description "Like `when` but the opposite: body is executed only if the result is false or nil." - :examples [{:code "(when-not (some-condition) :okay)"}] - :signature [{:params [test & body]}]}} - - [test & body] - - `(cond - ~test - nil - ~(cons 'do - body))) - - -;;;;;;;;;; Rest of the API - - -(defn account? - - ^{:doc {:description "Returns true if the given address refers to an existing actor or user account, false otherwise." - :examples [{:code "(account? *caller*)"}] - :signature [{:params [address] - :return Boolean}]}} - - [addr] - - (cond - (address? addr) - (boolean (account addr)) - false)) - - - -(defn actor? - - ^{:doc {:description "Returns true if the given address refers to an actor." - :examples [{:code "(actor? #1345)"}] - :signature [{:params [address] - :return Boolean}]}} - - [addr] - - (cond - (address? addr) - (let [act (account addr)] - (cond act - (nil? (:key act)) - false)) - false)) - - - -(defmacro assert - - ^{:doc {:description "Evaluates each test (a form), and raises an `:ASSERT` error if any are not truthy." - :errors {:ASSERT "If a `test` form evaluates to false or nil."} - :examples [{:code "(assert (= owner *caller*))"}] - :signature [{:params [& tests]}]}} - - [& tests] - - (cons 'do - (map (fn [test] - `(cond - ~test - nil - (fail :ASSERT - ~(str "Assert failed: " - (str test))))) - tests))) - - - -(defmacro call - - ^{:doc {:description ["Calls a function in another account, optionally offering coins which the account may receive using `accept`." - "Must refer to a callable function defined in the actor, called with appropriate arguments."] - :errors {:ARGUMENT "If the offer is negative." - :ARITY "If the supplied arguments are the wrong arity for the called function." - :CAST "If the address argument is an Address, the offer is not a Long, or the function name is not a Symbol." - :STATE "If the address does not refer to an Account with the callable function specified by fn-name."} - :examples [{:code "(call some-contract 1000 (contract-fn arg1 arg2))"}] - :signature [{:params [address call-form] - :return Any} - {:params [address offer call-form] - :return Any}]}} - - [addr & more] - - (let [addr (unsyntax addr)] - (if (empty? more) - (fail :ARITY - "Insufficient arguments to call")) - (let [n (count more) - fnargs (unsyntax (last more)) - _ (or (list? fnargs) - (fail :COMPILE - "`call` must have function call list form as last argument.")) - sym (unsyntax (first fnargs)) - fnlist (cons (list 'quote - sym) - (next fnargs))] - (cond - (== n 1) (cons 'call* - addr - 0 - fnlist) - (== n 2) (cons 'call* - addr - (first more) - fnlist))))) - - - -(defn comp - - ^{:doc {:description ["Returns a function that is the composition of the given functions." - "Functions are executed left to right, The righmost function may take a variable number of arguments." - "The result of each function is passed to the next one."] - :examples [{:code "((comp inc inc) 1)"}] - :signature [{:params [f & more] - :return Function}]}} - - - ([f] - - f) - - - ([f g] - - (fn [& args] - (f (apply g - args)))) - - - ([f g h] - - (fn [& args] - (f (g (apply h - args))))) - - - ([f g h & more] - - (apply comp - (fn [x] - (f (g (h x)))) - more))) - - - -(defn create-account - - ^{:doc {:description "Creates an account with the specified account public key and returns its address." - :errors {:CAST "If the argument is not a blob key of 32 bytes."} - :examples [{:code "(create-account 0x817934590c058ee5b7f1265053eeb4cf77b869e14c33e7f85b2babc85d672bbc)"}] - :signature [{:params [key] - :return Address}]}} - - [key] - - (or (blob? key) - (fail :CAST - "create-account requires a blob key")) - (deploy `(set-key ~key))) - - -(defmacro defined? - - ^{:doc {:description "Returns true if the given symbol name is defined in the current environment, false otherwise." - :examples [{:code "(defined? max)"}] - :signature [{:params [sym]}]}} - - [sym] - - (or (symbol? sym) - (fail :CAST - "defined? requires a Symbol")) - `(boolean (lookup-meta (quote ~sym)))) - - - -(defmacro doc - - ^{:doc {:description "Returns the documentation for a given definition." - :examples [{:code "(doc count)"}] - :signature [{:params [sym]}]}} - - ;; Accepts actual symbols or lookups. - - [sym] - - `(:doc ~(if (symbol? sym) - `(lookup-meta (quote ~sym)) - `(lookup-meta ~(nth sym - 1) - (quote ~(nth sym - 2)))))) - - - -(defmacro dotimes - - ^{:doc {:description ["Repeats execution of the body `count` times, binding the specified symbol from 0 to `(- count 1)` on successive iterations." - "Always Returns nil."] - :examples [{:code "(dotimes [i 10] (transfer *address* 10))"}] - :signature [{:params [[sym count] & body]}]}} - - [[sym count] & body] - - (let [n (long count) - sym (if (symbol? (unsyntax sym)) - sym - (fail :CAST - "`dotimes` requires a symbol for loop binding"))] - `(loop [~sym 0] - (if (< ~sym - ~n) - (do - ~(cons do - body) - (recur (inc ~sym))) - nil)))) - - - -(defn filter - - ^{:doc {:description ["Filters a collection by applying the given predicate to each element." - "Each element is included in in a new collection if and onfly if the predicate returns a truthy value (anything but false or nil)."] - :errors {:CAST "If the coll argeument is not a Data Structure."} - :examples [{:code "(filter (fn [x] (> 2 x)) [1 2 3 4])"}] - :signature [{:params [key] - :return Address}]}} - - [pred coll] - - (reduce (fn [acc e] - (cond (pred e) - (conj acc - e) - acc)) - (empty coll) - ;; Lists must be reversed so that elements are conjed in the correct order. - ;; - (cond (list? coll) - (reverse coll) - coll))) - - - -(defmacro for - - ^{:doc {:description "Executes the body with the symbol `sym` bound to each value of the given sequence. Returns a vector of results." - :examples [{:code "(for [x [1 2 3]] (inc x))"}] - :signature [{:params [[sym sequence] & body]}]}} - - [[sym sequence] & body] - - `(map ~(cons 'fn - (vector sym) - body) - (vec ~sequence))) - - - -(defn identity - - ^{:doc {:description "An identity function which returns a single argument unchanged. Most useful when you want a \"do nothing\" operation in higher order functions." - :examples [{:code "(identity :foo)"} - {:code "(map identity [1 2 3])"}] - :signature [{:params [x]}]}} - - [x] - - x) - - - -(defmacro import - - ^{:doc {:description ["Imports a library for use in the current environment." - "Creates an alias to the library so that symbols defined in the library can be addressed directly in the form 'alias/symbol-name'." - "Returns the address of the imported account."] - :examples [{:code "(import some.library :as alias)"}] - :signature [{:params [& args]}]}} - - [addr as sym] - - (let [code (cond (symbol? addr) - `(or (call* *registry* - 0 - 'cns-resolve - (quote ~addr)) - (fail :NOBODY - (str "Could not resolve library name for import: " - (quote ~addr)))) - `(address ~addr)) - sym (cond (symbol? sym) - sym - (fail "import: alias must be a symbol"))] - (assert (= :as - as)) - `(def ~sym - ~code))) - - - -(defn mapcat - - ^{:doc {:description "Maps a funcion across the given collections, then concatenates the results. Nil is treated as an empty collection. See `map`." - :examples [{:code "(mapcat vector [:foo :bar :baz] [1 2 3])"}] - :signature [{:params [test & body]}]}} - - [f coll & more] - - (apply concat - (empty coll) - (apply map - f - coll - more))) - - - -(defn mapv - - ^{:doc {:description "Like `map` but systematically returns the result as a vector." - :examples [{:code "(mapv inc '(1 2 3))"}] - :signature [{:params [f & colls] }]}} - - [f & colls] - - (vec (apply map - f - colls))) - - - -(defn max - - ^{:doc {:description "Returns the numerical maximum of the given values." - :examples [{:code "(max 1 2 3)"}] - :signature [{:params [& numbers]}]}} - - [fst & more] - - (let [n (count more)] - (loop [m (+ fst - 0) ;; Adds zero to ensure number. - i 0] - (cond (>= i - n) - m - (let [v (nth more i)] - (and (nan? v) - (return v)) - (recur (cond (> v - m) - v - m) - (inc i))))))) - - - -(defn min - - ^{:doc {:description "Returns the numerical minimum of the given values." - :examples [{:code "(min 1 2 3)"}] - :signature [{:params [& numbers]}]}} - - [fst & more] - - (let [n (count more)] - (loop [m (+ fst 0) - i 0] ;; Adds zero to ensure number. - (cond (>= i - n) - m - (let [v (nth more - i)] - (and (nan? v) - (return v)) - (recur (cond (< v - m) - v - m) - (inc i))))))) - - - -(defmacro schedule - - ^{:doc {:description "Schedules a transaction for future execution under this account. Expands and compiles code now, but does not execute until the specified timestamp." - :examples [{:code "(schedule (+ *timestamp* 1000) (transfer my-friend 1000000))"}] - :signature [{:params [timestamp code]}]}} - - [timestamp code] - - `(schedule* ~timestamp - (compile ~(list 'quote - code)))) - - - -(defmacro tailcall - - ^{:doc {:description ["Advanced feature. While `return` stops the execution of a function and return, `tailcall` calls another one without consuming additional stack depth." - "Rest of the current function will never be executed."] - :examples [{:code "(tailcall (some-function 1 2 3))"}] - :signature [{:params [[f & args]] }]}} - - [callspec] - - (let [] - (or (list? callspec) - (fail :ARGUMENT - "Tailcall requires a list representing function invocation")) - (let [n (count callspec)] - (cond (== n - 0) - (fail :ARGUMENT - "Tailcall requires at least a function argument in call list")) - (cons 'tailcall* - callspec)))) - - - -(defmacro undef - - ^{:doc {:description "Opposite of `def`. Undefines a symbol, removing the mapping from the current environment if it exists." - :examples [{:code "(do (def foo 1) (undef foo))"}] - :signature [{:params [sym]}]}} - - [sym] - - `(undef* ~(list 'quote - sym))) diff --git a/convex-core/src/main/cvx/convex/core/metadata.cvx b/convex-core/src/main/cvx/convex/core/metadata.cvx deleted file mode 100644 index 5761dbb6a..000000000 --- a/convex-core/src/main/cvx/convex/core/metadata.cvx +++ /dev/null @@ -1,1158 +0,0 @@ -;; -;; -;; Metadata for core symbols that are implemented at the level of the CVM: -;; -;; - Functions defined in Java code -;; - Convex Lisp special forms -;; -;; - - -{*address* - {:doc {:description "Returns the address of the current account (user address in regular transaction, actor address in actor calls)." - :examples [{:code "(address? *address*)"}]} - :special? true} - - - *memory* - {:doc {:description "Returns the current memory Allowance for this account. May be zero - in which case any new memory allocations will be charged at the current memory exchange pool price." - :examples [{:code "*memory*"}]} - :special? true} - - - *balance* - {:doc {:description ["Returns the available balance of the current account (in Convex coins)." - "The available balance excludes reserved balance for transaction execution, so this number may be somewhat less than the total account balance during transaction execution."] - :examples [{:code "*balance*"}]} - :special? true} - - - *caller* - {:doc {:description "During an actor call, returns the address of the account doing the call. Nil otherwise." - :examples [{:code "*caller*"}]} - :special? true} - - - *depth* - {:doc {:description ["Returns the CVM execution stack depth, at the point the `*depth*` operation is executed. If the depth becomes too deep, the transaction will fail with a `:DEPTH` exception." - "InformativeIn most cases, the allowable depth should be sufficient."] - :examples [{:code "*depth*"}]} - :special? true} - - - *holdings* - {:doc {:description ["Returns the holdings blob map for this account." - "Holdings are data values controlled by other accounts (usually actors). They can be used to indicate that an account may have special rights or holdings with respect to a specific actor, for instance." - "See `get-holding`, `set-holding`."] - :examples [{:code "*holdings*"}]} - :special? true} - - - *initial-expander* - {:doc {:description "Initial expander used to expand forms, before compilation." - :examples [{:code "(expand '(if 1 2 3) *initial-expander*)"}] - :signature [{:params [form cont]}]}} - - - *juice* - {:doc {:description ["Returns the amount of execution juice remaining at this point of the current transaction." - "Juice is required for every CVM operation executed, and the transaction will fail immediately with a `:JUICE` error if an attempt is made to consume juice beyond this value."] - :examples [{:code "*juice*"}]} - :special? true} - - - *key* - {:doc {:description "Returns the public key for this account. Returns nil in the case of an actor since actors are accounts without keys." - :examples [{:code "*key*"}]} - :special? true} - - - *offer* - {:doc {:description "Returns the amount of native coin offered by `*caller*` during a call. See `call`. Will usually be zero, unless the caller has included an offer with a 'call' expression." - :examples [{:code "*offer*"}]} - :special? true} - - - *origin* - {:doc {:description ["Similar to `*caller*` but returns the address of the account that initially signed this transaction." - "In a chain of calls, this address is the very first link." - "Usually, should NOT be used for access control, since a rogue actor can potentially trick a user into creating a transaction that allows code to be indirectly executed. Consider using `*caller*` for access control instead."] - :examples [{:code "*origin*"}]} - :special? true} - - - *result* - {:doc {:description "Returns the result of the last CVM operation executed. Can be used, in some cases, to access the value of the previous expression. Will be nil for new transactions, or at the start of an actor call." - :examples [{:code "(do 1 *result*)"}]} - :special? true} - - - *sequence* - {:doc {:description "Returns the current sequence number for this account. The sequence number is equal to the number of signed transactions executed. The next valid sequence number is `(+ *sequence* 1)." - :examples [{:code "*sequence*"}]} - :special? true} - - - *state* - {:doc {:description "Returns the current CVM state record. This is a very large object, and should normally only be used temporarily to look up relevant values." - :examples [{:code "(keys *state*)"} - {:code "(get-in *state* [:accounts *address* :balance])"}]} - :special? true} - - - *timestamp* - {:doc {:description ["Returns the current timestamp." - "The timestamp is a `long` value that is equal to the greatest timestamp of any block executed (including the current block)." - "A timestamp can be interpreted as the number of milliseconds since January 1, 1970, 00:00:00 GMT. The block timestamp should always be less than or equal to the Unix timestamp of peers that are in consensus." - "Commonly used with `schedule`."] - :examples [{:code "*timestamp*"}]} - :special? true} - - - < - {:doc {:description "Tests if numeric arguments are in strict increasing order. Reads as 'less-than'." - :examples [{:code "(< 1 2 3)"}] - :signature [{:params [& xs] - :return Boolean}]}} - - - > - {:doc {:description "Tests if numeric arguments are in strict decreasing order. Reads as 'greater-than'." - :examples [{:code "(> 3 2 1)"}] - :signature [{:params [& xs] - :return Boolean}]}} - - - <= - {:doc {:description "Tests if numeric arguments are in increasing order. Reads as 'less-than-or-equal'." - :examples [{:code "(<= 1 1 3)"}] - :signature [{:params [& xs] - :return Boolean}]}} - - - >= - {:doc {:description "Tests if numeric arguments are in decreasing order. Reads as 'greater-than-or-equal'." - :examples [{:code "(>= 3 2 2)"}] - :signature [{:params [& xs] - :return Boolean}]}} - - - == - {:doc {:description ["Tests if arguments are equal in numerical value." - "Difference with `=` is that types are erased (eg. `long` value is comparable with a `double` value)."] - :examples [{:code "(== 2 2.0)" - :return "true"}] - :signature [{:params [& xs] - :return Boolean}]}} - - - = - {:doc {:description "Tests if arguments are equal in value." - :examples [{:code "(= :foo :foo)" - :return "true"}] - :signature [{:params [& vals] - :return Boolean}]}} - - - + - {:doc {:description "Adds numerical arguments. Result will be a `long` if all arguments are integers, or a double if any floating point values are included." - :examples [{:code "(+ 1 2 3)"}] - :signature [{:params [& xs]}]}} - - - - - {:doc {:description "Subtracts numerical arguments from the first argument. Negates a single argument." - :examples [{:code "(- 10 7)"}] - :signature [{:params [x]} - {:params [x y & more]}]}} - - - * - {:doc {:description "Multiplies numeric arguments. Result will be a `double` if any arguments are floating point values, otherwise it will be a `long`." - :examples [{:code "(* 1 2 3 4 5)"}] - :signature [{:params [& xs]}]}} - - - / - {:doc {:description "Double precision point divide. With a single argument, returns the reciprocal of a number. With multiple arguments, divides the first argument by the others in order." - :examples [{:code "(/ 10 3)"}] - :signature [{:params [divisor]} - {:params [numerator divisor]} - {:params [numerator divisor & more]}]}} - - - abs - {:doc {:description "Computes the absolute value of a numerical argument. Supports `double` and `long` results." - :errors {:CAST "If the parameter is not a number."} - :examples [{:code "(abs -1.5)"} - {:code "(abs 100)"}] - :signature [{:params [x] - :return Number}]}} - - - accept - {:doc {:description ["Used during a `call`, accepts offered coins up to the amount of `*offer*` from `*caller*`. Returns the amount accepted if successful." - "Amount must cast to `long`. If successful, the amount will be added immediately to the `*balance*` of the current `*address*`." - "This is the recommended way of transferring balance between actors, as it requires a positive action to confirm receipt."] - :errors {:ARGUMENT "If accepted amount is negative" - :CAST "If accepted amount is not a `long`" - :STATE "If ´*caller*´ has not offered sufficient coins to fulfil the offer"} - :examples [{:code "(accept *offer*)"}] - :signature [{:params [amount] - :return Number}]}} - - - account - {:doc {:description "Returns the account record for a given addess, or nil if the account does not exist. Argument must cast to `address`." - :errors {:CAST "If the argument is not a valid Address."} - :examples [{:code "(account *address*)"}] - :signature [{:params [address] - :return Account}]}} - - - address - {:doc {:description "Casts the argument to an `address`. Valid argument is a hex strings, a `long`, and `address` or a `blob` with the correct length (8 bytes)." - :errors {:CAST "If the argument is not castable to a valid `address`."} - :examples [{:code "(address 451)"}] - :signature [{:params [x] - :return Address}]}} - - - address? - {:doc {:description "Returns true if the argument is an actual `address`." - :examples [{:code "(address? #777)"} - {:code "(address? :foo)"}] - :signature [{:params [x] - :return Boolean}]}} - - - apply - {:doc {:description ["Applies a function to the specified arguments, after flattening the last argument. Last argument must be a sequential collection, or 'nil' which is considered an empty collection." - "Only useful in particular cases when interacting with variadic functions."] - :errors {:ARITY "If the additional arguments cause an arity error in the applied function." - :CAST "If the first argument is not castable to a valid function."} - :examples [{:code "(apply + [1 2 3])"} - {:code "(apply + 1 2 [3 4 5])"}] - :signature [{:params [f & args more-args] - :return Any}]}} - - assoc - {:doc {:description "Adds entries into an associative data structure, taking each two arguments as key/value pairs. A nil data structure is considered as an empty map." - :errors {:ARITY "If the additional arguments are not an even number (key and value pairs)." - :ARGUMENT "If one or more of the supplied keys is invalid for the data structure." - :CAST "If the first argument is not a valid DataStructure."} - :examples [{:code "(assoc {1 2} 3 4)"}] - :signature [{:params [coll & kvs] - :return DataStructure}]}} - - - assoc-in - {:doc {:description "Associates a value entries into an nested associative data structure, as if using `assoc` at each level." - :errors {:ARGUMENT "If one or more of the supplied keys is invalid for the data structure." - :CAST "If the first argument is not a valid data structure, or the second argument is not a a sequential data structure."} - :examples [{:code "(assoc-in {1 [1 2 3]} [1 2] 4)"}] - :signature [{:params [coll keys v] - :return DataStructure}]}} - - balance - {:doc {:description "Returns the coin balance of the specified account, which must be an `address`. Returns nil if and only the account does not exist." - :errors {:CAST "If the argument is not a valid address."} - :examples [{:code "(balance *caller*)"}] - :signature [{:params [address] - :return Long}]}} - - - blob - {:doc {:description "Casts the argument to a blob. Arugment can be an `address`, a hex string, or another `blob`." - :errors {:CAST "If the argument is not castable to a blob."} - :examples [{:code "(blob \"1234abcd\")"}] - :signature [{:params [address] - :return Blob}]}} - - - blob-map - {:doc {:description ["Creates a blob map. Blob maps support blob types as keys only (see `blob`)." - "Optional arguments must be pairs of blob keys and values to be included in the blob map."] - :errors {:ARGUMENT "If any of the keys supplied is not castable to a blob." - :ARITY "If there is not an even number of arguments (key and value pairs)."} - :examples [{:code "(blob-map 0x1234 :foo)"}] - :signature [{:params [& kvs] - :return BlobMap}]}} - - - blob? - {:doc {:description "Returns true if the argument is a `Blob`, false otherwise." - :examples [{:code "(blob? 0x1234)"}] - :signature [{:params [x] - :return Boolean}]}} - - - boolean - {:doc {:description "Casts any value to a `boolean`. Returns false if value is nil or actually `false`, true in any other case." - :examples [{:code "(boolean 123)"}] - :signature [{:params [x] - :return Boolean}]}} - - - boolean? - {:doc {:description "Returns true if the argument is a boolean (either `true` or `false`)." - :examples [{:code "(boolean? false)"}] - :signature [{:params [x] - :return Boolean}]}} - - - byte - {:doc {:description "Casts a value to a Byte. Discards high bits of larger integer types." - :errors {:CAST "If the argument is not castable to a Byte."} - :examples [{:code "(byte 1234)"}] - :signature [{:params [x] - :return Byte}]}} - - - call* - {:doc {:description ["Like `call` but lower-level. Instead of a form, takes a symbol referring to the function, and then arguments separately." - "Same kind of errors may happen."] - :errors {:ARITY "If the supplied arguments are the wrong arity for the called function." - :CAST "If the address argument is an Address, the offer is not a Long, or the function name is not a Symbol." - :STATE "If the address does not refer to an Account with the callable function specified by fn-name." - :ARGUMENT "If the offer is negative."} - :examples [{:code "(call* some-actor 1000 'contract-fn arg1 arg2)"}] - :signature [{:params [address offer fn-name & args] - :return Any}]}} - - - ceil - {:doc {:description "Computes the mathematical ceiling (rounding up towards positive infinity) for a numerical argument. Uses double precision mathematics." - :errors {:CAST "If the argument is not a number."} - :examples [{:code "(ceil 16.3)"}] - :signature [{:params [x] - :return Double}]}} - - - char - {:doc {:description "Casts a value to a `char`. Discards high bits of larger integer types." - :errors {:CAST "If the argument is not castable to a character."} - :examples [{:code "(char 97)"}] - :signature [{:params [x] - :return Character}]}} - - coll? - {:doc {:description "Returns true if the argument is a collection: list, map, set, or vector. Returns false otherwise." - :examples [{:code "(coll? [1 2 3])"}] - :signature [{:params [x] - :return Boolean}]}} - - - cond - {:doc {:description ["Performs conditional tests on successive pairs of `test` -> `result`, returning the `result` for the first `test` that succeeds." - "Performs short-circuit evaluation: result expressions that are not used and any test expressions after the first success will not be executed." - "In the case that no test succeeds, a single aditional argument may be added as a fallback value. If no fallback value is available, nil will be returned."] - :examples [{:code "(cond test-1 result-1 else-value)"} - {:code "(cond test-1 result-1 test-2 result-2 test-3 result--3)"}] - :signature [{:params []} - {:params [test]} - {:params [test result]} - {:params [test result fallback-value]} - {:params [test-1 result-1 test-2 result-2 & more]}]} - :special? true} - - - compile - {:doc {:description "Compiles a form, returning an operation. See `eval`." - :errors {:COMPILE "If a compiler error occurs."} - :examples [{:code "(compile '(fn [x] (* x 2)))"}] - :signature [{:params [form] - :return Op}]}} - - - concat - {:doc {:description "Concatenates sequential data structures (lists or maps), returning a new sequential data structure of the same type as the first non-nil argument. Nil is treated as an empty sequence." - :errors {:CAST "If any of the arguments is neither a sequential data structure nor nil."} - :examples [{:code "(concat [1 2] [3 4])"}] - :signature [{:params [& seqs] - :return DataStructure}]}} - - - conj - {:doc {:description "Adds elements to a data structure, in the natural mode of addition for the data structure. Supports sequential collections, sets and maps." - :errors {:ARGUMENT "If a provided element is not of correct type for the given data structure." - :CAST "If the first argument is not a data structure (or nil)."} - :examples [{:code "(conj [1 2] 3)"} - {:code "(conj {1 2} [3 4])"} - {:code "(conj #{1 2} 3)"}] - :signature [{:params [coll & elems] - :return DataStructure}]}} - - cons - {:doc {:description "Constructs a list, by prepending the leading arguments to the last argument. The last argument must be coercable to a sequential data structure." - :errors {:CAST "If the last argument is not a sequence."} - :examples [{:code "(cons 1 '(2 3))"} - {:code "(cons 1 2 '(3 4))"}] - :signature [{:params [arg & more-args coll]}]}} - - - contains-key? - {:doc {:description "Returns true if the given associative data structure contains the given key, false otherwise." - :examples [{:code "(contains-key? {:foo 1 :bar 2} :foo)"}] - :signature [{:params [coll key] - :return Boolean}]}} - - - create-peer - {:doc {:description "Creates a new peer on the network. The peer must have an account number and sufficient balance to place a stake amount" - :errors {:CAST "If the first argument is not a valid account-key."} - :examples [{:code "(create-peer account-key 700000000)"}] - :signature [{:params [account-key stake-amount] - :return stake-amount}]}} - - - count - {:doc {:description "Returns the number of elements in the given collection, blob, or string." - :errors {:CAST "If the argument is not a countable object."} - :examples [{:code "(count [1 2 3])"}] - :signature [{:params [coll] - :return Long}]}} - - dec - {:doc {:description "Decrements the given `long` by 1." - :errors {:CAST "If the argument is not a long."} - :examples [{:code "(dec 10)"}] - :signature [{:params [long] - :return Long}]}} - - - def - {:doc {:description ["Creates a definition in the current environment. This value will persist in the environment owned by the current account." - "The name argument must be a symbol, or a Symbol wrapped in a syntax object with optional metadata."] - :errors {:CAST "If the argument is neither a valid symbol name nor a syntax containing a symbol value."} - :examples [{:code "(def a 10)"}] - :signature [{:params [name value]}]} - :special? true} - - - deploy - {:doc {:description "Deploys an actor. The code provided will be executed to initialise the actor's account. Returns the Address of the deployed actor." - :errors {:COMPILE "If a compiler error occurred deploying the given code."} - :examples [{:code "(deploy '(defn my-fn [x y] (+ x y)))"}] - :signature [{:params [code] - :return Address}]}} - - - difference - {:doc {:description "Computes the difference of one or more sets. Nil is accepted and treated like an empty set." - :errors {:CAST "If any of the arguments is neither a set nor nil."} - :examples [{:code "(difference #{1 2} #{2 3})"}] - :signature [{:params [set & more] - :return Set}]}} - - - disj - {:doc {:description "Removes the specified key(s) from a set. Nil is treated as an empty Set." - :errors {:CAST "If the first argument is not a set."} - :examples [{:code "(disj #{1 2 3} 1)"}] - :signature [{:params [coll key]}]}} - - - dissoc - {:doc {:description "Removes entries with the specified key(s) from a map or blob map." - :errors {:CAST "If the first argument is not a map."} - :examples [{:code "(dissoc {1 2 3 4} 3)"}] - :signature [{:params [coll & keys] - :return Map}]}} - - do - {:doc {:description "Executes multiple expressions sequentially, and returns the value of the final expression." - :examples [{:code "(do (count [1 2 3]) :done)"}] - :signature [{:params [& expressions]}]} - :special? true} - - - double - {:doc {:description "Casts any numerical value to a `double`." - :errors {:CAST "If the argument is not castable to double."} - :examples [{:code "(double 3)"}] - :signature [{:params [x] - :return Double}]}} - - empty - {:doc {:description "Returns an empty collection of the same type as the argument. `(empty nil)` returns nil." - :errors {:CAST "If the argument is neither nil nor a data structure."} - :examples [{:code "(empty [1 2 3])"}] - :signature [{:params [coll] - :return DataStructure}]}} - - - empty? - {:doc {:description "Checks if the argument is an empty collection. Nil is considered empty. " - :errors {:CAST "If the argument is neither nil nor a data structure."} - :examples [{:code "(empty? [])"}] - :signature [{:params [coll] - :return Boolean}]}} - - - encoding - {:doc {:description ["Returns the byte encoding for a given value as a blob." - "The encoding is the unique canonical binary representation of a value. Encodings may change between Convex versions - it is unwise to rely on the exact representation."] - :examples [{:code "(encoding {1 2})"}] - :signature [{:params [value] - :return Blob}]}} - - - eval - {:doc {:description "Evaluates code in the current context, expanding and compiling the form if necessary (see `expand`,`compile`)." - :errors {:COMPILE "If a compiler error occurred evaluating the form." - :EXPAND "If an expander error occurred while expanding the form."} - :examples [{:code "(eval '(+ 1 2))"}] - :signature [{:params [form]}]}} - - eval-as - {:doc {:description "Like `eval` but evaluates code in the environment of the specifed account. The current account must have controller privileges to execute this operation (see `set-controller`)." - :examples [{:code "(eval-as #666 '(+ 1 2))"}] - :signature [{:params [address form]}]}} - - - exp - {:doc {:description "Returns `e` raised to the power of the given numerical argument." - :errors {:CAST "If the argument is not a number."} - :examples [{:code "(exp 1.0)"}] - :signature [{:params [x] - :return Double}]}} - - - expand - {:doc {:description ["Expands the given form." - "Uses the specified expander (including macros) as the primary expander if provided, the default `*initial-expander*` otherwise." - "If also provided, a continuation expander will be passed to the primary expander, otherwise the primary will be used as its own continuation." - "Expanders are an advanced feature."] - :examples [{:code "(expand '(if a :truthy :falsey))"}] - :signature [{:params [form]} - {:params [form expander]} - {:params [form expander cont]}]}} - - - callable? - {:doc {:description "Returns true the given function name (a symbol) is a callable function in actor. See `call`." - :errors {:CAST "If the actor argument is not an address."} - :examples [{:code "(callable? actor-address 'function-name)"}] - :signature [{:params [actor symbol]}]}} - - - fail - {:doc {:description ["Causes execution to fail at the current position." - "Error type defaults to `:ASSERT` if not specified, and cannot be nil. Typically a keyword, it can actually be any value." - "Error message defaults to nil if not specified. The message may be any value, but the use of short descriptive strings is recommended."] - :examples [{:code "(fail :ASSERT \"Assertion failed\")"}] - :signature [{:params []} - {:params [message]} - {:params [error-type message]}]}} - - - first - {:doc {:description "Returns the first element from a collection which must contain at least one element. Also see `empty?`" - :errors {:BOUNDS "If the collection is empty." - :CAST "If the first argument is not a countable collection."} - :examples [{:code "(first [1 2 3])"}] - :signature [{:params [coll]}]}} - - floor - {:doc {:description "Computes the mathematical floor (rounding down towards negative infinity) for a numerical argument. Uses double precision mathematics." - :examples [{:code "(floor 16.3)"}] - :signature [{:params [x] - :return Double}]}} - - - fn - {:doc {:description "Creates an anonymous function (closure) with the specified argument list and function body. Will close over variables in the current lexical scope." - :examples [{:code "(let [f (fn [x y] (* x y))] (f 10 7))"}] - :signature [{:params [args & body]}]} - :special? true} - - - fn? - {:doc {:description "Returns true if the argument is a function, false otherwise. Some values may sometimes operate as functions but are not functions themselves (eg. maps and vectors)." - :examples [{:code "(fn? count)"}] - :signature [{:params [x] - :return Boolean}]}} - - get - {:doc {:description ["Gets an element from a collection at the specified index value. Works on all collection types including maps, sets and sequences. Nil is treated as an empty collection." - "If the index is not present, returns `not-found` value (nil by default)."] - :examples [{:code "(get {:foo 10 :bar 15} :foo)"}] - :signature [{:params [coll key]} - {:params [coll key not-found]}]}} - - get-holding - {:doc {:description "Gets the holding value for a specified owner account address. Owner account must exist. Holding will be null by default. See `*holdings*`, `set-holding`." - :errors {:CAST "If the argument is not an address."} - :examples [{:code "(get-holding *caller*)"}] - :signature [{:params [owner]}]}} - - - get-in - {:doc {:description "Gets an element by successively looking up keys in a collection according to the logic of `get`. If any lookup does not find the appropriate key, will return `not-found` (nil by default)." - :errors {:CAST "If the first argument is not an associative collection."} - :examples [{:code "(get-in [[1 2] [3 4]] [1 1])"}] - :signature [{:params [coll keys]} - {:params [coll keys not-found]}]}} - - halt - {:doc {:description ["Completes execution in the current context with the specified result, or null if not provided. Does not roll back any state changes made." - "If the currently executing context is an actor, the result will be used as the return value from the actor call."] - :examples [{:code "(halt :we-are-finished-here)"}] - :signature [{:params []} - {:params [result]}]}} - - hash - {:doc {:description "Calculates the 32-byte SHA3-256 cryptographic hash of a `blob` or an `address` (which is a specialized type of `blob`). Returns a 32-byte `blob`." - :examples [{:code "(hash 0x1234)"} - {:code "(hash (encoding :foo))"}] - :signature [{:params [value] - :return Blob}]}} - - - hash-map - {:doc {:description "Constructs a map with the given keys and values. If a key is repeated, the last value will overwrite previous ones." - :errors {:ARITY "If the number of arguments is not even (key-value pairs)."} - :examples [{:code "(hash-map 1 2 3 4)"}] - :signature [{:params [& kvs]}]}} - - - hash-set - {:doc {:description "Constructs a set with the given values. If a value is repeated, it will be included only once in the set." - :examples [{:code "(hash-set 1 2 3)"}] - :signature [{:params [& values]}]}} - - - inc - {:doc {:description "Increments the given `long` by 1." - :errors {:CAST "If the actor argument is not a `long`."} - :examples [{:code "(inc 10)"}] - :signature [{:params [num] - :return Long}]}} - - - intersection - {:doc {:description "Computes the intersection of one or more sets. Nil is treated as an empty set." - :errors {:CAST "If any of the arguments is neither a set nor nil."} - :examples [{:code "(intersection #{1 2} #{2 3})"}] - :signature [{:params [set & more] - :return Set}]}} - - - into - {:doc {:description "Adds elements to a collection, in a collection-defined manner as with `conj`." - :errors {:ARGUMENT "If any of the elements is not a valid type for the given data structure." - :CAST "If either argument is not a data structure."} - :examples [{:code "(into {} [[1 2] [3 4]])"}] - :signature [{:params [coll elements] - :return DataStructure}]}} - - keys - {:doc {:description "Returns a vector of keys in the given map, in the map defined order. Also see `values`." - :errors {:CAST "If the argument is not a map."} - :examples [{:code "(keys {:foo 1 :bar 2})"}] - :signature [{:params [m] - :return Vector}]}} - - keyword - {:doc {:description "Coerces the argument to a keyword: a symbol, a keyword, or a string between 1 and 64 characters." - :errors {:ARGUMENT "If the keyword name is of illegal length." - :CAST "If the argument is not of a type castable to keyword."} - :examples [{:code "(keyword \"foo\")"}] - :signature [{:params [name] - :return Keyword}]}} - - - keyword? - {:doc {:description "Returns true if the argument is a keyword, false otherwise." - :examples [{:code "(keyword? :foo)"}] - :signature [{:params [x] - :return Boolean}]}} - - - last - {:doc {:description "Returns the last element of a data structure, in collection-defined order. Collection argument must be coercible to a sequential data structure." - :errors {:CAST "If the argument is not coercible to a sequential data structure (list or vector)."} - :examples [{:code "(last [1 2 3])"}] - :signature [{:params [coll]}]}} - - - let - {:doc {:description "Binds local variables according to symbol-expression pairs in a binding vectors, then execute following expressions in an implicit do block." - :examples [{:code "(let [x 10] (* x x))"}] - :signature [{:params [bindings & exps]}]} - :special? true} - - - list - {:doc {:description "Creates a list containing the given arguments as elements." - :examples [{:code "(list 1 2 3)"}] - :signature [{:params [& elements] - :return List}]}} - - - list? - {:doc {:description "Returns true if the argument is a list." - :examples [{:code "(list? :foo)"}] - :signature [{:params [x] - :return Boolean}]}} - - - log - {:doc {:description ["Outputs a sequence of values to the CVM log for the current account. Any valid CVM values can be logged. Returns a vector containing logged values." - "The CVM log is NOT stored on chain. It may be used by peers for external audits."] - :examples [{:code "(log :EVENT 123 [:some :data])"}] - :signature [{:params [& values] - :return Vector}]}} - - - long - {:doc {:description "Casts the given argument to a 64-bit signed integer." - :errors {:CAST "If the argument is not castable to `long`."} - :examples [{:code "(long 10)"}] - :signature [{:params [num]}]}} - - - long? - {:doc {:description "Returnes true if the argment is a `long` value, false otherwise." - :examples [{:code "(long? 1234)"}] - :signature [{:params [x] - :return Boolean}]}} - - - lookup - {:doc {:description "Looks up the value of a symbol in the current execution environment, or the account of the given address if specified." - :errors {:NOBODY "If the target account for lookup does not exist."} - :examples [{:code "(do (def a 13) (lookup a))"} - {:code "(lookup #8 count)"}] - :signature [{:params [sym]} - {:params [address sym]}]} - :special? true} - - - lookup-meta - {:doc {:description "Looks up metadata for a symbol in the current execution environment, or the account of the given address if specified. Returns nil if not found." - :examples [{:code "(lookup-meta 'count)"}] - :signature [{:params [name]} - {:params [address name]}]}} - - - loop - {:doc {:description ["Creates a loop body, binding one or more loop variables in a manner similar to `let`." - "Within the loop body, `recur` can be used to return to the start of the loop while re-binding the loop variables with new values. Does not consume stack."] - :examples [{:code "(loop [i 10 acc 1] (if (> i 1) (recur (dec i) (* acc i)) acc))"}] - :signature [{:params [bindings & body]}]} - :special? true} - - - map - {:doc {:description "Applies a function to each element of a data structure in sequence, and returns a vector of results. Additional collection may be provided to call a function with higher arity." - :examples [{:code "(map inc [1 2 3])"}] - :signature [{:params [f coll]} - {:params [f coll1 coll2 & more-colls ]}]}} - - - map? - {:doc {:description "Returns true if argument is a map, false otherwise." - :examples [{:code "(map? {1 2})"}] - :signature [{:params [coll] - :return Boolean}]}} - - merge - {:doc {:description "Merges zero or more maps (not blob maps), replacing existing values. Nil is considered as an empty map." - :examples [{:code "(merge {1 2 3 4} {3 5 7 9})"}] - :signature [{:params [& maps]}]}} - - - meta - {:doc {:description "Returns metadata for a `syntax` object. Returns nil if the argument is not a syntax object." - :examples [{:code "(meta (syntax 'foo {:bar 1}))"}] - :signature [{:params [syntax] - :return Map}]}} - - mod - {:doc {:description "Returns the integer modulus of a numerator divided by a divisor. The result will always be positive, in consistent with Euclidean Divsion." - :examples [{:code "(mod 13 5)"}] - :signature [{:params [num div]}]}} - - - nan? - {:doc {:description "Returns true if argment is `##NaN`. Returns false for any other value (including non-numerical arguments)." - :examples [{:code "(nan? ##NaN)"}] - :signature [{:params [x] - :return Boolean}]}} - - - name - {:doc {:description "Gets the string name of an object: a keyword, a symbol, or a string." - :errors {:CAST "If the argument is not castable to a String name."} - :examples [{:code "(name :foo)" - :return "\"foo\""}] - :signature [{:params [named-object] - :return String}]}} - - - next - {:doc {:description "Returns the elements of a sequential data structure after the first element, or nil if no more elements remain." - :errors {:CAST "If the argument is not a sequential data structure."} - :examples [{:code "(next [1 2 3])"}] - :signature [{:params [coll] - :return Sequence}]}} - - - nil? - {:doc {:description "Returns true if argument is nil, false otherwise." - :examples [{:code "(nil? nil)"}] - :signature [{:params [x] - :return Boolean}]}} - - - not - {:doc {:description "Inverts a truth value. Returns true on false or nil, returns false on any other value." - :examples [{:code "(not true)"} - {:code "(not nil)"}] - :signature [{:params [b] - :return Boolean}]}} - - - nth - {:doc {:description ["Gets the nth element of a countable data structure: a collection as defined in `collection?`, a blob, or a string." - "The index must be a valid long between 0 (inclusive) and the element count of the collection (exclusive)."] - :errors {:CAST "If the first argument is not countable data structure."} - :examples [{:code "(nth [1 2 3] 2)"}] - :signature [{:params [coll index]}]}} - - - number? - {:doc {:description "Returns true if the argument is a numeric value, false otherwise." - :examples [{:code "(number? 2.3)"}] - :signature [{:params [x] - :return Boolean}]}} - - - pow - {:doc {:description "Returns the first argument raised to the power of the second argument. Uses double precision maths." - :errors {:CAST "If the argument is not a Number."} - :examples [{:code "(pow 2 3)"}] - :signature [{:params [x y] - :return Double}]}} - - - quasiquote - {:doc {:description "Returns the quoted value of a form, without evaluating it. Like 'quote', but elements within the form may be unquoted via `unquote`." - :examples [{:code "(quasiquote foo)" - :return "foo"} - {:code "(quasiquote (:a :b (unquote (+ 2 3))))" - :return "(:a :b 5)"}] - :signature [{:params [form]}]} - :expander? true - :special? true} - - - query - {:doc {:description "Runs forms in query mode. When returning result, any state change will be rolled back as if nothing happened." - :examples [{:code "(query (def a 10) a)"} - {:code "(query (call unsafe-actor (do-something)))"}] - :signature [{:params [& forms]}]} - :special? true} - - - quot - {:doc {:description "Returns the quotient of a numerator divided by a divisor. Performs truncated division (ie. rounds towards zero)." - :examples [{:code "(quot 13 5)"}] - :signature [{:params [num div]}]}} - - - - quote - {:doc {:description "Returns the quoted value of a form, without evaluating it. For example, you can quote a symbol to get the symbol itself rather than the value in the environment that it refers to." - :examples [{:code "(quote foo)"} - {:code "(eval (quote (+ 1 2 3)))"}] - :signature [{:params [form]}]} - :expander? true - :special? true} - - - recur - {:doc {:description "Escapes from the currently executing code and recurs at the level of the next loop or function body." - :examples [{:code "(recur acc (dec i))"}] - :signature [{:params [x y]}]} - :special? true} - - - reduce - {:doc {:description ["Efficient and convenient looping mechanism." - "Reduces over a collection, calling `f` with an accumulator value and, successively each element of that collection (see example showing a summation)." - "Looping can be stopped at any moment by calling `reduced`. Otherwise, all elements in the collection are processed." - "If an initial accumulator value is not supplied, the initial value will be determined by calling the function on the first 0, 1 or 2 elements of the collection (however many are available)."] - :errors {:CAST "If the first argument is not a function, or the final argument is not a sequential collection."} - :examples [{:code "(reduce (fn [acc item] (+ acc item)) 0 [1 2 3 4 5])"}] - :signature [{:params [f coll]} - {:params [f init coll]}]}} - - - reduced - {:doc {:description "Returns immediately from the enclosing `reduce` function, providing the given value as the result of the whole `reduce` operation. This can be used to terminate early from a reduce operation, saving transaction costs." - :examples [{:code "(reduce (fn [acc x] (reduced :exit)) 1 [1 2 3 4 5])"}] - :signature [{:params [result]}]}} - - - rem - {:doc {:description "Returns the remainder of a numerator divided by a divisor, consistent with division performed by `quot`. The remainder will therefore have the same sign as the numerator." - :examples [{:code "(rem 13 5)"}] - :signature [{:params [num div]}]}} - - - return - {:doc {:description "Escapes from the currently executing code and returns the specified value from the current function. Expressions following `return` will not be executed." - :examples [{:code "(do (return :finished) 42)"}] - :signature [{:params [value]}]} - :special? true} - - - reverse - {:doc {:description "Reverses a sequential data structure. Lists are converted to vectors, and vice versa for efficieny reasons. Nil is treated as an empty vector." - :examples [{:code "(reverse [1 2 3])"}] - :signature [{:params [value] - :return Sequence}]} - :special? true} - - - rollback - {:doc {:description "Escapes from the currently executing smart contract. Rolls back any state changes, as if nothing happened during that transaction. Returns the given value." - :examples [{:code "(rollback :aborted)"}] - :signature [{:params [value]}]} - :special? true} - - - schedule* - {:doc {:description "Schedules a form for future execution under this account. Expands and compiles form now, but does not execute until the specified timestamp." - :examples [{:code "(schedule* (+ *timestamp* 1000) '(transfer my-friend 1000000))"}] - :signature [{:params [timestamp code]}]}} - - - second - {:doc {:description "Returns the second element of a countable collection." - :errors {:BOUNDS "If the argument does not have a second value" - :CAST "If the argument is not a countable collection."} - :examples [{:code "(second [1 2 3])"}] - :signature [{:params [coll]}]}} - - - set - {:doc {:description "Coerces any data structure to a set." - :errors {:CAST "If the argument is not a countable data structure."} - :examples [{:code "(set [1 2 3])"}] - :signature [{:params [coll] - :return Set}]}} - - - set! - {:doc {:description ["Sets a local binding identified by an unqualified symbol to the given value." - "This local change will be be visible until the scope leaves the current binding form (eg. `let` binding, function body or recur)." - "This is probably most useful for updating a local variable in imperative style. Returns the value assigned to the local binding if successful."] - :errors {:ARGUMENT "If the symbol is qualified."} - :examples [{:code "(let [a 10] (set! a 20) a)"}] - :signature [{:params [sym value]}]} - :special? true} - - - set-controller - {:doc {:description ["Sets the controller for the current account." - "Controller account is granted powerful access privileges including the ability to run `eval-as`. Setting to nil disable such control (default value)."] - :errors {:CAST "If the argument is neither a validaAddress nor nil" - :NOBODY "If the address does not refer to an existing account."} - :examples [{:code "(set-controller #9)"}] - :signature [{:params [addr]}]}} - - - set-holding - {:doc {:description "Sets the holding value for a specified owner account address. Owner account must exist. Returns the new holding value." - :errors {:CAST "If the first argument is not an address."} - :examples [{:code "(set-holding *caller* 1000)"}] - :signature [{:params [owner value]}]}} - - - set-key - {:doc {:description ["Sets the public key (32-byte blob) for this account. May set to nil to turn this account into an actor and disable future external transactions." - "WARNING: You may lose access to the account if you do not have access to the associated private key."] - :signature [{:params [new-key]}]}} - - - set-memory - {:doc {:description "Sets the free memory allowance for the current account address, in number of bytes. Increases in memory allowance may cost coin balance. Decreases in memory allowance may earn a coin refund." - :examples [{:code "(set-memory 10000)"}] - :signature [{:params [mem]}]}} - - - set-peer-data - {:doc {:description "Sets metadata on a given peer. Metadata must be a map. Peer must exist, with a public account key." - :errors {:CAST "If the first argument is not a valid peer Key, or the second argument is not a map."} - :examples [{:code "(set-peer-data peer-key {:url \"my-peer.com:4242\"})"}] - :signature [{:params [peer map]}]}} - - - set-peer-stake - {:doc {:description "Sets the peer stake. Stake must be a `long`. Peer must exist, with a public account key." - :errors {:CAST "If the first argument is not a valid peer key, or the second argument is not a `long`."} - :examples [{:code "(set-peer-data peer-key {:url \"my-peer.com:4242\"})"}] - :signature [{:params [peer stake]}]}} - - - set? - {:doc {:description "Returns true if the argument is a set, false otherwise." - :examples [{:code "(set? #{1 2 3})"}] - :signature [{:params [x] - :return Boolean}]}} - - - signum - {:doc {:description "Returns the signum of a numeric value, defined to be -1, 0 or 1. Results in a cast error if the argument is not a number (including `##NaN`)." - :examples [{:code "(signum -1)"}] - :signature [{:params [x] - :return Double}]}} - - - sqrt - { :doc {:description "Computes the square root of a numerical argument. Uses double precision mathematics. May return `##NaN` for negative values." - :examples [{:code "(sqrt 16.0)"}] - :signature [{:params [x] - :return Double}]}} - - - stake - {:doc {:description "Sets the stake on a given peer. Peer must exist, and funds must be available to set the stake to the specified level. Setting stake to zero removes the stake entirely." - :examples [{:code "(stake trusted-peer-account-key 7000000000)"}] - :signature [{:params [account-key amount]}]}} - - str - {:doc {:description "Coerces values into strings and concatenates them." - :examples [{:code "(str \"Hello \" name)"}] - :signature [{:params [& args] - :return String}]}} - - - str? - {:doc {:description "Returns true if the argument is a string, false otherwise." - :examples [{:code "(str? name)"}] - :signature [{:params [x] - :return Boolean}]}} - - - subset? - {:doc {:description "Returns true if `set-1` is a subset of `set-2u. Both arguments must be sets, nil being considered as an empty set." - :examples [{:code "(subset? #{1} #{1 2 3})"}] - :signature [{:params [set-1 set-2] - :return Boolean}]}} - - - symbol - {:doc {:description "Creates a symbol from keyword, a symbol, or a string between 1 and 64 character" - :examples [{:code "(symbol :foo)"}] - :signature [{:params [name] - :return Symbol}]}} - - - str? - {:doc {:description "Returns true if the argument is a string, false otherwise." - :examples [{:code "(str? \"foo\")"}] - :signature [{:params [x] - :return Boolean}]}} - - - symbol? - {:doc {:description "Returns true if the argument is a symbol, false otherwise." - :examples [{:code "(symbol? 'foo)"}] - :signature [{:params [x] - :return Boolean}]}} - - - syntax - {:doc {:description "Wraps a value as a syntax object, if it is not already one. If metadata is provided, merge the metadata into the resulting syntax object." - :examples [{:code "(syntax 'bar)"} - {:code "(syntax 'bar {:some :metadata})"}] - :signature [{:params [value] - :return Syntax} - {:params [value meta] - :return Syntax}]}} - - - syntax? - {:doc {:description "Returns true if the argument is a syntax object." - :examples [{:code "(syntax? form)"}] - :signature [{:params [x] - :return Boolean}]}} - - - tailcall* - {:doc {:description "Like `tailcall` but lower-level. Instead of a form, takes a function and then arguments separately." - :examples [{:code "(tailcall* + 1 2 3)"}] - :signature [{:params [f & args] }]}} - - - transfer - {:doc {:description "Transfers the specified amount of coins to the target `address`. Returns the amount transferred if successful." - :errors {:FUNDS "If there is insufficient balance in the sender's account." - :STATE "If the receiver is an actor that is unable to accept funds."} - :examples [{:code "(transfer #42 12345678)"}] - :signature [{:params [address amount] - :return Long}]}} - - - transfer-memory - {:doc {:description "Transfers the specified amount of memory allowance to the target `address`. Returns the amount transferred if successful." - :errors {:MEMORY "If there is insufficient balance in the sender's account."} - :examples [{:code "(transfer-memory my-friend-address 100)"}] - :signature [{:params [address amount] - :return Long}]}} - - - undef* - {:doc {:description "Undefines a symbol, removing the mapping from the current environment if it exists. Helper function for the 'undef' macro." - :examples [{:code "(undef* 'a)"}] - :signature [{:params [sym]}]}} - - - union - {:doc {:description "Computes the union of zero or more sets. Nil is treated as the empty set." - :examples [{:code "(union #{1 2} #{2 3})"}] - :signature [{:params [& sets] - :return Set}]}} - - unsyntax - {:doc {:description "Unwraps a value from a syntax object. If the argument is not a syntax object, returns it unchanged." - :examples [{:code "(unsyntax form)"}] - :signature [{:params [form]}]}} - - - values - {:doc {:description "Gets the values from a map. Also see `keys`." - :examples [{:code "(values {1 2 3 4})"}] - :signature [{:params [map] - :return Vector}]}} - - vec - {:doc {:description "Coerces the argument to a vector. Arguement must be coercible to a sequential data structure." - :examples [{:code "(vec #{1 2 3 4})"}] - :signature [{:params [coll] - :return Vector}]}} - - vector - {:doc {:description "Creates a vector with the given elements." - :examples [{:code "(vector 1 2 3)"}] - :signature [{:params [& elements] - :return Vector}]}} - - vector? - {:doc {:description "Returns true if the argument is a vector, false otherwise." - :examples [{:code "(vector? [1 2 3])"}] - :signature [{:params [x] - :return Boolean}]}} - - zero? - {:doc {:description "Returns true if the argument has the numeric value zero, false otherwise." - :examples [{:code "(zero? 0.1)"}] - :signature [{:params [x] - :return Boolean}]}} - -} diff --git a/convex-core/src/main/cvx/convex/fungible.cvx b/convex-core/src/main/cvx/convex/fungible.cvx deleted file mode 100644 index 7d98a565e..000000000 --- a/convex-core/src/main/cvx/convex/fungible.cvx +++ /dev/null @@ -1,380 +0,0 @@ -'convex.fungible - - -(call *registry* - (register {:description ["Provides library functions for building and managing standard fungible assets." - "Quantity is expressed as a long representing the amount of an asset." - "The `build-token` function creates deployable code that follows the interface described in `convex.asset`."] - :name "Fungible token creation and management"})) - - -;;;;;;;;;; Building actors - - -(defn add-mint - - ^{:doc {:description ["Creates deployable code that, when added to actor code from `build-token`, allows priviledged accounts to mint and burn tokens." - "Configuration map contains:" - "- `:max-supply`, a long designating the maximum mintable supply (optional, defaults to 1000000000000000000, the allowed maximum)" - "- `:minter`, a single address or a Trust Monitor from `convex.trust` (mandatory)"] - :examples [{:code "(deploy [(build-token {}) (add-mint {:minter *address* :max-supply 1000000000})])"}] - :signature [{:params [config]}]}} - - [config] - - (let [max-supply (long (or (:max-supply config) - 1000000000000000000)) - minter (address (or (:minter config) - *address*))] - (assert (<= 0 - max-supply - 1000000000000000000)) - `(do - (import convex.trust :as trust) - - ;; Who is allowed to mint tokens? - ;; - (def minter - ~minter) - - ;; Maximum supply (limit after minting) - ;; - (def max-supply - ~max-supply) - - - (defn burn - - ^{:callable? true} - - [amount] - - (when-not (trust/trusted? minter - *caller*) - (fail :TRUST - "No rights to burn")) - (let [amount (long amount) - bal (balance *caller*)] - ;; Burn amount must be less than or equal to caller's balance. - ;; - (assert (<= 0 - amount - bal)) - (set-holding *caller* - (- bal - amount)) - (def supply (- supply - amount)))) - - - (defn mint - - ^{:callable? true} - - [amount] - - (when-not (trust/trusted? minter - *caller*) - (fail :TRUST - "No rights to mint")) - (let [amount (long amount) - new-supply (+ supply - amount) - bal (balance *caller*) - new-bal (+ bal - amount)] - ;; Mint amount. - ;; - (assert (<= 0 - new-bal - max-supply)) - ;; New supply must be in valid range. - ;; - (assert (<= 0 - new-supply - max-supply)) - (set-holding *caller* - new-bal) - (def supply - new-supply)))))) - - - -(defn build-token - - ^{:doc {:description ["Creates deployable code for a new fungible token which follows the interface described in `convex.asset`." - "An optional config map can be provided:" - "- `:initial-holder`, address which will hold the initial supply (defaults to `*address*`)" - "- `:supply`, supply created and attributed to `:initial-holder` (long, defaults to 1000000)"] - :examples [{:code "(deploy (build-token {:supply 1000000 :initial-holder *address*}))"}] - :signature [{:params [config]}]}} - - [config] - - (let [supply (or (:supply config) - 1000000) - initial-holder (or (:initial-holder config) - *address*)] - `(do - - (def supply - ~supply) - - (set-holding ~initial-holder - ~supply) - - ;; Map of holder-address -> {offeree-address -> positive long amount} - ;; Must enforce valid positive offers - ;; - (def offers {}) - - - ;; Functions of the interface described in the `convex.asset` library - - (defn accept - - ^{:callable? true} - - [sender quantity] - - (let [sender (address sender) - quantity (if quantity - (long quantity) - 0) - om (or (get offers - sender) - 0) - sendbal (or (get-holding sender) - 0) - offer (or (get om - *caller*) - 0)] - (cond - (< quantity - 0) - (fail "Can't accept a negative quantity of fungible tokens.") - - (< offer - quantity) - (fail "Offer is insufficient") - - (< sendbal - quantity) - (fail "Sender token balance is insufficient") - - (let [new-offer (- offer - quantity)] - (def offers - (assoc offers - sender - (if (> new-offer - 0) - (assoc om - *caller* - new-offer) - (dissoc om *caller*)))) - (set-holding sender - (- sendbal - quantity)) - (set-holding *caller* - (+ (or (get-holding *caller*) - 0) - quantity)))))) - - - (defn balance - - ^{:callable? true} - - [addr] - - (or (get-holding addr) - 0)) - - - ;; No restrictions on transfer by default. - ;; - (defn check-transfer - - ^{:callable? true} - - [_sender _receiver _quantity] - - nil) - - - (defn direct-transfer - - ^{:callable? true} - - [addr amount] - - (let [addr (address addr) - amount (if amount - (long amount) - 0) - bal (or (get-holding *caller*) - 0) - tbal (or (get-holding addr) - 0)] - ;; Amount must be in valid range. - ;; - (assert (<= 0 - amount - bal)) - ;; Need this check in case of self-transfers. - (when (= *caller* - addr) - (return amount)) - (set-holding *caller* - (- bal - amount)) - (set-holding addr - (+ tbal - amount)))) - - - (defn get-offer - - ^{:callable? true} - - [sender receiver] - - (or (get-in offers - [sender - receiver]) - 0)) - - - (defn offer - - ^{:callable? true} - - [receiver quantity] - - (let [receiver (address receiver) - quantity (if quantity - (long quantity) - 0) - om (get offers - *caller*)] - (if (<= quantity - 0) - (when (get om - receiver) - (def offers - (assoc offers - *caller* - (dissoc om - receiver)))) - (def offers - (assoc-in offers - [*caller* - receiver] - quantity))) - quantity)) - - - ;; TODO. Shouldn't also implement `owns?` - - - (defn quantity-add - - ^{:callable? true} - - [a b] - - (let [a (if a - (long a) - 0) - b (if b - (long b) - 0)] - (+ a b))) - - - (defn quantity-sub - - ^{:callable? true} - - [a b] - - (let [a (if a - (long a) - 0) - b (if b - (long b) - 0)] - (if (> a b) - (- a - b) - 0))) - - - (defn quantity-subset? - - ^{:callable? true} - - [a b] - - (<= (if a - (long a) - 0) - (if b - (long b) - 0)))))) - - -;;;;;;;;;; API for handling actors - - -(defn balance - - ^{:doc {:description "Gets the balance from a fungible token. Checks the balance for the specified holder, or the current *address* if not specified." - :examples [{:code "(balance my-token-address)"}] - :signature [{:params [token holder]}]}} - - [token holder] - - (call token - (balance holder))) - - - -(defn burn - - ^{:doc {:description "Burns an amount of tokens for the given token. User must have minting privileges. Amount must be non-negative and no greater than the caller's balance." - :examples [{:code "(mint my-token-address 1000)"}] - :signature [{:params [token amount]}]}} - - [token amount] - - (call token - (burn amount))) - - - -(defn mint - - ^{:doc {:description "Mints an amount of tokens for the given token. User must have minting privileges. Amount may be negative to burn fungible tokens." - :examples [{:code "(mint my-token-address 1000)"}] - :signature [{:params [token amount]}]}} - - [token amount] - - (call token - (mint amount))) - - - -(defn transfer - - ^{:doc {:description "Transfers balance of a fungible token." - :examples [{:code "(transfer my-token-address my-friend 100)"}] - :signature [{:params [token target amount]}]}} - - [token target amount] - - (call token - (direct-transfer target - amount))) diff --git a/convex-core/src/main/cvx/convex/play.cvx b/convex-core/src/main/cvx/convex/play.cvx deleted file mode 100644 index 9afc2e9b9..000000000 --- a/convex-core/src/main/cvx/convex/play.cvx +++ /dev/null @@ -1,80 +0,0 @@ -'convex.play - - -(call *registry* - (register {:description ["Provides a playable environment, akin to a text game, where transactions are actually interpreted by an actor." - "Based on `*lang*`." - "User can always send a transaction `stop` to stop the playable environment and resume normal transaction processing." - "Actor must define callable functions:" - "- `(command trx)`, takes a transaction and do any arbitrary operation, returns a displayable message or nil to stop" - "- `(start)`, for initializing the environment"] - :name "Playable environment library"})) - - -;; -;; -;; The intention of this library is to provide tools to safely delegate command execution to an actor -;; which can intrepret user input as a custom language. -;; -;; Safety measures: -;; - All actor commands executed in the actor's environment: cannot control or modify user account -;; - User can always type `stop` to stop, avoids getting locked -;; -;; - - -;;;;;;;;;; Private API - - - -(defn -stop - - ^{:private? true} - - ;; Private because unreachable: - ;; - ;; - User most likely cannot reach because transactions are interpreted by actor - ;; - Actor cannot reach because it would define `*lang*` in its environment, not user's - - [] - - (undef *lang*) - "Stop.") - - -;;;;;;;;;; Public API - - -(defn runner - - ^{:doc {:description ["Returns a function that can be set to `*lang*` and which calls the `command` function on `actor` with each transaction." - "User can always transact `stop` to remove it from `*lang*` and resume normal transaction processing."] - :examples [{:code "(runner #1234)"}] - :signature [{:params [actor]}]}} - - [actor] - - (fn [trx] - (if (= trx - 'stop) - (-stop) - (let [result (call actor - (command trx))] - (if (nil? result) - (-stop) - result))))) - - - -(defn start - - ^{:doc {:description "Prepares `*lang*` using `runner` and then calls `start` on `actor` to launch the playable environment." - :examples [{:code "(start #1234)"}] - :signature [{:params [actor]}]}} - - [actor] - - (def *lang* - (runner actor)) - (call actor - (start))) diff --git a/convex-core/src/main/cvx/convex/registry.cvx b/convex-core/src/main/cvx/convex/registry.cvx deleted file mode 100644 index 12fccf2a6..000000000 --- a/convex-core/src/main/cvx/convex/registry.cvx +++ /dev/null @@ -1,140 +0,0 @@ -'convex.registry - -(set-holding *address* - {:description ["Actor hosting a registry for resolving arbitrary symbols to addresses." - "Typically, actors and libraries are registered so that they can be retrieved and consumed using standard `import`." - "Each record in the registry has a controller that can update that record in any way." - "A controller is an address or, more speficially, a trust monitor as described in `convex.trust`." - "This actor also provides a standard way for adding metadata to an address."] - :name "Convex Name Service"}) - - -;; -;; -;; Deployed by default during network initialisation at a well-known address. -;; Initialization takes care of registering this registry alongside other actors and libraries. -;; -;; This make it accessible from early in network bootstrap as a way to register and locate Accounts. -;; -;; - - -;;;;;;;;;; Values - -(def cns-database - - ^{:private? true} - - ;; Map of `symbol` -> `[address-target address-controller]`. - - {}) - - - -(def trust - - ^{:private? true} - - ;; Address of the `convex.trust`, it is deployed right after this account, hence it is predictable. - - (address (inc (long *address*)))) - - - -;;;;;;;;;; Address metadata - - -(defn lookup - - ^{:callable? true - :doc {:description "Looks up registry metadata for a given address." - :examples [{:code "(call *registry* (lookup somebody)"}] - :signature [{:params [addr]}]}} - - [addr] - - (get-holding (address addr))) - - - -(defn register - - ^{:callable? true - :doc {:description "Registers metadata for the *caller* account. Metadata can be an arbitrary value, but by convention is a map with defined fields." - :examples [{:code "(call *registry* (register {:name \"My Name\"})"}] - :signature [{:params [metadata]}]}} - - [data] - - (set-holding *caller* - data)) - - - -(defn cns-control - - ^{:callable? true - :doc {:description "Updates a CNS name mapping to set a new controller. May only be peformed by a current controller." - :examples [{:code "(call *registry* (cns-control 'my.actor trust-monitor-address)"}] - :signature [{:params [name addr]}]}} - - [name addr] - - (let [record (get cns-database - name)] - (when (nil? record) - (fail :STATE - "CNS record does not exist")) - (when (not (trust/trusted? (second record) - *caller*)) - (fail :TRUST - "Caller is not trusted with transferring control for that CNS record")) - (def cns-database - (assoc cns-database - name - (assoc record - 1 - addr))))) - - - -(defn cns-resolve - - ^{:callable? true - :doc {:description "Resolves a name in the Convex Name Service." - :examples [{:code "(call *registry* (cns-resolve 'convex.registry)"}] - :signature [{:params [addr]}]}} - - [name] - - (assert (symbol? name)) - (when-let [record (get cns-database - name)] - (first record))) - - - -(defn cns-update - - ^{:callable? true - :doc {:description "Updates or adds a name mapping in the Convex Name Service. Only the owner of a CNS record may update the mapping for an existing name" - :examples [{:code "(call *registry* (cns-update 'my.actor addr)"}] - :signature [{:params [name addr]}]}} - - [name addr] - - (let [record (get cns-database - name)] - (when (and record - (not (trust/trusted? (second record) - *caller*))) - (fail :TRUST - "Caller is not trusted with updating the requested CNS record")) - (when-not (account addr) - (fail :NOBODY - "Can only use an existing account")) - (def cns-database - (assoc cns-database - name - [addr - *caller*])))) diff --git a/convex-core/src/main/cvx/convex/trust.cvx b/convex-core/src/main/cvx/convex/trust.cvx deleted file mode 100644 index 0e4345dd9..000000000 --- a/convex-core/src/main/cvx/convex/trust.cvx +++ /dev/null @@ -1,260 +0,0 @@ -'convex.trust - - -(call *registry* - (register {:description ["Based on the reference monitor security model." - "See comments about trusted monitors in `trusted?`." - "Provides the creation of blacklists, whitelists, and upgradable actors."] - :name "Trust monitor library"})) - - -;; -;; See: https://en.wikipedia.org/wiki/Reference_monitor -;; - - -;;;;;;;;;; Private - - -(def -self - - ^{:private? true} - - *address*) - - -;;;;;;;;;; Checking trust - - -(defn trusted? - - ^{:doc {:description ["Returns true if `subject` is trusted by `trust-monitor`, false otherwise." - "A trust monitor is an address, pointing to either:" - "- A user account that can only trust itself" - "- An actor implementing `(check-trusted? subject action object)` which returns true or false." - "`action` and `object` are arbitrary values specific to the trust monitor." - "In practice, `subject` is often an address, although this is specific to the trust monitor as well." - "See `build-blacklist` and `build-whitelist`."] - :examples [{:code "(trusted? my-blacklist *caller*)"}] - :signature [{:params [trust-monitor subject]} - {:params [trust-monitor subject action]} - {:params [trust-monitor subject action object]}]}} - - - ([trust-monitor subject action] - - (trusted? trust-monitor - subject - action - nil)) - - - ([trust-monitor subject] - - (trusted? trust-monitor - subject - nil - nil)) - - - ([trust-monitor subject action object] - - (if (actor? trust-monitor) - (query (call trust-monitor - (check-trusted? subject - action - object))) - (= (address trust-monitor) - subject)))) - - -;;;;;;;;;; Building black/white lists - - -(defn build-blacklist - - ^{:doc {:description ["Creates deployable code for a new blacklist, an actor acting as a trust monitor." - "An optional configuration map may be provided:" - "- `:blacklist`, collection of addresses forming the initial blacklist" - "- `:controller`, address that has the ability to modify the blacklist"] - :examples [{:code "(deploy (build-blacklist {:controller *address* :blacklist [my-foe-1 my-foe-2]}))"}] - :signature [{:params [config]}]}} - - [config] - - (let [blacklist (reduce (fn [w x] - (conj w - (address x))) - #{} - (or (:blacklist config) - [*address*])) - controller (address (or (:controller config) - *address*))] - `(do - (def trust - ~-self) - - (def blacklist - - ;; blacklist of addresses that are denied. - - ~blacklist) - - - (def controller - - ;; Controller address determines who can modify the blacklist. - - ~controller) - - - (defn check-trusted? - - ^{:callable? true} - - [subject action object] - - (not (contains-key? blacklist - (address subject)))) - - - (defn set-trusted - - ^{:callable? true} - - [subject allow?] - - (if (trust/trusted? controller - *caller*) - (def blacklist - ((if allow? - disj - conj) - blacklist - (address subject))) - (fail :TRUST - "No access to blacklist!")))))) - - - -(defn build-whitelist - - ^{:doc {:description ["Creates deployable code for a new whitelist, an actor acting as a trust monitor." - "An optional configuration map may be provided:" - "- `:controller`, address that has the ability to modify the whitelist" - "- `:whitelist`, collection of addresses forming the initial whitelist"] - :examples [{:code "(deploy (build-whitelist {:controller *address* :whitelist [*address*]}))"}] - :signature [{:params [config]}]}} - - [config] - - (let [whitelist (reduce (fn [w x] - (conj w - (address x))) - #{} - (or (:whitelist config) - [*address*])) - controller (or (:controller config) - *address*)] - `(do - (def trust - ~-self) - - - (def whitelist - - ;; A whitelist of addresses that are accepted. - - ~whitelist) - - - (def controller - - ;; Controller address determines who can modify the whitelist. - - ~(address controller)) - - - (defn check-trusted? - - ^{:callable? true} - - [subject action object] - - (contains-key? whitelist - (address subject))) - - - (defn set-trusted - - ^{:callable? true} - - [subject allow?] - - (if (trust/trusted? controller - *caller*) - (def whitelist - ((if allow? conj disj) - whitelist - (address subject))) - (fail :TRUST - "No access to whitelist!")))))) - - -;;;;;;;;;; Upgradable actors - - -(defn add-trusted-upgrade - - ;; TODO. Improve docstring, `:root` is a blacklist or a whitelist. - - ^{:doc {:description ["Creates deployable code for an upgradable actor where any arbitrary code can be executed." - "An optional configuration map may be provided:" - "- `:root`, address that can execute arbitrary code in the actor (defaults to `*address*`)" - "Meant to be used wisely."] - :examples [{:code "(deploy (add-trusted-upgrade {:root *address*}))"}] - :signature [{:params [config]}]}} - - [config] - - (let [root (or (:root config) - *address*)] - `(do - (def trust - ~-self) - - - (def upgradable-root - ~root) - - - (defn upgrade - - ^{:callable? true} - - [code] - - (if (trust/trusted? upgradable-root - *caller*) - (eval code) - (fail :TRUST - "No root access to upgrade capability!")))))) - - - -(defn remove-upgradability! - - ^{:doc {:description ["Removes upgradability from an actor, previously added using `add-trusted-upgrade`." - "Cannot be undone, meant to be used wisely after considering all implications."] - :examples [{:code "(remove-upgradability! upgradable-actor)"}] - :signature [{:params [config]}]}} - - [actor] - - (call actor - (upgrade - '(do - ;; Undefine things used for upgradability - (undef upgrade) - (undef upgradable-root)))) - nil) diff --git a/convex-core/src/main/cvx/convex/trusted-oracle.cvx b/convex-core/src/main/cvx/convex/trusted-oracle.cvx deleted file mode 100644 index 6a5e0ff49..000000000 --- a/convex-core/src/main/cvx/convex/trusted-oracle.cvx +++ /dev/null @@ -1,141 +0,0 @@ -'convex.trusted-oracle - - -(call *registry* - (register {:description ["API for simple oracle actors that depend on a trusted set of addresses who may provide results." - "Default actor used is `convex.trusted-oracle.actor`." - "At first, a key (any arbitrary value) is registered via `register`." - "When ready, a result for that key is provided via `provide` by a trusted account." - "Consumers can fetch data about keys using `data`, check if a result is provided using `finalized?`, and read results using `read`." - "Implementating a simple oracle actor requires defining callable function versions of:" - "- `(data key)`" - "- `(finalized? key)`" - "- `(read key)`" - "- `(register key result)`" - "- `(provide key result)`"] - :name "Trusted oracle API"})) - - -(import convex.trusted-oracle.actor :as default-actor) - - -;;;;;;;;;; API - - -(defn data - - ^{:doc {:description "Returns data registered for `key`." - :examples [{:code "(data :foo)"}] - :signature [{:params [key]} - {:params [actor key]}]}} - - - ([key] - - (data default-actor - key)) - - - ([actor key] - - (call actor - (data key)))) - - - -(defn finalized? - - ^{:callable? true - :doc {:description "Returns a boolean indicating if a results has been provided for `key`." - :examples [{:code "(finalized? :foo)"}] - :signature [{:params [key]} - {:params [actor key]}]}} - - - ([key] - - (finalized? default-actor - key)) - - - ([actor key] - - (call actor - (finalized? key)))) - - - -(defn read - - ^{:callable? true - :doc {:description "Returns the result for `key`." - :examples [{:code "(read :foo)"}] - :signature [{:params [key]} - {:params [actor key]}]}} - - - ([key] - - (read default-actor - key)) - - - ([actor key] - - (call actor - (read key)))) - - - -(defn register - - ^{:callable? true - :doc {:description ["Callable function for registering a new oracle key." - "Returns true if successful, false if key already exists." - "Data should be a map containg at least `:trust`, a set of addresses trusted for using `provide` on that key." - "Without `:trust`, a result cannot be delivered."] - :examples [{:code "(register :foo {:trust #{*address*}})"}] - :signature [{:params [key data]} - {:params [actor key data]}]}} - - - ([key data] - - (register default-actor - key - data)) - - - ([actor key data] - - (call actor - (register key - data)))) - - - -(defn provide - - ^{:callable? true - :doc {:description ["Provides a result for a key registered using `register`." - "Does not change anything if a resulted has already been provided for that key." - "Returns the result associated with that key."] - :errors {:STATE "When key does not exist" - :TRUST "When caller is untrusted"} - :examples [{:code "(provide :foo 42)"}] - :signature [{:params [key result]} - {:params [actor key result]}]}} - - - ([key result] - - (provide default-actor - key - result)) - - - ([actor key result] - - (call actor - (provide key - result)))) diff --git a/convex-core/src/main/cvx/convex/trusted-oracle/actor.cvx b/convex-core/src/main/cvx/convex/trusted-oracle/actor.cvx deleted file mode 100644 index 9f452bb44..000000000 --- a/convex-core/src/main/cvx/convex/trusted-oracle/actor.cvx +++ /dev/null @@ -1,107 +0,0 @@ -'convex.trusted-oracle.actor - - -(call *registry* - (register {:description ["Default actor used by `convex.trusted-oracle`." - "Go there for more information about this implementation."] - :name "Trusted oracle actor implementation"})) - - -;;;;;;;;;; Values - - -(def *list* - - ;; Map of `key` -> `arbitrary map describing an oracle`. - - {}) - -(def *results* - - ;; Map of `key` -> `result`. - - {}) - - -;;;;;;;;;; Callable functions - - -(defn data - - ^{:callable? true} - - [key] - - (*list* key)) - - - -(defn finalized? - - ^{:callable? true} - - [key] - - (contains-key? *results* - key)) - - - -(defn read - - ^{:callable? true} - - [key] - - (*results* key)) - - - -(defn register - - ^{:callable? true} - - [key data] - - (if (contains-key? *list* - key) - false - (do - (def *list* - (assoc *list* - key - data)) - true))) - - - -(defn provide - - ^{:callable? true} - - [key value] - - (cond - (not (*list* key)) - (fail :STATE - (str "Unknown oracle key: " - key)) - - (not (get-in *list* - [key - :trust - *caller*])) - (fail :TRUST - "Untrusted caller") - - (contains-key? *results* - key) - (*results* key) - - :else - (do - (def *results* - (assoc *results* - key - value)) - value))) diff --git a/convex-core/src/main/cvx/lab/convex/xform.cvx b/convex-core/src/main/cvx/lab/convex/xform.cvx deleted file mode 100644 index 9080753d7..000000000 --- a/convex-core/src/main/cvx/lab/convex/xform.cvx +++ /dev/null @@ -1,175 +0,0 @@ -;; -;; -;; Prototype for Clojure-like transducers -;; -;; https://clojure.org/reference/transducers -;; -;; - - - -(call *registry* - (cns-update 'convex.xform - *address*)) - - - -(defn filter - - [f] - - (fn [rf] - - (fn - ([] - (rf)) - - ([result] - (rf result)) - - ([acc x] - - (if (f x) - (rf acc - x) - acc))))) - - - -(defn map - - [f] - - (fn [rf] - - (fn - ([] - (rf)) - - ([result] - (rf result)) - - ([acc x] - (rf acc - (f x))) - - ([acc x & x+] - (rf acc - (apply f - x - x+)))))) - - - -(defn transduce - - - ([xform f coll] - - (transduce xform - f - (f) - coll)) - - - ([xform f init coll] - - (let [f-2 (xform f)] - (f-2 (reduce f-2 - init - coll))))) - - - -(defn first - - ([] - - nil) - - - ([result] - - result) - - - ([_acc x] - - (reduced x))) - - - -(defn first-n - - [n] - - (fn - ([] []) - - - ([result] - - result) - - - ([acc x] - - (let [acc-2 (conj acc - x)] - (if (>= (count acc-2) - n) - (reduced acc-2) - acc-2))))) - - - -(defn last - - ([] - - nil) - - - ([result] - - result) - - - ([_acc x] - - x)) - - - -(defn last-n - - ;; TODO. Fails because of: https://github.com/Convex-Dev/convex/issues/193 - - [n] - - (fn - ([] - - [0 - (loop [acc [] - i 0] - (if (< i - n) - (recur (conj acc - nil) - (inc i)) - acc))]) - - - ([[_pointer acc]] - - acc) - - - ([[pointer acc] x] - - [(inc pointer) - (assoc acc - (rem pointer - n) - x)]))) diff --git a/convex-core/src/main/cvx/lab/messenger.cvx b/convex-core/src/main/cvx/lab/messenger.cvx deleted file mode 100644 index 197e886a9..000000000 --- a/convex-core/src/main/cvx/lab/messenger.cvx +++ /dev/null @@ -1,9 +0,0 @@ - (defn accept - - ^{:callable? true - :doc {:description "Accepts a message and offered assets." - :examples [{:code "(call messenger (accept message-id))"}] - :signature [{:params [token holder]}]}} - [message-id] - - ) diff --git a/convex-core/src/main/cvx/lab/prediction-market.cvx b/convex-core/src/main/cvx/lab/prediction-market.cvx deleted file mode 100644 index 389ced014..000000000 --- a/convex-core/src/main/cvx/lab/prediction-market.cvx +++ /dev/null @@ -1,102 +0,0 @@ -(defn build-prediction-market [oracle oracle-key outcomes] - `(do - ;; store oracle address and key in environment - (def oracle (address ~oracle)) - (def oracle-key ~oracle-key) - (def outcomes ~outcomes) - - - ;; Stakes are a map of outcome value to map of ( address ->stake) - (def stakes (into {} (map (fn [x] [x {}]) ~outcomes))) - - ;; Total stake for each outcome - (def totals (into {} (map (fn [x] [x 0]) ~outcomes))) - - ;; NOTE: total current bonded value is the balance of this contract - - ;; Bonding curve function for a given map of stakes - (defn bond - ^{:callable? true} - [stks] - (let [sxx (reduce (fn [acc [k v]] (let [x (double v)] (+ acc (* x x)))) 0.0 stks)] - (sqrt sxx))) - - ;; Adjust stake on an outcome - ;; must be called with a sufficient offer to fund any increase - ;; returns positive cost of increased stake, or negative refund of reduced stake - (defn stake - ^{:callable? true} - [outcome new-stake] - (assert (contains-key? stakes outcome) (>= new-stake 0)) - - (let [old-stake (or (get (stakes outcome) *caller*) 0) - _ (if (== old-stake new-stake) (return 0)) - nos (assoc (stakes outcome) *caller* new-stake) - new-stakes (assoc stakes outcome nos) - new-totals (assoc totals outcome (+ (totals outcome) (- new-stake old-stake))) - new-bond (long (bond new-totals)) - dval (- new-bond *balance*)] - - ;; get or refund funds. Will error if not enough provided for stake increase? - (cond - (== dval 0) nil ;; nothing to do.... can this happen? - (> dval 0) (accept dval) - (< dval 0) (transfer *caller* (- dval))) - - (def totals new-totals) - (def stakes new-stakes) - dval)) - - ;; Get the effective price for an outcome. May be NaN if no balance at all. - (defn price - ^{:callable? true} - [outcome] - (when-not (contains-key? totals outcome) (return il)) - (let [tstk (double (totals outcome)) - bal (double *balance*)] - (/ (* tstk tstk) (* bal bal)))) - - ;; Get the collection of possible outcomes - (defn get-outcomes - ^{:callable? true} - [data] - outcomes) - - ;; final outcome - (def final-outcome nil) - (def final-flag false) - - ;; Check if contract is finalised - (defn finalized? - ^{:callable? true} - [] - (when final-flag (return true)) - (cond (call oracle (finalized? oracle-key)) :OK (return false)) - - (let [result (call oracle (read oracle-key))] - (def final-outcome result) - (def final-flag true) - true)) - - ;; call to refund all stakes, not for external use - ;; called in event of a unanticipated outsome - (def refund [] - ;; TODO - ) - - - ;; call to claim payout - ;; returns amount paid out, or null if not yet finalised - (defn payout - ^{:callable? true} - [] - (if (finalized?) :OK (return nil)) - (when-not (contains-key? totals final-outcome) (return (refund))) - (let [total (double (totals final-outcome)) ;; total stake on winning outcome - fo-stakes (stakes final-outcome {}) - stk (double (or (fo-stakes *caller*) 0.0)) ;; caller's stake on final outcome - quota (long (* *balance* (/ stk total)))] - (def stakes (assoc stakes final-outcome (dissoc fo-stakes *caller*))) - (transfer *caller* quota) - quota)) - )) diff --git a/convex-core/src/main/cvx/lab/secured-loan.con b/convex-core/src/main/cvx/lab/secured-loan.con deleted file mode 100644 index e5d2f6f7a..000000000 --- a/convex-core/src/main/cvx/lab/secured-loan.con +++ /dev/null @@ -1,18 +0,0 @@ -;; Smart contract implementing a secured loan -;; -;; Concept: -;; 1. Issuer creates secured-loan contract in :initial state -;; 2. Issuer places collateral assets (tokens, etc.) in account under control of secured-loan contract -;; 3. Issuer switches state to :offered -;; 4. Lender may provide the loan, in which case state changes to :accepted -;; 5. Once accepted, issuer may withdraw borrowed funds but cannot touch collateral -;; 6. Issuer must ensure principal plus interest is repaid before the maturity date -;; 7. At maturity, either: -;; a) Loan is repaid, and issuer regains control of collateral assets -;; b) Loan defaults, and lender gains control of collateral assets - -(do - - - - ) \ No newline at end of file diff --git a/convex-core/src/main/cvx/torus/currencies.cvx b/convex-core/src/main/cvx/torus/currencies.cvx deleted file mode 100644 index 6562d0d34..000000000 --- a/convex-core/src/main/cvx/torus/currencies.cvx +++ /dev/null @@ -1,12 +0,0 @@ -[ -["USD" "US Dollar" "US National Currency" "$" 1000000000 2 1.00] -["JPY" "Japanese Yen" "Japanese National Currency" "¥" 1000000000 0 0.0092] -["EUR" "Euro" "European Union Currency" "€" 1000000000 2 1.18819] -["GBP" "Pound Sterling" "UK National Currency" "£" 1000000000 2 1.38700] -["THB" "Thai Baht" "Thai National Currency" "฿" 1000000000 2 0.03244] -["VND" "Vietnamese Dong" "Vietnamese National Currency" "₫" 1000000000 2 0.00004] -["MYR" "Malaysian Ringgit" "Malaysian National Currency" "RM" 1000000000 2 0.24260] -["CHF" "Swiss Franc" "Swiss National Currency" "Fr." 1000000000 2 1.07269] -["HKD" "Hong Kong Dollar" "Hong Kong Currency" "HK$" 1000000000 2 0.12879] -["SGD" "Singapore Dollar" "Singapore National Currency" "S$" 1000000000 2 0.751] -] \ No newline at end of file diff --git a/convex-core/src/main/cvx/torus/exchange.cvx b/convex-core/src/main/cvx/torus/exchange.cvx deleted file mode 100644 index 26d4b20d0..000000000 --- a/convex-core/src/main/cvx/torus/exchange.cvx +++ /dev/null @@ -1,632 +0,0 @@ -'torus.exchange - -(call *registry* - (register {:description ["Torus establishes automated market makers for fungible assets (see `convex.fungible`)." - "It creates singleton CVX/Token trading pairs for each fungible asset."] - :name "Torus exchange"})) - - -;; TODO. Add docstrings to library functions. - - -;;;;;;;;;; Imports - - -(import convex.asset :as asset) -(import convex.fungible :as fungible) -(import convex.trust :as trust) - - -;;;;;;;;;; Values - - -(def markets - - ^{:private? true} - - ;; Blob-map of `token address` -> `market actor address`. - - (blob-map)) - - -;;;;;;;;;; API - Creating markets - - -(defn build-market - - ^{:doc {:description "Creates deployable code for a new Torus token market." - :examples [{:code "(deploy (build-market {:token token-address}))"}] - :signature [{:params [token torus-addr]}]}} - - ;; Deployable code is `[fungible-token-code market-code]`. - - [token torus] - - [(fungible/build-token {:supply 0}) - `(do - (import convex.asset :as asset) - (import convex.core :as core) - (import convex.fungible :as fungible) - - - (def token ~token) - (def torus ~torus) - (def token-balance - 0) - - - (defn add-liquidity - - ^{:callable? true} - - [amount] - - (let [;; Amount of tokens deposited. - amount (long amount) - ;; Price of token in CVX (double), nil if no current liquidity. - price (price) - initial-cvx-balance *balance* - ;; Amount of CVX required (all if initial deposit). - cvx (core/accept (if price - (long (* price - amount)) - *offer*)) - ;; Ensures tokens are transferred from caller to market actor. - _ (asset/accept *caller* - [token - amount]) - ;; Compute new total balances for actor/ - new-token-balance (+ token-balance - amount) - ;; Compute number of new shares for depositor = increase in liquidity (%) * current total shares. - ;; If no current liquidity just initialise with the geometric mean of amounts deposited. - delta (if (> supply - 0) - (let [;; Initial size of liquidity pool (geometric mean). - liquidity (sqrt (* (double initial-cvx-balance) - token-balance)) - new-liquidity (sqrt (* (double *balance*) - new-token-balance))] - (long (* (- new-liquidity - liquidity) - (/ supply - liquidity)))) - (long (sqrt (* (double amount) - cvx))))] - ;; Perform updates to reflect new holdings of liquidity pool shares and total token balance (all longs) - (set-holding *caller* - (+ delta - (or (get-holding *caller*) - 0))) - (def supply (+ supply - delta)) - (def token-balance - new-token-balance) - delta)) - - - (defn buy-cvx - - ^{:callable? true} - - [amount] - - (let [amount (long amount) - required-tokens (or (buy-cvx-quote amount) - (fail :FUNDS - "Pool cannot supply this amount of CVX"))] - (asset/accept *caller* - [token - required-tokens]) - (def token-balance - (+ token-balance - required-tokens)) - ;; Must be done last. - ;; - (core/transfer *caller* - amount) - required-tokens)) - - - (defn buy-cvx-quote - - ^{:callable? true} - - [amount] - - ;; Security: check pool can provide. - ;; - (when-not (< 0 - amount - *balance*) - (return nil)) - (let [;; Computes pool and fees/ - cvx-balance *balance* - pool (* (double token-balance) - cvx-balance) - rate (calc-rate)] - ;; Computes required payment in tokens. - (long (ceil (* (+ 1.0 - rate) - (- (/ pool - (- cvx-balance - amount)) - token-balance)))))) - - - (defn buy-tokens - - ^{:callable? true} - - [amount] - - (let [amount (long amount) - required-cvx (or (buy-tokens-quote amount) - (fail "Pool cannot supply this amount of tokens"))] - (core/accept required-cvx) - (def token-balance - (- token-balance - amount)) - ;; Must be done last. - ;; - (fungible/transfer token - *caller* - amount) - required-cvx)) - - - (defn buy-tokens-quote - - ^{:callable? true} - - [amount] - - ;; Security: check pool can provide. - ;; - (when-not (< 0 - amount - token-balance) - (return nil)) - (let [;; Computes pool and fees. - cvx-balance *balance* - pool (* (double token-balance) - cvx-balance) - rate (calc-rate)] - ;; Computes required payment in CVX. - (long (ceil (* (+ 1.0 - rate) - (- (/ pool - (- token-balance - amount)) - cvx-balance)))))) - - - (defn calc-rate - - ;; TODO. Have variable rate set by torus and/or trade velocity. - ;; Maybe BASE_FEE / 1 + (THROUGHPUT / LIQUIDITY) ? - - [] - - 0.001) - - - (defn price - - ^{:callable? true} - - ;; Price is CVX amount per token, or nil if there are no tokens in liquidity pool. - - [] - - (when (> token-balance - 0) - (/ (double *balance*) - token-balance))) - - - (defn sell-cvx - - ^{:callable? true} - - [amount] - - (let [amount (long amount) - gained-tokens (or (sell-cvx-quote amount) - (fail "Cannot sell this amount into pool"))] - (core/accept amount) - (def token-balance - (- token-balance - gained-tokens)) - ;; Must be done last. - ;; - (asset/transfer *caller* - [token - gained-tokens]) - gained-tokens)) - - - (defn sell-cvx-quote - - ^{:callable? true} - - [amount] - - ;; Security: check amount is positive. - ;; - (when (< amount - 0) - (return nil)) - (let [;; Computes pool and fees. - cvx-balance *balance* - pool (* (double token-balance) - cvx-balance) - rate (calc-rate) - new-cvx-balance (+ cvx-balance - amount)] - ;; Computes gained Tokens coins from sale. - (long (/ (- token-balance - (/ pool - new-cvx-balance)) - (+ 1.0 - rate))))) - - - (defn sell-tokens - - ^{:callable? true} - - [amount] - - (let [amount (long amount) - gained-cvx (or (sell-tokens-quote amount) - (fail "Cannot sell this amount into pool"))] - (asset/accept *caller* - [token - amount]) - (def token-balance - (+ token-balance - amount)) - ;; Must be done last. - ;; - (core/transfer *caller* - gained-cvx) - gained-cvx)) - - - (defn sell-tokens-quote - - ^{:callable? true} - - [amount] - - ;; Security: check amount is positive. - ;; - (when-not (< 0 - amount) - (return nil)) - (let [;; Computes pool and fees. - cvx-balance *balance* - pool (* (double token-balance) - cvx-balance) - rate (calc-rate) - new-token-balance (+ token-balance - amount)] - ;; Computes gained Convex coins from sale. - (long (/ (- cvx-balance - (/ pool - new-token-balance)) - (+ 1.0 - rate))))) - - - (defn withdraw-liquidity - - ^{:callable? true} - - [shares] - - (let [;; Amount of shares to withdraw. - shares (long shares) - ;; Shares of holder. - own-holding (or (get-holding *caller*) - 0) - _ (assert (<= 0 - shares - own-holding)) - proportion (if (> supply - 0) - (/ (double shares) - supply) - 0.0) - coin-refund (long (* proportion - *balance*)) - token-refund (long (* proportion - token-balance))] - ;; SECURITY: - ;; 1. Update balances then transfer coins first. Risk of re-entrancy attack if transfers are made while - ;; this actor is in an inconsistent state so we MUST do accounting first. - (def token-balance - (- token-balance - token-refund)) - (set-holding *caller* - (- own-holding - shares)) - (def supply - (- supply - shares)) - ;; 2. Transfer back coins. Be aware caller might do *anything* in transfer callbacks! - (transfer *caller* - coin-refund) - ;; 3. Finally transfer asset. We've accounted this already, so safe - ;; TODO. Decide which of these is best - ;;(asset/transfer *caller* [token token-refund] :withdraw) - (fungible/transfer token - *caller* - token-refund) - shares)))]) - - - -(defn create-market - - ^{:callable? true - :doc {:description "Gets or creates the canonical market for a token." - :examples [{:code "(deploy (build-market {:token token-address}))"}] - :signature [{:params [config]}]}} - - [token] - - (when-not (= *address* - ~*address*) - (return (call ~*address* - (create-market token)))) - (assert (address? token)) - (let [existing-market (get markets - token)] - (or existing-market - (let [market (deploy (build-market token - *address*))] - (def markets - (assoc markets - token - market)) - market)))) - - - -(defn get-market - - ^{:doc {:description "Gets the canonical market for a token. Returns nil if the market does not exist." - :examples [{:code "(deploy-once (build-market {:token token-address}))"}] - :signature [{:params [token]}]}} - - [token] - - (get markets - token)) - - -;;;;;;;;;; API - Handling markets - - -(defn add-liquidity - - ^{:doc {:description nil - :signature [{:params [token token-amount cvx-amount]}]}} - - [token token-amount cvx-amount] - - (let [market (create-market token)] - (asset/offer market - [token - token-amount]) - (call market - (long cvx-amount) - (add-liquidity token-amount)))) - - - -(defn buy - - ^{:doc {:description nil - :signature [{:params [of-token amount with-token]}]}} - - [of-token amount with-token] - - (let [market (or (get-market of-token) - (fail (str "Torus: market does not exist for token: " - of-token))) - cvx-amount (or (call market - (buy-tokens-quote amount)) - (fail :LIQUIDITY - "No liquidity available to buy token")) - sold (buy-cvx with-token - cvx-amount)] - (buy-tokens of-token - amount) - sold)) - - - -(defn buy-cvx - - ^{:doc {:description nil - :signature [{:params [token amount]}]}} - - [token amount] - - (let [market (or (get-market token) - (fail :LIQUIDITY - (str "Torus: market does not exist for token: " - token)))] - ;; Note we can offer all tokens, market will accept what it needs to complete order. - (asset/offer market - [token - (fungible/balance token - *address*)]) - (call market - *balance* - (buy-cvx amount)))) - - - -(defn buy-tokens - - ^{:doc {:description nil - :signature [{:params [token amount]}]}} - - [token amount] - - (let [market (or (get-market token) - (fail :LIQUIDITY - (str "Torus: market does not exist for token: " - token)))] - ;; Note we can offer all CVX - (call market - *balance* - (buy-tokens amount)))) - - - -(defn buy-quote - - ^{:doc {:description nil - :signature [{:params [of-token amount]} - {:params [of-token amount with-token]}]}} - - - ([of-token amount] - - (when-let [market (get-market of-token)] - (call market (buy-tokens-quote amount)))) - - - ([of-token amount with-token] - - (when-let [market (get-market with-token)] - (when-let [cvx-amount (buy-quote of-token amount)] - (call market - (buy-cvx-quote cvx-amount)))))) - - - -(defn price - - ^{:doc {:description "Gets the current price for a token, in CVX or an optional given currency. Returns nil if a market with liquidity does not exist." - :examples [{:code "(price USD)"} - {:code "(price GBP USD)"}] - :signature [{:params [token]} - {:params [token currency]}]}} - - - ([token] - - (when-let [market (get-market token)] - (call market - (price)))) - - - ([token currency] - - (let [market.token (or (get-market token) - (return nil)) - price.cvx (or (call market.token - (price)) - (return nil)) - market.currency (or (get-market currency) - (return nil)) - price.currency (or (call market.currency - (price)) - (return nil))] - (/ price.cvx - price.currency)))) - - - -(defn sell - - ^{:doc {:description nil - :signature [{:params [of-token amount with-token]}]}} - - [of-token amount with-token] - - (let [cvx-amount (sell-tokens of-token - amount)] - (sell-cvx with-token - cvx-amount))) - - - -(defn sell-cvx - - ^{:doc {:description nil - :signature [{:params [token amount]}]}} - - [token amount] - - (let [market (or (get-market token) - (fail :LIQUIDITY - (str "Torus: market does not exist for token: " - token)))] - ;; Offer the amount of CVX being sold. - (call market - amount - (sell-cvx amount)))) - - - -(defn sell-quote - - ^{:doc {:description nil - :signature [{:params [of-token amount]} - {:params [of-token amount with-token]}]}} - - ([of-token amount] - - (when-let [market (get-market of-token)] - (call market - (sell-tokens-quote amount)))) - - - ([of-token amount with-token] - - (when-let [market (get-market with-token)] - (when-let [cvx-amount (sell-quote of-token - amount)] - (call market - (sell-cvx-quote cvx-amount)))))) - - - -(defn sell-tokens - - ^{:doc {:description nil - :signature [{:params [token amount]}]}} - - [token amount] - - (let [market (or (get-market token) - (fail :LIQUIDITY - (str "Torus: market does not exist for token: " - token)))] - ;; Offer the amount of tokens being sold. - (asset/offer market - [token - amount]) - (call market - (sell-tokens amount)))) - - - -(defn withdraw-liquidity - - ^{:doc {:description nil - :signature [{:params [token shares]}]}} - - [token shares] - - (let [market (or (get-market token) - (fail "No market exists to withdraw liquidity"))] - (call market - (withdraw-liquidity shares)))) diff --git a/convex-core/src/main/java/convex/core/Belief.java b/convex-core/src/main/java/convex/core/Belief.java deleted file mode 100644 index ee53ff80f..000000000 --- a/convex-core/src/main/java/convex/core/Belief.java +++ /dev/null @@ -1,723 +0,0 @@ -package convex.core; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.function.Function; - -import convex.core.crypto.AKeyPair; -import convex.core.data.ABlob; -import convex.core.data.ACell; -import convex.core.data.AMap; -import convex.core.data.ARecord; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.BlobMap; -import convex.core.data.BlobMaps; -import convex.core.data.Format; -import convex.core.data.Hash; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.MapEntry; -import convex.core.data.PeerStatus; -import convex.core.data.SignedData; -import convex.core.data.Tag; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.BadSignatureException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.impl.RecordFormat; -import convex.core.util.Counters; -import convex.core.util.Utils; - -/** - * Class representing a Peer's view of the overall network consensus state. - * - * Belief is immutable, and is designed to be independent of any particular Peer - * so that it can be efficiently merged towards consensus. - * - * Belief can be merged with other Beliefs from the perspective of a Peer. This - * property is fundamental to the Convex consensus algorithm. - * - * "Sorry to be a wet blanket. Writing a description for this thing for general - * audiences is bloody hard. There's nothing to relate it to." – Satoshi - * Nakamoto - */ -public class Belief extends ARecord { - private static final RecordFormat BELIEF_KEYS = RecordFormat.of(Keywords.ORDERS, Keywords.TIMESTAMP); - - /** - * The latest view of signed Orders held by other Peers - */ - private final BlobMap> orders; - - /** - * The timestamp at which this belief was created - */ - private final long timestamp; - - // private final long timeStamp; - - private Belief(BlobMap> orders, long timestamp) { - super(BELIEF_KEYS); - this.orders = orders; - this.timestamp = timestamp; - } - - @Override - public ACell get(ACell k) { - if (Keywords.ORDERS.equals(k)) return orders; - if (Keywords.TIMESTAMP.equals(k)) return CVMLong.create(timestamp); - return null; - } - - @SuppressWarnings("unchecked") - @Override - protected Belief updateAll(ACell[] newVals) { - BlobMap> newOrders = (BlobMap>) newVals[0]; - long newTimestamp = ((CVMLong) newVals[1]).longValue(); - if ((this.orders == newOrders)&&(this.timestamp==newTimestamp)) { - return this; - } - return new Belief(newOrders, newTimestamp); - } - - /** - * Gets an empty Belief - * @return Empty Belief - */ - public static Belief initial() { - return create(BlobMaps.empty()); - } - - /** - * Create a Belief with a single order signed by the given key pair, using initial timestamp. - * @param kp Peer Key pair with which to sign the order. - * @param order Order of blocks that the Peer is proposing - * @return new Belief representing the isolated belief of a single Peer. - */ - public static Belief create(AKeyPair kp, Order order) { - BlobMap> orders=BlobMap.of(kp.getAccountKey(),kp.signData(order)); - return create(orders); - } - - - private static Belief create(BlobMap> orders, long timestamp) { - return new Belief(orders, timestamp); - } - - private static Belief create(BlobMap> orders) { - return create(orders, Constants.INITIAL_TIMESTAMP); - } - - /** - * Create a Belief with a single empty order. USeful for Peer startup. - * - * @param kp Keypair for Peer - * @return New Belief - */ - public static Belief createSingleOrder(AKeyPair kp) { - AccountKey address = kp.getAccountKey(); - SignedData order = kp.signData(Order.create()); - return create(BlobMap.of(address, order)); - } - - /** - * The Belief merge function - * - * @param mc MergeContext for Belief Merge - * @param beliefs An array of Beliefs. May contain nulls, which will be ignored. - * @return The updated merged belief, or the same Belief if there is no change. - * @throws BadSignatureException In case of a bad signature - * @throws InvalidDataException In case of invalid data - */ - public Belief merge(MergeContext mc, Belief... beliefs) throws BadSignatureException, InvalidDataException { - Belief newBelief = mergeOnce(mc, beliefs); - - // May repeat belief update until stable, this handles the case when the Peer's - // own voting stake is sufficient to change proposed / actual consensus - // if we updated the Belief, then do a quick update again. - // this may be needed to stabilise state in the case that this peer's update - // changes the consensus - if (this != newBelief) { - newBelief = newBelief.mergeOnce(mc); - } - return newBelief; - } - - /** - * Merges Beliefs one time. This may need to be repeated. - * - * @param mc MergeContext for Belief Merge - * @param beliefs An array of Beliefs. May contain nulls, which will be ignored. - * @return The updated merged belief, or the same Belief if there is no change. - * @throws BadSignatureException In case of a bad signature - * @throws InvalidDataException In case of invalid data - */ - Belief mergeOnce(MergeContext mc, Belief... beliefs) throws BadSignatureException, InvalidDataException { - - Counters.beliefMerge++; - - // accumulate combined list of latest chains for all peers - final BlobMap> accOrders = accumulateOrders(mc, beliefs); - - // vote for new proposed chain - final BlobMap> resultOrders = vote(mc, accOrders); - if (resultOrders == null) return this; - - // update my belief with the resulting Orders - long newTimestamp = mc.getTimeStamp(); - if ((orders == resultOrders) && (timestamp == newTimestamp)) return this; - final Belief result = new Belief(resultOrders, newTimestamp); - - return result; - } - - private BlobMap> accumulateOrders(MergeContext mc, - Belief[] beliefs) { - // Initialise result with existing Orders from this Belief - BlobMap> result = this.orders; - - // assemble the latest list of orders from all peers - for (Belief belief : beliefs) { - if (belief == null) continue; // ignore null beliefs, might happen if invalidated - if (belief.equals(this)) continue; // ignore an identical belief. Nothing to update. - BlobMap> bOrders = belief.orders; - - long bcount=bOrders.count(); - for (long i=0; i> be=bOrders.entryAt(i); - ABlob key=be.getKey(); - - // Skip merging own Key. We should always have our own latest Order - if(key.equalsBytes(mc.getAccountKey())) continue; - - SignedData a=result.get(key); - if (a == null) {result=result.assocEntry(be); continue;} - SignedData b=be.getValue(); - if (b == null) continue; - - // Check signature - if (!b.checkSignature()) { - // TODO: Better handling than just ignoring, e.g. slashing? - continue; - }; - - if (a.equals(b)) continue; // PERF: fast path for no changes - - Order ac = a.getValue(); - Order bc = b.getValue(); - - // TODO: penalise inconsistency? - // TODO: check for forks / inconsistent values? - // TODO: check logic? - - // prefer advanced consensus first! - if (bc.getConsensusPoint() > ac.getConsensusPoint()) {result=result.assocEntry(be); continue;}; - - // prefer longer orders, must be later? - if (bc.getBlockCount() > ac.getBlockCount()) {result=result.assocEntry(be); continue;}; - - // prefer advanced proposals - if (bc.getProposalPoint() > ac.getProposalPoint()) {result=result.assocEntry(be); continue;}; - - // keep current view (more stable?) - } - } - return result; - } - - /** - * Conducts a stake-weighted vote across a map of consistent chains, in the - * given merge context - * - * @param accOrders Accumulated map for latest Orders received from all Peer Beliefs - * @param mc Merge context - * @param filteredChains - * @return - * @throws BadSignatureException @ - */ - private BlobMap> vote(final MergeContext mc, final BlobMap> accOrders) - throws BadSignatureException { - AccountKey myAddress = mc.getAccountKey(); - - // get current Order for this peer. - final Order myOrder = getMyOrder(mc); - assert (myOrder != null); // we should always have a Order! - - // get the Consensus state from this Peer's current perspective - // this is needed for peer weights: we only trust peers who have stake in the - // current consensus! - State votingState = mc.getConsensusState(); - - // filter chains for compatibility with current chain for inclusion in Initial Voting Set - // TODO: figure out what to do with new blocks filtered out? - final BlobMap> filteredOrders = accOrders.filterValues(signedOrder -> { - try { - Order otherOrder = signedOrder.getValue(); - return myOrder.checkConsistent(otherOrder); - } catch (Exception e) { - throw Utils.sneakyThrow(e); - } - }); - - // Current Consensus Point - long consensusPoint = myOrder.getConsensusPoint(); - - // Compute stake for all peers in consensus state - AMap peers = votingState.getPeers(); - HashMap weightedStakes = votingState.computeStakes(); - double totalStake = weightedStakes.get(null); - - // Extract unique proposed chains from provided map, computing vote for each. - // compute the total weighted vote at the same time in accumulator - // Peers with no stake should be ignored (might be old peers etc.) - HashMap stakedOrders = new HashMap<>(peers.size()); - double consideredStake = prepareStakedOrders(filteredOrders, weightedStakes, stakedOrders); - - // Get the winning chain for this peer, including new blocks encountered - AVector winningBlocks = computeWinningOrder(stakedOrders, consensusPoint, consideredStake); - if (winningBlocks == null) return null; // if no voting stake on any chain - - // winning chain should have same consensus as my initial chain - Order winningOrder = myOrder.updateBlocks(winningBlocks); - - final double P_THRESHOLD = totalStake * Constants.PROPOSAL_THRESHOLD; - final Order proposedOrder = updateProposal(winningOrder, stakedOrders, P_THRESHOLD); - - assert (proposedOrder != null); - - final double C_THRESHOLD = totalStake * Constants.CONSENSUS_THRESHOLD; - final Order consensusOrder = updateConsensus(proposedOrder, stakedOrders, C_THRESHOLD); - - BlobMap> resultOrders = filteredOrders; - if (!consensusOrder.equals(myOrder)) { - // Only sign and update Order if it has changed - final SignedData signedOrder = mc.sign(consensusOrder); - resultOrders = resultOrders.assoc(myAddress, signedOrder); - } - return resultOrders; - } - - /** - * Updates the consensus point for the winning Order, given an overall map of - * staked orders and consensus threshold. - */ - private Order updateConsensus(Order proposedOrder, HashMap stakedOrders, double THRESHOLD) { - AVector proposedBlocks = proposedOrder.getBlocks(); - ArrayList agreedChains = Utils.sortListBy(new Function() { - @Override - public Long apply(Order c) { - // scoring function scores by level of proposed agreement with proposed chain - // in order to sort by length of matched proposals - long blockMatch = proposedBlocks.commonPrefixLength(c.getBlocks()); - - long minProposal = Math.min(proposedOrder.getProposalPoint(), c.getProposalPoint()); - - long match = Math.min(blockMatch, minProposal); - if (match <= proposedOrder.getConsensusPoint()) return null; // skip if no progress vs existing - // consensus - return -match; - } - }, stakedOrders.keySet()); - int numAgreed = agreedChains.size(); - // assert(proposedChain.equals(agreedChains.get(0))); - double accumulatedStake = 0.0; - int i = 0; - for (; i < numAgreed; i++) { - Order c = agreedChains.get(i); - Double chainStake = stakedOrders.get(c); - accumulatedStake += chainStake; - if (accumulatedStake > THRESHOLD) break; - } - - if (i < numAgreed) { - // we have a consensus! - Order lastAgreed = agreedChains.get(i); - long prefixMatch = proposedOrder.getBlocks().commonPrefixLength(lastAgreed.getBlocks()); - long proposalMatch = Math.min(proposedOrder.getProposalPoint(), lastAgreed.getProposalPoint()); - long newConsensusPoint = Math.min(prefixMatch, proposalMatch); - if (newConsensusPoint < proposedOrder.getConsensusPoint()) { - throw new Error("Consensus going backwards! prefix=" + prefixMatch + " propsalmatch=" + proposalMatch); - } - return proposedOrder.withConsenusPoint(newConsensusPoint); - } else { - return proposedOrder; - } - } - - /** - * Updates the proposal point for the winning Order, given an overall map of - * staked Orders and consensus threshold. - */ - private Order updateProposal(Order winningOrder, HashMap stakedOrders, double THRESHOLD) { - AVector winningBlocks = winningOrder.getBlocks(); - - // sort all chains according to extent of agreement with winning chain - ArrayList agreedOrders = sortByAgreement(stakedOrders, winningBlocks); - int numAgreed = agreedOrders.size(); - - // accumulate stake to see how many agreed chains are required to meet proposal - // threshold - double accumulatedStake = 0.0; - int i = 0; - for (; i < numAgreed; i++) { - Order c = agreedOrders.get(i); - double orderStake = stakedOrders.get(c); - accumulatedStake += orderStake; - if (accumulatedStake > THRESHOLD) break; - } - - if (i < numAgreed) { - // we have a proposed consensus - Order lastAgreed = agreedOrders.get(i); - AVector lastBlocks = lastAgreed.getBlocks(); - long newProposalPoint = winningBlocks.commonPrefixLength(lastBlocks); - return winningOrder.withProposalPoint(newProposalPoint); - } else { - return winningOrder; - } - } - - /** - * Sorts a set of Orders according to level of agreement with a given vector of - * Blocks. Orders with longest common prefix length are placed first. - * - * @param stakedOrders Map with Orders as keys - * @param winningBlocks Vector of blocks to seek agreement with - * @return List of Orders in agreement order - */ - private ArrayList sortByAgreement(HashMap stakedOrders, AVector winningBlocks) { - return Utils.sortListBy(new Function() { - @Override - public Long apply(Order c) { - long match = winningBlocks.commonPrefixLength(c.getBlocks()); - return -match; // sort highest matches first - } - }, stakedOrders.keySet()); - } - - /** - * Gets an ordered list of new blocks from a collection of Chains. Ordering is a - * partial order based on when a block is first observed. This is an important - * heuristic (thou to avoid re-ordering new blocks from the same peer. - */ - private static ArrayList collectNewBlocks(Collection> orders, long consensusPoint) { - // We want to preserve order, remove duplicates - HashSet newBlocks = new HashSet<>(); - ArrayList newBlocksOrdered = new ArrayList<>(); - for (AVector blks : orders) { - if (blks.count()<=consensusPoint) continue; - Iterator it = blks.listIterator(consensusPoint); - while (it.hasNext()) { - Block b = it.next(); - if (!newBlocks.contains(b)) { - newBlocks.add(b); - newBlocksOrdered.add(b); - } - } - } - return newBlocksOrdered; - } - - /** - * Compute the new winning Order for this Peer, including any new blocks - * encountered - * - * @param stakedOrders Amount of stake on each distinct Order - * @param consensusPoint Current consensus point - * @param initialTotalStake Total stake under consideration - * @return Vector of Blocks in wiing Order - */ - public static AVector computeWinningOrder(HashMap stakedOrders, long consensusPoint, - double initialTotalStake) { - assert (!stakedOrders.isEmpty()); - // Get the Voting Set. Will be updated each round to winners of previous round. - HashMap, Double> votingSet = combineToBlocks(stakedOrders); - - // Accumulate new blocks. - ArrayList newBlocksOrdered = collectNewBlocks(votingSet.keySet(), consensusPoint); - - double totalStake = initialTotalStake; - long point = consensusPoint; - - findWinner: - while (votingSet.size() > 1) { - // Accumulate candidate winning Blocks for this round, indexed by next Block - HashMap, Double>> blockVotes = new HashMap<>(); - - for (Map.Entry, Double> me : votingSet.entrySet()) { - AVector blocks = me.getKey(); - long cCount = blocks.count(); - - if (cCount <= point) continue; // skip Ordering with insufficient blocks: cannot win this round - - Block b = blocks.get(point); - - // update hashmap of Orders voting for each block (i.e. agreed on current Block) - HashMap, Double> agreedOrders = blockVotes.get(b); - if (agreedOrders == null) { - agreedOrders = new HashMap<>(); - blockVotes.put(b, agreedOrders); - } - Double stake = me.getValue(); - agreedOrders.put(blocks, stake); - if (stake >= totalStake * 0.5) { - // have a winner for sure, no point continuing so populate final Voting set and break - votingSet.clear(); - votingSet.put(blocks, stake); - break findWinner; - } - } - - if (blockVotes.size() == 0) { - // we have multiple chains, but no more blocks - so they should be all equal - // we can break loop and continue with an arbitrary choice - break findWinner; - } - - Map.Entry, Double>> winningResult = null; - double winningVote = Double.NEGATIVE_INFINITY; - for (Map.Entry, Double>> me : blockVotes.entrySet()) { - HashMap, Double> agreedChains = me.getValue(); - double blockVote = computeVote(agreedChains); - if (blockVote > winningVote) { - winningVote = blockVote; - winningResult = me; - } - } - - if (winningResult==null) throw new Error("This shouldn't happen!"); - votingSet = winningResult.getValue(); // Update Orderings to be included in next round - totalStake = winningVote; // Total Stake among winning Orderings - - // advance to next block position for next round - point++; - } - - if (votingSet.size() == 0) { - // no vote for any Order. Might happen if the peer doesn't have any stake - // and doesn't have any Orders from other peers with stake? - return null; - } - AVector winningBlocks = votingSet.keySet().iterator().next(); - - // add new blocks back to winning chain if not already included - AVector fullWinningBlocks = appendNewBlocks(winningBlocks, newBlocksOrdered, consensusPoint); - - return fullWinningBlocks; - } - - private static final AVector appendNewBlocks(AVector blocks, ArrayList newBlocksOrdered, - long consensusPoint) { - HashSet newBlocks = new HashSet<>(); - newBlocks.addAll(newBlocksOrdered); - - // exclude new blocks already in the base Order - // TODO: what about blocks already in consensus? - Iterator it = blocks.listIterator(Math.min(blocks.count(), consensusPoint)); - while (it.hasNext()) { - newBlocks.remove(it.next()); - } - newBlocksOrdered.removeIf(b -> !newBlocks.contains(b)); - - // sort new blocks by timestamp and append to winning Order - // must be a stable sort to maintain order from equal timestamps - newBlocksOrdered.sort(Block.TIMESTAMP_COMPARATOR); - - AVector fullBlocks = blocks.appendAll(newBlocksOrdered); - return fullBlocks; - } - - /** - * Combine stakes from multiple orders to a single stake for each distinct Block ordering. - * - * @param stakedOrders - * @return Map of AVector to total stake - */ - private static HashMap, Double> combineToBlocks(HashMap stakedOrders) { - HashMap, Double> result = new HashMap<>(); - for (Map.Entry e : stakedOrders.entrySet()) { - Order c = e.getKey(); - Double stake = e.getValue(); - AVector blocks = c.getBlocks(); - Double acc = result.get(blocks); - if (acc == null) { - result.put(blocks, stake); - } else { - result.put(blocks, acc + stake); - } - } - return result; - } - - /** - * Computes the total vote for all entries in a HashMap - * - * @param The type of values used as keys in the HashMap - * @param m A map of values to votes - * @return The total voting stake - */ - public static double computeVote(HashMap m) { - double result = 0.0; - for (Map.Entry me : m.entrySet()) { - result += me.getValue(); - } - return result; - } - - /** - * Compute the total stake for every distinct Order seen. Stores results in - * a map of Orders to staked value. - * - * @param peerOrders A map of peer addresses to signed proposed Orders - * @param peerStakes A map of peers addresses to weighted stakes for each peer - * @param dest Destination hashmap to store the stakes for each Order - * @return The total stake of all chains among peers under consideration - */ - public static double prepareStakedOrders(AMap> peerOrders, - HashMap peerStakes, HashMap dest) { - return peerOrders.reduceValues((acc, signedOrder) -> { - try { - // Get the Order for this peer - Order order = signedOrder.getValue(); - AccountKey cAddress = signedOrder.getAccountKey(); - Double cStake = peerStakes.get(cAddress); - if ((cStake == null) || (cStake == 0.0)) return acc; - Double stake = dest.get(order); - if (stake == null) { - dest.put(order, cStake); // new Order to consider - } else { - dest.put(order, stake + cStake); // add stake to existing Order - } - return acc + cStake; - } catch (Exception e) { - throw Utils.sneakyThrow(e); - } - }, 0.0); - } - - /** - * Gets the Order for the current peer specified by a MergeContext in this - * Belief - * - * @param mc - * @return Order for current Peer, or null if not found - * @throws BadSignatureException - */ - private Order getMyOrder(MergeContext mc) throws BadSignatureException { - AccountKey myAddress = mc.getAccountKey(); - SignedData signed = (SignedData) orders.get(myAddress); - if (signed == null) return null; - assert (signed.getAccountKey().equals(myAddress)); - return signed.getValue(); - } - - /** - * Updates this Belief with a new set of Chains for each peer address - * - * @param newOrders New map of peer keys to Orders - * @return The updated belief, or the same Belief if no change. - */ - public Belief withOrders(BlobMap> newOrders) { - if (newOrders == orders) return this; - return Belief.create(newOrders); - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=getTag(); - return encodeRaw(bs,pos); - } - - @Override - public int estimatedEncodingSize() { - return 1+orders.estimatedEncodingSize()+12; - } - - /** - * Read a Belief from a ByteBuffer. Assumes tag already read. - * @param bb ByteBuffer to read from - * @return Belief instance - * @throws BadFormatException If encoding is invalid - */ - public static Belief read(ByteBuffer bb) throws BadFormatException { - BlobMap> chains = Format.read(bb); - if (chains == null) throw new BadFormatException("Null orders in Belief"); - CVMLong timestamp = Format.read(bb); - if (timestamp == null) throw new BadFormatException("Null timestamp"); - return new Belief(chains, timestamp.longValue()); - } - - @Override - public byte getTag() { - return Tag.BELIEF; - } - - /** - * Gets the current Order for a given Address within this Belief. - * - * @param address Address of peer - * @return The chain for the peer within this Belief, or null if noy found. - */ - public Order getOrder(AccountKey address) { - SignedData sc = orders.get(address); - if (sc == null) return null; - return sc.getValue(); - } - - /** - * Get the map of orders for this Belief - * @return Orders map - */ - public BlobMap> getOrders() { - return orders; - } - - @Override - public void validateCell() throws InvalidDataException { - if (orders == null) throw new InvalidDataException("Null orders", this); - orders.validateCell(); - } - - /** - * Returns the timestamp of this Belief. A Belief should have a new timestamp if - * and only if the Peer incorporates new information. - * @return Timestamp of belief - */ - public long getTimestamp() { - return timestamp; - } - - @Override - public boolean equals(AMap a) { - if (this == a) return true; // important optimisation for e.g. hashmap equality - if (a == null) return false; - if (a.getTag()!=getTag()) return false; - Belief as=(Belief)a; - return equals(as); - } - - /** - * Tests if this Belief is equal to another - * @param a Belief to compare with - * @return true if equal, false otherwise - */ - public boolean equals(Belief a) { - if (a == null) return false; - Hash h=this.cachedHash(); - if (h!=null) { - Hash ha=a.cachedHash(); - if (ha!=null) return Utils.equals(h, ha); - } - - if (timestamp!=a.timestamp) return false; - if (!(Utils.equals(orders, a.orders))) return false; - return true; - } - -} diff --git a/convex-core/src/main/java/convex/core/Block.java b/convex-core/src/main/java/convex/core/Block.java deleted file mode 100644 index 148a8d626..000000000 --- a/convex-core/src/main/java/convex/core/Block.java +++ /dev/null @@ -1,243 +0,0 @@ -package convex.core; - -import java.nio.ByteBuffer; -import java.util.Comparator; -import java.util.List; - -import convex.core.data.ACell; -import convex.core.data.AMap; -import convex.core.data.ARecord; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.Format; -import convex.core.data.Hash; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.SignedData; -import convex.core.data.Tag; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.RT; -import convex.core.lang.impl.RecordFormat; -import convex.core.transactions.ATransaction; -import convex.core.util.Utils; - -/** - * A block contains an ordered collection of signed transactions that may be applied - * collectively as part of a state update. - * - * Blocks represent the units of novelty in the consensus system: a future state is - * 100% deterministic given the previous state and the Block to be applied. - * - * "Man, the living creature, the creating individual, is always more important - * than any established style or system." - Bruce Lee - * - */ -public final class Block extends ARecord { - private final long timestamp; - private final AVector> transactions; - private final AccountKey peerKey; - - private static final Keyword[] BLOCK_KEYS = new Keyword[] { Keywords.TIMESTAMP, Keywords.TRANSACTIONS, Keywords.PEER }; - private static final RecordFormat FORMAT = RecordFormat.of(BLOCK_KEYS); - - /** - * Comparator to sort blocks by timestamp - */ - static final Comparator TIMESTAMP_COMPARATOR = new Comparator<>() { - @Override - public int compare(Block a, Block b) { - int sig = Long.compare(a.getTimeStamp(), b.getTimeStamp()); - return sig; - } - }; - - private Block(long timestamp, AVector> transactions, AccountKey peer) { - super(FORMAT); - this.timestamp = timestamp; - this.transactions = transactions; - this.peerKey=peer; - - if (peerKey==null) throw new Error("Trying to construct block with null peer key"); - } - - @Override - public ACell get(ACell k) { - if (Keywords.TIMESTAMP.equals(k)) return CVMLong.create(timestamp); - if (Keywords.TRANSACTIONS.equals(k)) return transactions; - if (Keywords.PEER.equals(k)) return peerKey; - return null; - } - - @SuppressWarnings("unchecked") - @Override - protected Block updateAll(ACell[] newVals) { - long newTimestamp = RT.ensureLong(newVals[0]).longValue(); - AVector> newTransactions = (AVector>) newVals[1]; - AccountKey newPeer = (AccountKey) newVals[2]; - if ((this.transactions == newTransactions) && (this.timestamp == newTimestamp) && (peerKey==newPeer)) { - return this; - } - return new Block(newTimestamp, newTransactions,newPeer); - } - - /** - * Gets the timestamp of this block - * - * @return Timestamp, as a long value - */ - public long getTimeStamp() { - return timestamp; - } - - /** - * Gets the Peer for this block - * - * @return Address of Peer publishing this block - */ - public AccountKey getPeer() { - return peerKey; - } - - /** - * Creates a block with the given timestamp and transactions - * - * @param timestamp Timestamp for the newly created Block. - * @param transactions A java.util.List instance containing the required transactions - * @param peerKey Peer Key of Peer producing Block - * @return A new Block containing the specified signed transactions - */ - public static Block create(long timestamp, List> transactions, AccountKey peerKey) { - return new Block(timestamp, Vectors.create(transactions),peerKey); - } - - /** - * Creates a block with the given transactions. - * - * @param timestamp Timestamp of block creation, according to Peer - * @param peerKey Public key of Peer producing Block - * @param transactions Vector of transactions to include in Block - * - * @return A new Block containing the specified signed transactions - */ - public static Block create(long timestamp, AccountKey peerKey, AVector> transactions) { - return new Block(timestamp, transactions,peerKey); - } - - /** - * Creates a block with the given transactions. - * - * @param timestamp Timestamp of block creation, according to Peer - * @param peerKey Public key of Peer producing Block - * @param transactions Array of transactions to include in Block - * @return New Block - */ - @SafeVarargs - public static Block of(long timestamp, AccountKey peerKey, SignedData... transactions) { - return new Block(timestamp, Vectors.of((Object[])transactions),peerKey); - } - - /** - * Gets the length of this block in number of transactions - * - * @return Number of transactions on this block - */ - public int length() { - return Utils.checkedInt(transactions.count()); - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=getTag(); - // generic record writeRaw, handles all fields in declared order - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = Utils.writeLong(bs,pos, timestamp); - pos = transactions.encode(bs,pos); - pos = peerKey.writeToBuffer(bs, pos); - return pos; - } - - @Override - public int estimatedEncodingSize() { - return 10+transactions.estimatedEncodingSize()+AccountKey.LENGTH; - } - - /** - * Reads a Block from the given bytebuffer, assuming tag is already read - * - * @param bb ByteBuffer containing Block representation - * @return A Block - * @throws BadFormatException if a Block could noy be read. - */ - public static Block read(ByteBuffer bb) throws BadFormatException { - long timestamp = Format.readLong(bb); - try { - AVector> transactions = Format.read(bb); - if (transactions==null) throw new BadFormatException("Null transactions"); - - AccountKey peer=AccountKey.readRaw(bb); - if (peer==null) throw new BadFormatException("Bad peer key in Block"); - return Block.create(timestamp, peer,transactions); - } catch (ClassCastException e) { - throw new BadFormatException("Error reading Block format", e); - } - } - - /** - * Get the vector of transactions in this Block - * @return Vector of transactions - */ - public AVector> getTransactions() { - return transactions; - } - - @Override - public boolean isCanonical() { - if (!transactions.isCanonical()) return false; - return true; - } - - @Override - public byte getTag() { - return Tag.BLOCK; - } - - @Override - public void validateCell() throws InvalidDataException { - transactions.validateCell(); - } - - @Override - public boolean equals(AMap a) { - if (!(a instanceof Block)) return false; - return equals((Block)a); - } - - /** - * Tests if this Block is equal to another - * @param a PeerStatus to compare with - * @return true if equal, false otherwise - */ - public boolean equals(Block a) { - if (a == null) return false; - if (timestamp!=a.timestamp) return false; - - Hash h=this.cachedHash(); - if (h!=null) { - Hash ha=a.cachedHash(); - if (ha!=null) return Utils.equals(h, ha); - } - - if (!(Utils.equals(peerKey, a.peerKey))) return false; - - if (!(Utils.equals(transactions, a.transactions))) return false; - return true; - } - -} diff --git a/convex-core/src/main/java/convex/core/BlockResult.java b/convex-core/src/main/java/convex/core/BlockResult.java deleted file mode 100644 index 0ec9b9320..000000000 --- a/convex-core/src/main/java/convex/core/BlockResult.java +++ /dev/null @@ -1,211 +0,0 @@ -package convex.core; - -import java.nio.ByteBuffer; - -import convex.core.data.ACell; -import convex.core.data.AMap; -import convex.core.data.ARecord; -import convex.core.data.AVector; -import convex.core.data.Format; -import convex.core.data.Hash; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.Tag; -import convex.core.data.Vectors; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.impl.RecordFormat; -import convex.core.util.Utils; - -/** - * Class representing the result of applying a Block to a State. - * - * Each transaction in the block has a corresponding result entry, which may - * either be a valid result or an error. - * - */ -public class BlockResult extends ARecord { - private State state; - private AVector results; - - private static final Keyword[] BLOCKRESULT_KEYS = new Keyword[] { Keywords.STATE, - Keywords.RESULTS}; - - private static final RecordFormat FORMAT = RecordFormat.of(BLOCKRESULT_KEYS); - - - private BlockResult(State state, AVector results) { - super(FORMAT); - this.state = state; - this.results = results; - } - - /** - * Create a BlockResult - * @param state Resulting State - * @param results Results of transactions in Block - * @return BlockResult instance - */ - public static BlockResult create(State state, Result[] results) { - int n=results.length; - Object[] rs=new Object[n]; - for (int i=0; i results) { - return new BlockResult(state, results); - } - - /** - * Get the State resulting from this Block. - * @return State after Block is executed - */ - public State getState() { - return state; - } - - /** - * Gets the Results of all transactions in the Block - * @return Vector of Results - */ - public AVector getResults() { - return results; - } - - /** - * Checks if a result at a specific position is an error - * @param i Index of result in block - * @return True if result at index i is an error, false otherwise. - */ - public boolean isError(long i) { - return getResult(i).isError(); - } - - /** - * Gets a specific Result - * @param i Index of Result - * @return Result at specified index for the current Block - */ - public Result getResult(long i) { - return results.get(i); - } - - /** - * Gets the error code for a given transaction - * @param i Index of Result - * @return Error code, or null if the transaction succeeded. - */ - public Object getErrorCode(long i) { - Result result=results.get(i); - return result.getErrorCode(); - } - - @Override - public ACell get(ACell key) { - if (Keywords.STATE.equals(key)) return state; - if (Keywords.RESULTS.equals(key)) return results; - return null; - } - - @Override - public byte getTag() { - return Tag.BLOCK_RESULT; - } - - @SuppressWarnings("unchecked") - @Override - protected BlockResult updateAll(ACell[] newVals) { - State newState=(State)newVals[0]; - AVector newResults=(AVector)newVals[1]; - return create(newState,newResults); - } - - @Override - public void validateCell() throws InvalidDataException { - // TODO Auto-generated method stub - - } - - @Override - public void validate() throws InvalidDataException { - super.validate(); - results.validate(); - state.validate(); - - long n=results.count(); - for (long i=0; i newResults=Format.read(bb); - if (newResults==null) throw new BadFormatException("Null results"); - return create(newState,newResults); - } - - @Override - public boolean equals(AMap a) { - if (this == a) return true; // important optimisation for e.g. hashmap equality - if (a == null) return false; - if (a.getTag()!=getTag()) return false; - BlockResult as=(BlockResult)a; - return equals(as); - } - - /** - * Tests if this BlockResult is equal to another - * @param a BlockResult to compare with - * @return true if equal, false otherwise - */ - public boolean equals(BlockResult a) { - if (a == null) return false; - Hash h=this.cachedHash(); - if (h!=null) { - Hash ha=a.cachedHash(); - if (ha!=null) return Utils.equals(h, ha); - } - - if (!(Utils.equals(results, a.results))) return false; - if (!(Utils.equals(state, a.state))) return false; - return true; - } - -} diff --git a/convex-core/src/main/java/convex/core/Coin.java b/convex-core/src/main/java/convex/core/Coin.java deleted file mode 100644 index df3b85ea1..000000000 --- a/convex-core/src/main/java/convex/core/Coin.java +++ /dev/null @@ -1,43 +0,0 @@ -package convex.core; - -/** - * Static Constants for Coin sizes and total supply - */ -public class Coin { - /** - * Copper coin, the lowest (indivisible) denomination. - */ - public static final long COPPER=1L; - - /** - * Copper coin, a denomination for small change/ Equal to 1000 Copper - */ - public static final long BRONZE=1000*COPPER; - - /** - * Silver Coin, a denomination for small payments. Equal to 1000 Bronze - */ - public static final long SILVER=1000*BRONZE; - - /** - * A denomination suitable for medium/large payments. Equal to 1000 Silver, and divisible into one billion copper coins. - * - * Intended as the primary "human scale" quanity of Convex Coins in regular usage. - */ - public static final long GOLD=1000*SILVER; - - /** - * A large denomination. 1000 Gold. - */ - public static final long DIAMOND=1000*GOLD; - - /** - * A massively valuable amount of Convex Coins. One million Gold. - */ - public static final long EMERALD=1000*DIAMOND; - - /** - * The total Convex Coin maximum supply limit. One billion Gold Coins - */ - public static final long SUPPLY=1000*EMERALD; -} diff --git a/convex-core/src/main/java/convex/core/Constants.java b/convex-core/src/main/java/convex/core/Constants.java deleted file mode 100644 index b0de4c348..000000000 --- a/convex-core/src/main/java/convex/core/Constants.java +++ /dev/null @@ -1,195 +0,0 @@ -package convex.core; - -import java.time.Instant; - -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.Format; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMBool; - -/** - * Static class for global configuration constants that affect protocol - * behaviour - */ -public class Constants { - - /** - * Limit of scheduled transactions run in a single Block - */ - public static final long MAX_SCHEDULED_TRANSACTIONS_PER_BLOCK = 100; - - /** - * Threshold of stake required to propose consensus - */ - public static final double PROPOSAL_THRESHOLD = 0.50; - - /** - * Threshold of stake required to confirm consensus - */ - public static final double CONSENSUS_THRESHOLD = 0.67; - - /** - * Initial timestamp for new States - */ - public static final long INITIAL_TIMESTAMP = Instant.parse("2020-02-02T00:20:20.0202Z").toEpochMilli(); - - /** - * Juice price in the initial Genesis State - */ - public static final long INITIAL_JUICE_PRICE = 2L; - - /** - * Initial memory Pool of 1gb - */ - public static final long INITIAL_MEMORY_POOL = 1000000000L; - - /** - * Initial memory price per byte - */ - public static final long INITIAL_MEMORY_PRICE = 10L; - - /** - * Max juice allowable for execution of a single transaction. - */ - public static final long MAX_TRANSACTION_JUICE = 1000000; - - /** - * Constant to set deletion of Etch temporary files on exit. Probably should be true, unless you want to dubug temp files. - */ - public static final boolean ETCH_DELETE_TEMP_ON_EXIT = true; - - /** - * Sequence number used for any new account - */ - public static final long INITIAL_SEQUENCE = 0; - - /** - * Size in bytes of constant overhead applied per non-embedded Cell in memory accounting - */ - public static final long MEMORY_OVERHEAD = 64; - - /** - * Default timeout in milliseconds for client transactions - */ - public static final long DEFAULT_CLIENT_TIMEOUT = 6000; - - /** - * Allowance for initial user / peer accounts - */ - public static final long INITIAL_ACCOUNT_ALLOWANCE = 10000000; - - /** - * Maximum supply of Convex Coins set at protocol level - */ - public static final long MAX_SUPPLY = Coin.SUPPLY; - - /** - * Maximum CVM execution depth - */ - public static final int MAX_DEPTH = 256; - - /** - * Initial global values for a new State - */ - public static final AVector INITIAL_GLOBALS = Vectors.of( - Constants.INITIAL_TIMESTAMP, 0L, Constants.INITIAL_JUICE_PRICE); - - /** - * Maximum length of a symbolic name in characters (keywords and symbols) - * - * Note: Chosen so that small qualified symbolic values are always embedded - */ - public static final int MAX_NAME_LENGTH = 64; - - /** - * Value used to indicate inclusion of a key in a Set. Must be a singleton instance - */ - public static final CVMBool SET_INCLUDED = CVMBool.TRUE; - - /** - * Value used to indicate exclusion of a key from a Set. Must be a singleton instance - */ - public static final CVMBool SET_EXCLUDED = CVMBool.FALSE; - - /** - * Length for public keys - */ - public static final int KEY_LENGTH = 32; - - /** - * Length for Hash values - */ - public static final int HASH_LENGTH = 32; - - /** - * Default number of outgoing connections for a Peer - */ - public static final Integer DEFAULT_OUTGOING_CONNECTION_COUNT = 20; - - /** - * Number of milliseconds average time to drop low-staked Peers - */ - public static final double PEER_CONNECTION_DROP_TIME = 20000; - - /** - * Minimum stake for a PEer to be considered by other Peers in consensus - */ - public static final long MINIMUM_EFFECTIVE_STAKE = Coin.GOLD*1; - - /** - * Default size for client receive buffers. - */ - public static final int RECEIVE_BUFFER_SIZE = Format.LIMIT_ENCODING_LENGTH*10+20; - - /** - * Default size for client receive buffers. - */ - public static final int SEND_BUFFER_SIZE = Format.LIMIT_ENCODING_LENGTH*10+20; - - - /** - * Size of default server socket receive buffer - */ - public static final int SOCKET_SERVER_BUFFER_SIZE = 16*65536; - - /** - * Size of default server socket buffers for a peer connection - */ - public static final int SOCKET_PEER_BUFFER_SIZE = 16*65536; - - /** - * Size of default client socket receive buffer - */ - public static final int SOCKET_RECEIVE_BUFFER_SIZE = 65536; - - /** - * Size of default client socket send buffer - */ - public static final int SOCKET_SEND_BUFFER_SIZE = 65536; - - /** - * Delay before rebroadcasting Belief if not in consensus - */ - public static final long REBROADCAST_DELAY = 200; - - /** - * Delay before a Peer produces another Block - */ - public static final long MIN_BLOCK_TIME = 100; - - /** - * Timeout for syncing with an existing Peer - */ - public static final long PEER_SYNC_TIMEOUT = 60000; - - /** - * Number of fields in a Peer STATUS message - */ - public static final long STATUS_COUNT = 5; - - /** - * Default port for Convex Peers - */ - public static final int DEFAULT_PEER_PORT = 18888; -} diff --git a/convex-core/src/main/java/convex/core/ErrorCodes.java b/convex-core/src/main/java/convex/core/ErrorCodes.java deleted file mode 100644 index c197e9caa..000000000 --- a/convex-core/src/main/java/convex/core/ErrorCodes.java +++ /dev/null @@ -1,190 +0,0 @@ -package convex.core; - -import convex.core.data.Keyword; - -/** - * Standard codes used for CVM Exceptional Conditions. - * - * An Exceptional Condition may include a Message, which is kept outside CVM state but may be user to return - * information to the relevant client. - */ -public class ErrorCodes { - /** - * Error code for a bad sequence. This Error Condition is generated by the CVM only during transaction - * preparation if the Sequence Number for the new transaction is wrong for the given Account (it must be one - * greater than the Sequence Number of the last transaction executed which is stored in the Account). - * - * This Error code may be returned by Peers before publishing a transaction to the network, if the - * Sequence Number cannot possibly be correct (i.e. less than the current Sequence Number). - * - * The message is expected to be the current sequence number: Clients may use this to automatically correct - * and re-submit transactions with the correct sequence, although this is unreliable if multiple clients are - * sending transactions for the same Account. - */ - public static final Keyword SEQUENCE = Keyword.create("SEQUENCE"); - - /** - * Error code for when the specified account does not have enough available funds to perform an operation - */ - public static final Keyword FUNDS = Keyword.create("FUNDS"); - - /** - * Error code for when a transaction runs out of available juice - */ - public static final Keyword JUICE = Keyword.create("JUICE"); - - /** - * Error code for when a transaction exceeds execution depth limits. Typically, this indicates - * infinite recursion. - */ - public static final Keyword DEPTH = Keyword.create("DEPTH"); - - /** - * Error code for situations where a transaction is unable to complete due to insufficient - * Memory Allowance. - * - * This Error Condition is only be generated by the CVM during the failure of transaction completion. - * Within transactions, memory usage may exceed allowances as long as there is enough juice to - * pay for the temporary allocations. - */ - public static final Keyword MEMORY = Keyword.create("MEMORY"); - - /** - * Error code when attempting to perform an action using a non-existent Account - */ - public static final Keyword NOBODY = Keyword.create("NOBODY"); - - /** - * Error code when function or expander application has an inappropriate number of arguments. - * Arity is checked first: it takes precedence over CAST and ARGUMENT errors. - */ - public static final Keyword ARITY = Keyword.create("ARITY"); - - /** - * Error code when an undeclared symbol is accessed - */ - public static final Keyword UNDECLARED = Keyword.create("UNDECLARED"); - - /** - * Error code when the type of some argument cannot be cast to a suitable type for - * some requested operation. ARITY errors take predecence over CAST errors if both - * are applicable. - */ - public static final Keyword CAST = Keyword.create("CAST"); - - /** - * Error code for when indexed access is attempted that is out of bounds for some sequential object. - * - */ - public static final Keyword BOUNDS = Keyword.create("BOUNDS"); - - /** - * Error code for when an argument is of the correct type, but is not an allowable value. - */ - public static final Keyword ARGUMENT = Keyword.create("ARGUMENT"); - - /** - * Error code for a request that would normally be valid, but failed because some aspect of - * actor / system state was wrong. Typically indicates that some preparatory step was omitted, - * appropriate pre-conditions were not checked, or an operation was attempted at an inappropriate time - */ - public static final Keyword STATE = Keyword.create("STATE"); - - /** - * Error code caused by compilation failure with an invalid AST. Should only occur during - * compile phase of on-chain Compiler - */ - public static final Keyword COMPILE = Keyword.create("COMPILE"); - - /** - * Error code caused by failure to successfully expand an AST node. Should only occur during - * expand phase of on-chain Compiler - */ - public static final Keyword EXPAND = Keyword.create("EXPAND"); - - /** - * Error code indicating that an asserted condition was not met. This usually indicates invalid - * input that failed a precondition check. The message should be used to give meaningful feedback to - * the User. - */ - public static final Keyword ASSERT = Keyword.create("ASSERT"); - - /** - * Error code indicating that an a trust condition was violated. This usually means a USer or Actor - * attempted to perform an unauthorised operation. - */ - public static final Keyword TRUST = Keyword.create("TRUST"); - - /** - * ErrorCode for an unexpected Error. Likely fatal. - */ - public static final Keyword UNEXPECTED = Keyword.create("UNEXPECTED"); - - /** - * Error code for unhandled exceptions - */ - public static final Keyword EXCEPTION = Keyword.create("EXCEPTION"); - - // Error codes for non-error values - - /** - * Exceptional Condition indicating a halt operation was executed. - * - * This will halt the currently executing transaction context and return to the caller. - */ - public static final Keyword HALT = Keyword.create("HALT"); - - /** - * Exceptional Condition indicating a recur operation was executed - * - * This will return execution to the surrounding loop or function binding, which will be - * re-executed with new bindings provided to the recur operation. - */ - public static final Keyword RECUR = Keyword.create("RECUR"); - - /** - * Exceptional Condition indicating a tailcall operation has been executed - * - * This will return execution to the surrounding loop or function binding, which will be - * re-executed with new bindings provided to the recur operation. - */ - public static final Keyword TAILCALL = Keyword.create("TAILCALL"); - - /** - * Exceptional Condition indicating a return operation was executed - * - * This will return execution to the caller of surrounding function binding, with whatever - * value is passed to the return operation as a result. - */ - public static final Keyword RETURN = Keyword.create("RETURN"); - - /** - * Exceptional condition indicated a 'reduced' result. - */ - public static final Keyword REDUCED = Keyword.create("REDUCED"); - - /** - * Exceptional Condition indicating a halt operation was executed. - * - * This will terminate the currently executing transaction context, roll back any state changes - * and return to the caller with whatever value is passed as the rollback result. - */ - public static final Keyword ROLLBACK = Keyword.create("ROLLBACK"); - - /** - * Exceptional Condition indicating a bad signature on a transaction. - */ - public static final Keyword SIGNATURE = Keyword.create("SIGNATURE"); - - /** - * Exceptional Condition indicating something is not yet implemented - */ - public static final Keyword TODO = Keyword.create("TODO"); - - /** - * ErrorCode for a FATAL Error. Should trigger Peer shutdown. - */ - public static final Keyword FATAL = Keyword.create("FATAL"); - - -} diff --git a/convex-core/src/main/java/convex/core/MergeContext.java b/convex-core/src/main/java/convex/core/MergeContext.java deleted file mode 100644 index 79ed1e0ee..000000000 --- a/convex-core/src/main/java/convex/core/MergeContext.java +++ /dev/null @@ -1,87 +0,0 @@ -package convex.core; - -import convex.core.crypto.AKeyPair; -import convex.core.data.ACell; -import convex.core.data.AccountKey; -import convex.core.data.SignedData; - -/** - * Class representing the context to be used for a Belief merge/update function. This - * context must be created by a Peer to perform a valid Belief merge. It can be safely - * discarded after use. - * - * SECURITY: contains a hot key pair! We need this to sign new belief updates - * including any chains we want to communicate. Don't allow this to leak - * anywhere! - * - */ -public class MergeContext { - - private final AccountKey publicKey; - private final State state; - private final AKeyPair keyPair; - private final long timestamp; - - private MergeContext(AKeyPair peerKeyPair, long mergeTimestamp, State consensusState) { - this.state = consensusState; - this.publicKey = peerKeyPair.getAccountKey(); - this.keyPair = peerKeyPair; - this.timestamp = mergeTimestamp; - } - - /** - * Create a MergeContext - * @param kp Keypair - * @param timestamp Timestamp - * @param s Consensus State - * @return New MergeContext instance - */ - public static MergeContext create(AKeyPair kp, long timestamp, State s) { - return new MergeContext(kp, timestamp, s); - } - - /** - * Get the address of the current Peer (the one performing the merge) - * - * @return The Address of the peer. - */ - public AccountKey getAccountKey() { - return publicKey; - } - - /** - * Sign a value using the keypair for this MergeContext - * @param Type of value - * @param value Value to sign - * @return Signed value - */ - public SignedData sign(T value) { - return SignedData.create(keyPair, value); - } - - /** - * Gets the timestamp of this merge - * @return Timestamp - */ - public long getTimeStamp() { - return timestamp; - } - - /** - * Updates the timestamp of this MergeContext - * @param newTimestamp New timestamp - * @return Updated MergeContext - */ - public MergeContext withTimestamp(long newTimestamp) { - return new MergeContext(keyPair, newTimestamp, state); - } - - /** - * Gets the Consensus State for this merge - * @return Consensus State - */ - public State getConsensusState() { - return state; - } - -} diff --git a/convex-core/src/main/java/convex/core/Order.java b/convex-core/src/main/java/convex/core/Order.java deleted file mode 100644 index 2b7cd60fa..000000000 --- a/convex-core/src/main/java/convex/core/Order.java +++ /dev/null @@ -1,309 +0,0 @@ -package convex.core; - -import java.nio.ByteBuffer; - -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.Format; -import convex.core.data.IRefFunction; -import convex.core.data.Ref; -import convex.core.data.Tag; -import convex.core.data.Vectors; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; - -/** - * Class representing an Ordering of transactions, along with the consensus position. - * - * An Ordering contains: - *
    - *
  • The Vector of known verified Blocks announced by the Peer
  • - *
  • The proposed consensus point (point at which the peer believes there is sufficient - * alignment for consensus)
  • - *
  • The current consensus point (point at which the - * peer has observed sufficient consistent consensus proposals)
  • - *
- * - * An Ordering is immutable. - * - */ -public class Order extends ACell { - private final AVector blocks; - - private final long proposalPoint; - private final long consensusPoint; - - private Order(AVector blocks, long proposalPoint, long consensusPoint) { - this.blocks = blocks; - this.consensusPoint = consensusPoint; - this.proposalPoint = proposalPoint; - } - - /** - * Create an Order - * @param blocks Blocks in ORder - * @param proposalPoint Proposal Point - * @param consensusPoint Conesnsus Point - * @return New Order instance - */ - private static Order create(AVector blocks, long proposalPoint, long consensusPoint) { - return new Order(blocks, proposalPoint, consensusPoint); - } - - /** - * Create an empty Order - - * @return New Order instance - */ - public static Order create() { - return create(Vectors.empty(), 0, 0); - } - - private byte getRecordTag() { - return Tag.ORDER; - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=getRecordTag(); - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = blocks.encode(bs,pos); - pos = Format.writeVLCLong(bs,pos, proposalPoint); - pos = Format.writeVLCLong(bs,pos, consensusPoint); - return pos; - } - - @Override - public int estimatedEncodingSize() { - return blocks.estimatedEncodingSize()+30; // blocks plus enough size for points - } - - /** - * Decode an Order from a ByteBuffer - * @param bb ByteBuffer to read from - * @return Order instance - * @throws BadFormatException If encoding format is invalid - */ - public static Order read(ByteBuffer bb) throws BadFormatException { - AVector blocks = Format.read(bb); - if (blocks==null) { - throw new BadFormatException("Null blocks in Order!"); - } - long bcount=blocks.count(); - - long pp = Format.readVLCLong(bb); - long cp = Format.readVLCLong(bb); - - if ((cp < 0) || (cp > bcount)) { - throw new BadFormatException("Consensus point outside current block range: " + cp); - } - if (ppbcount) { - throw new BadFormatException("Proposal point outside block range: " + pp); - } - return new Order(blocks, pp, cp); - } - - - - @Override - public boolean isCanonical() { - // Always canonical? - return true; - } - - @Override public final boolean isCVMValue() { - // Orders exist outside CVM only - return false; - } - - @Override - public void print(StringBuilder sb) { - sb.append("{"); - sb.append(":prop " + getProposalPoint() + ","); - sb.append(":cons " + getConsensusPoint() + ","); - sb.append(":hash " + getHash() + ","); - sb.append(":blocks "); - blocks.print(sb); - sb.append("}\n"); - } - - /** - * Checks if another Order is consistent with this Order. - * - * Order is defined as consistent iff: - *
    - *
  • Blocks are equal up to the Consensus - * Point of this Order - *
  • - *
- * - * @param bc Order to compare with - * @return True if chains are consistent, false otherwise. - */ - public boolean checkConsistent(Order bc) { - long commonPrefix = blocks.commonPrefixLength(bc.blocks); - return commonPrefix >= consensusPoint; - } - - /** - * Gets the Consensus Point of this Order - * @return Consensus Point - */ - public long getConsensusPoint() { - return consensusPoint; - } - - /** - * Gets the Proposal Point of this Order - * @return Proposal Point - */ - public long getProposalPoint() { - return proposalPoint; - } - - /** - * Gets the Blocks in this Order - * @return Vector of Blocks - */ - public AVector getBlocks() { - return blocks; - } - - /** - * Get a specific Block in this Order - * @param i Index of Block - * @return Block at specified index. - */ - public Block getBlock(long i) { - return blocks.get(i); - } - - /** - * Propose a new block of transactions in this Order - * - * @param block Block to append - * @return The updated chain - */ - public Order propose(Block block) { - AVector newBlocks = blocks.append(block); - return create(newBlocks, proposalPoint, consensusPoint); - } - - /** - * Updates blocks in this Order. Returns the same Order if the blocks are identical. - * @param newBlocks New blocks to use - * @return Updated Order, or the same order if unchanged - */ - public Order withBlocks(AVector newBlocks) { - if (blocks == newBlocks) return this; - return create(newBlocks, proposalPoint, consensusPoint); - } - - /** - * Updates this Order with a new proposal position. It is an error to set the - * proposal point before the consensus point, or beyond the last block. - * - * @param newProposalPoint New Proposal Point in Order - * @return Updated Order - */ - public Order withProposalPoint(long newProposalPoint) { - if (this.proposalPoint == newProposalPoint) return this; - if (newProposalPoint < consensusPoint) { - throw new IllegalArgumentException( - "Trying to move proposed consensus before confirmed consensus?! " + newProposalPoint); - } - if (newProposalPoint > blocks.count()) throw new IndexOutOfBoundsException("Block index: " + newProposalPoint); - return new Order(blocks, newProposalPoint, consensusPoint); - } - - /** - * Updates this Order with a new consensus position. - * - * Proposal point will be set to the max of the consensus point and the current - * proposal point - * - * @param newConsensusPoint New consensus point - * @return Updated chain, or this Chain instance if no change. - */ - public Order withConsenusPoint(long newConsensusPoint) { - if (this.consensusPoint == newConsensusPoint) return this; - if (newConsensusPoint > blocks.count()) - throw new IndexOutOfBoundsException("Block index: " + newConsensusPoint); - long newProposalPoint = Math.max(proposalPoint, newConsensusPoint); - return create(blocks, newProposalPoint, newConsensusPoint); - } - - /** - * Get the number of Blocks in this Order - * @return Number of Blocks - */ - public long getBlockCount() { - return blocks.count(); - } - - /** - * Clears the consensus and proposal point - * @return Updated order with zeroed consensus positions - */ - public Order withoutConsenus() { - return create(blocks, 0, 0); - } - - /** - * Update this chain with a new list of blocks - * - * @param newBlocks New vector of blocks to use in this Chain - * @return The updated Order - */ - public Order updateBlocks(AVector newBlocks) { - if (blocks == newBlocks) return this; - long prefix = blocks.commonPrefixLength(newBlocks); - long newProposalPoint = Math.min(prefix, proposalPoint); - long newConsensusPoint = Math.min(consensusPoint, newProposalPoint); - return create(newBlocks, newProposalPoint, newConsensusPoint); - } - - @Override - public void validate() throws InvalidDataException { - super.validate(); - blocks.validate(); - } - - @Override - public void validateCell() throws InvalidDataException { - - } - - @Override - public int getRefCount() { - return blocks.getRefCount(); - } - - @Override - public Ref getRef(int i) { - return blocks.getRef(i); - } - - @Override - public Order updateRefs(IRefFunction func) { - AVector newBlocks = blocks.updateRefs(func); - return this.withBlocks(newBlocks); - } - - @Override - public byte getTag() { - return Tag.ORDER; - } - - @Override - public ACell toCanonical() { - return this; - } -} diff --git a/convex-core/src/main/java/convex/core/Peer.java b/convex-core/src/main/java/convex/core/Peer.java deleted file mode 100644 index 67a1c4b05..000000000 --- a/convex-core/src/main/java/convex/core/Peer.java +++ /dev/null @@ -1,564 +0,0 @@ -package convex.core; - -import java.io.IOException; -import java.util.function.Consumer; - -import convex.core.crypto.AKeyPair; -import convex.core.data.ACell; -import convex.core.data.AMap; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.Address; -import convex.core.data.BlobMap; -import convex.core.data.Hash; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.Maps; -import convex.core.data.PeerStatus; -import convex.core.data.Ref; -import convex.core.data.SignedData; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadSignatureException; -import convex.core.exceptions.InvalidDataException; -import convex.core.init.Init; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.store.AStore; -import convex.core.store.Stores; -import convex.core.transactions.ATransaction; -import convex.core.util.Utils; - -/** - *

- * Immutable class representing the encapsulated state of a Peer - *

- * - * SECURITY: - *
    - *
  • Needs to contain the Peer's unlocked private key for online signing.
  • - *
  • Manages Peer state transitions given external events. Must do so - * correctly.
  • - *
- * - *

- * Must have at least one state, the initial state. New states will be added as - * consensus updates happen. - *

- * - * - * "Don't worry about what anybody else is going to do. The best way to predict - * the future is to invent it." - Alan Kay - */ -public class Peer { - /** This Peer's key */ - private final AccountKey peerKey; - - /** This Peer's key pair - * - * Make transient to mark that this should never be Persisted by accident - * - */ - private transient final AKeyPair keyPair; - - /** The latest merged belief */ - private final SignedData belief; - - /** - * The latest observed timestamp. This is increased by the Server polling the - * local clock. - */ - private final long timestamp; - - /** - * Vector of states - */ - private final AVector states; - - /** - * Vector of results - */ - private final AVector blockResults; - - private Peer(AKeyPair kp, SignedData belief, AVector states, AVector results, - long timeStamp) { - this.keyPair = kp; - this.peerKey = kp.getAccountKey(); - this.belief = belief; - this.states = states; - this.blockResults = results; - this.timestamp = timeStamp; - } - - /** - * Constructs a Peer instance from persisted PEer Data - * @param keyPair Key Pair for Peer - * @param peerData Peer data map - * @return NEw Peer instance - */ - @SuppressWarnings("unchecked") - public static Peer fromData(AKeyPair keyPair,AMap peerData) { - SignedData belief=(SignedData) peerData.get(Keywords.BELIEF); - AVector results=(AVector) peerData.get(Keywords.RESULTS); - AVector states=(AVector) peerData.get(Keywords.STATES); - long timestamp=belief.getValue().getTimestamp(); - return new Peer(keyPair,belief,states,results,timestamp); - } - - /** - * Gets the Peer Datat map for this Peer - * @return Peer data - */ - public AMap toData() { - return Maps.of( - Keywords.BELIEF,belief, - Keywords.RESULTS,blockResults, - Keywords.STATES,states - ); - } - - /** - * Creates a Peer - * @param peerKP Key Pair - * @param initialState Genesis State - * @return New Peer instance - */ - public static Peer create(AKeyPair peerKP, State initialState) { - Belief belief = Belief.createSingleOrder(peerKP); - SignedData sb = peerKP.signData(belief); - AVector states=Vectors.of(initialState); - - // Ensure initial belief and states are persisted in current store - ACell.createPersisted(sb); - ACell.createPersisted(states); - - // Check belief persistence - Ref> sbr=Ref.forHash(sb.getHash()); - if (sbr==null) { - throw new Error("Belief not correctly persisted! "+sb.getHash()); - } - - return new Peer(peerKP, sb, states, Vectors.empty(), initialState.getTimeStamp().longValue()); - } - - /** - * Create a Peer instance from a remotely acquired Belief - * @param peerKP Peer KeyPair - * @param initialState Initial genesis State of the Network - * @param remoteBelief Remote belief to sync with - * @return New Peer instance - */ - public static Peer create(AKeyPair peerKP, State initialState, Belief remoteBelief) { - Peer peer=create(peerKP,initialState); - try { - peer=peer.mergeBeliefs(remoteBelief); - return peer; - } catch (Throwable e) { - throw Utils.sneakyThrow(e); - } - } - - /** - * Restores a Peer from the Etch database specified in Config - * @param store Store to restore from - * @param keyPair Key Pair to use for restored Peer - * @return Peer instance, or null if root hash was not found - * @throws IOException If store reading failed - */ - public static Peer restorePeer(AStore store,AKeyPair keyPair) throws IOException { - AMap peerData=getPeerData(store); - if (peerData==null) return null; - Peer peer=Peer.fromData(keyPair,peerData); - return peer; - } - - /** - * Gets Peer Data from a Store. - * - * @param store Store to retrieve Peer Datat from - * @return Peer data map, or null if not available - * @throws IOException If a store IO error occurs - */ - public static AMap getPeerData(AStore store) throws IOException { - AStore tempStore=Stores.current(); - try { - Stores.setCurrent(store); - Hash root = store.getRootHash(); - Ref ref=store.refForHash(root); - if (ref==null) return null; // not found case - if (ref.getStatus() peerData=(AMap) ref.getValue(); - return peerData; - } finally { - Stores.setCurrent(tempStore); - } - } - - /** - * Creates a new Peer instance at server startup using the provided - * configuration. Current store must be set to store for server. - * - * @param keyPair Key pair for genesis peer - * @param genesisState Genesis state, or null to generate fresh state - * @return A new Peer instance - */ - public static Peer createGenesisPeer(AKeyPair keyPair, State genesisState) { - if (keyPair == null) throw new IllegalArgumentException("Peer initialisation requires a keypair"); - - if (genesisState == null) { - genesisState=Init.createState(Utils.listOf(keyPair.getAccountKey())); - genesisState=genesisState.withTimestamp(Utils.getCurrentTimestamp()); - } - - return create(keyPair, genesisState); - } - - /** - * Gets a MergeContext for this Peer - * @return MergeContext - */ - public MergeContext getMergeContext() { - return MergeContext.create(keyPair, timestamp, getConsensusState()); - } - - /** - * Updates the timestamp to the specified time, going forwards only - * - * @param newTimestamp New Peer timestamp - * @return This peer upated with the given timestamp - */ - public Peer updateTimestamp(long newTimestamp) { - if (newTimestamp < timestamp) return this; - return new Peer(keyPair, belief, states, blockResults, timestamp); - } - - /** - * Compiles and executes a query on the current consensus state of this Peer. - * - * @param Type of result - * @param form Form to compile and execute. - * @param address Address to use for query execution - * @return The Context containing the query results. Will be NOBODY error if address / account does not exist - */ - @SuppressWarnings("unchecked") - public Context executeQuery(ACell form, Address address) { - State state=getConsensusState(); - - if (address==null) { - return Context.createFake(state).withError(ErrorCodes.NOBODY,"Null Address provided for query"); - } - - Context ctx= Context.createFake(state, address); - - if (state.getAccount(address)==null) { - return ctx.withError(ErrorCodes.NOBODY,"Account does not exist for query: "+address); - } - - Context> ectx = ctx.expandCompile(form); - if (ectx.isExceptional()) { - return (Context) ectx; - } - - AOp op = ectx.getResult(); - Context rctx = ctx.run(op); - return rctx; - } - - /** - * Estimates the coin cost of a executing a given transaction by performing a "dry run". - * - * This will be exact if no intermediate transactions affect the state, and if no time-dependent functionality is used. - * - * @param trans Transaction to test - * @return Estimated cost - */ - public long estimateCost(ATransaction trans) { - Address address=trans.getAddress(); - State state=getConsensusState(); - Context ctx=executeDryRun(trans); - return state.getBalance(address)-ctx.getState().getBalance(address); - } - - /** - * Executes a "dry run" transaction on the current consensus state of this Peer. - * - * @param Type of Result - * @param transaction Transaction to execute - * @return The Context containing the transaction results. - */ - public Context executeDryRun(ATransaction transaction) { - Context ctx=getConsensusState().applyTransaction(transaction); - return ctx; - } - - /** - * Executes a query in this Peer's current Consensus State, using a default address - * @param Type of query result - * @param form Form to execute as a Query - * @return Context after executing query - */ - public Context executeQuery(ACell form) { - return executeQuery(form,Init.getGenesisAddress()); - } - - /** - * Gets the timestamp of this Peer - * @return Timestamp - */ - public long getTimeStamp() { - return timestamp; - } - - /** - * Gets the Peer Key of this Peer. - * @return Peer Key of Peer. - */ - public AccountKey getPeerKey() { - return peerKey; - } - - /** - * Gets the controller Address for this Peer - * @return Address of Peer controller Account, or null if does not exist - */ - public Address getController() { - PeerStatus ps= getConsensusState().getPeer(peerKey); - if (ps==null) return null; - return ps.getController(); - } - - /** - * Gets the Peer Key of this Peer. - * @return Address of Peer. - */ - public AKeyPair getKeyPair() { - return keyPair; - } - - /** - * Get the current Belief of this Peer - * @return Belief - */ - public Belief getBelief() { - return belief.getValue(); - } - - /** - * Get the signed Belief of this Peer - * @return Signed Belief - */ - public SignedData getSignedBelief() { - return belief; - } - - /** - * Signs a value with the keypair of this Peer - * @param Type of value to sign - * @param value Value to sign - * @return Signed data value - */ - public SignedData sign(T value) { - return SignedData.create(keyPair, value); - } - - /** - * Gets the current consensus state for this chain - * - * @return Consensus state for this chain (initial state if no block consensus) - */ - public State getConsensusState() { - return states.get(states.count() - 1); - } - - /** - * Merges a set of new Beliefs into this Peer's belief. Beliefs may be null, in - * which case they are ignored. - * - * @param beliefs An array of Beliefs. May contain nulls, which will be ignored. - * @return Updated Peer after Belief Merge - * @throws InvalidDataException if - * @throws BadSignatureException IF a Signature validation fails - * - */ - public Peer mergeBeliefs(Belief... beliefs) throws BadSignatureException, InvalidDataException { - Belief belief = getBelief(); - MergeContext mc = MergeContext.create(keyPair, timestamp, getConsensusState()); - Belief newBelief = belief.merge(mc, beliefs); - - long ocp=getConsensusPoint(); - Order newOrder=newBelief.getOrder(peerKey); - if (ocp>newBelief.getOrder(peerKey).getConsensusPoint()) { - // This probably shouldn't happen, but just in case..... - System.err.println("Receding consensus? Old CP="+ocp +", New CP="+newOrder.getConsensusPoint()); - @SuppressWarnings("unused") - Belief newBelief2 = belief.merge(mc, beliefs); - - } - - return updateConsensus(newBelief); - } - - /** - * Update this Peer with Consensus State for an updated Belief - * - * @param newBelief - * @return - * @throws BadSignatureException - */ - private Peer updateConsensus(Belief newBelief) { - if (belief.getValue() == newBelief) return this; - Order myOrder = newBelief.getOrder(peerKey); // this peer's chain from new belief - long consensusPoint = myOrder.getConsensusPoint(); - long stateIndex = states.count() - 1; // index of last state - AVector blocks = myOrder.getBlocks(); - - // need to advance states - AVector newStates = this.states; - AVector newResults = this.blockResults; - while (stateIndex < consensusPoint) { // add states until last state is at consensus point - State s = newStates.get(stateIndex); - Block block = blocks.get(stateIndex); - BlockResult br = s.applyBlock(block); - newStates = newStates.append(br.getState()); - newResults = newResults.append(br); - stateIndex++; - } - SignedData sb = keyPair.signData(newBelief); - return new Peer(keyPair, sb, newStates, newResults, timestamp); - } - - /** - * Persist the state of the Peer to the current store. We ensure states and results are also persisted - * @param noveltyHandler Novelty handler for Belief - * @return Updates Peer - */ - public Peer persistState(Consumer> noveltyHandler) { - // Peer Belief must be announced using novelty handler - SignedData sb=this.belief; - sb.announce(noveltyHandler); - - // Persist states - AVector newStates = this.states; - newStates=ACell.createPersisted(newStates).getValue(); - - // Persist results - AVector newResults = this.blockResults; - newResults=ACell.createPersisted(newResults).getValue(); - - return new Peer(this.keyPair, sb, newStates, newResults, this.timestamp); - } - - /** - * Gets the vector of States maintained by this Peer, starting from the - * Genesis state (index 0). - * - * @return Vector of states - */ - public AVector getStates() { - return states; - } - - /** - * Gets the result of a specific transaction - * @param blockIndex Index of Block in Order - * @param txIndex Index of transaction in block - * @return Result from transaction - */ - public Result getResult(long blockIndex, long txIndex) { - return blockResults.get(blockIndex).getResult(txIndex); - } - - /** - * Gets the BlockResult of a specific block index - * @param i Index of Block - * @return BlockResult - */ - public BlockResult getBlockResult(long i) { - return blockResults.get(i); - } - - /** - * Propose a new Block. Adds the block to the current proposed chain for this - * Peer. - * - * @param block Block to publish - * @return Peer after proposing new Block in Peer's own Order - */ - public Peer proposeBlock(Block block) { - Belief b = getBelief(); - BlobMap> orders = b.getOrders(); - - Order myOrder = b.getOrder(peerKey); - if (myOrder==null) myOrder=Order.create(); - - Order newChain = myOrder.propose(block); - SignedData newSignedChain = sign(newChain); - BlobMap> newChains = orders.assoc(peerKey, newSignedChain); - Belief newBelief=b.withOrders(newChains); - return updateConsensus(newBelief); - } - - /** - * Gets the Consensus Point for this Peer - * @return Consensus Point value - */ - public long getConsensusPoint() { - Order order=getPeerOrder(); - if (order==null) return 0; - return order.getConsensusPoint(); - } - - /** - * Gets the current Order for this Peer - * - * @return The Order for this peer in its current Belief. Will return null if the Peer is not a peer in the current consensus state - * - */ - public Order getPeerOrder() { - return getBelief().getOrder(peerKey); - } - - /** - * Gets the current chain this Peer sees for a given peer address - * - * @param peerKey Peer Key - * @return The current Order for the specified peer - */ - public Order getOrder(AccountKey peerKey) { - return getBelief().getOrder(peerKey); - } - - /** - * Returns State as-of timestamp. - * - * Timestamp doesn't need to be an exact match; a leftmost State will be returned - unless timestamp is too old. - * - * @param timestamp Timestamp in milliseconds. - * @return State or null. - */ - public State asOf(CVMLong timestamp) { - return Utils.stateAsOf(states, timestamp); - } - - /** - * Construct a vector of States starting at specified timestamp, and with a given interval in milliseconds. - * - * @param timestamp Timestamp in milliseconds. - * @param interval Interval in milliseconds. - * @param count Number of times to query. - * @return Vector of States. - */ - public AVector asOfRange(CVMLong timestamp, long interval, int count) { - return Utils.statesAsOfRange(states, timestamp, interval, count); - } - - /** - * Get the Network ID for this PEer - * @return Network ID - */ - public Hash getNetworkID() { - return getStates().get(0).getHash(); - } -} diff --git a/convex-core/src/main/java/convex/core/Result.java b/convex-core/src/main/java/convex/core/Result.java deleted file mode 100644 index 0d6408a4d..000000000 --- a/convex-core/src/main/java/convex/core/Result.java +++ /dev/null @@ -1,199 +0,0 @@ -package convex.core; - -import java.nio.ByteBuffer; - -import convex.core.data.ACell; -import convex.core.data.ARecord; -import convex.core.data.ARecordGeneric; -import convex.core.data.AString; -import convex.core.data.AVector; -import convex.core.data.Keywords; -import convex.core.data.Tag; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.Context; -import convex.core.lang.impl.AExceptional; -import convex.core.lang.impl.ErrorValue; -import convex.core.lang.impl.RecordFormat; - -/** - * Class representing the result of a Query or Transaction. - * - * A Result is typically used to communicate the outcome of a Query or a Transaction from a Peer to a Client. - * - * - */ -public final class Result extends ARecordGeneric { - - private static final RecordFormat RESULT_FORMAT=RecordFormat.of(Keywords.ID,Keywords.RESULT,Keywords.ERROR_CODE,Keywords.TRACE); - - private Result(AVector values) { - super(RESULT_FORMAT, values); - } - - private static Result create(AVector values) { - return new Result(values); - } - - /** - * Create a Result - * @param id ID of Result message - * @param value Result Value - * @param errorCode Error Code (may be null for success) - * @param trace Error Trace - * @return Result instance - */ - public static Result create(CVMLong id, ACell value, ACell errorCode, ACell trace) { - return create(Vectors.of(id,value,errorCode,trace)); - } - - /** - * Create a Result - * @param id ID of Result message - * @param value Result Value - * @param errorCode Error Code (may be null for success) - * @return Result instance - */ - public static Result create(CVMLong id, ACell value, ACell errorCode) { - return create(id,value,errorCode,null); - } - - /** - * Create a Result - * @param id ID of Result message - * @param value Result Value - * @return Result instance - */ - public static Result create(CVMLong id, ACell value) { - return create(id,value,null,null); - } - - /** - * Returns the message ID for this result. Message ID is an arbitrary ID assigned by a client requesting a transaction. - * - * @return ID from this result - */ - public ACell getID() { - return values.get(0); - } - - /** - * Returns the value for this result. The value is the result of transaction execution (may be an error message if the transaction failed) - * - * @param Type of Value - * @return ID from this result - */ - @SuppressWarnings("unchecked") - public T getValue() { - return (T)values.get(1); - } - - /** - * Returns the stack trace for this result. May be null - * - * @return ID from this result - */ - @SuppressWarnings("unchecked") - public AVector getTrace() { - return (AVector) values.get(3); - } - - /** - * Returns the Error Code from this Result. Normally this should be a Keyword. - * - * Will be null if no error occurred. - * - * @return ID from this result - */ - public ACell getErrorCode() { - return values.get(2); - } - - @Override - public AVector values() { - return values; - } - - @Override - protected ARecord withValues(AVector newValues) { - if (values==newValues) return this; - return new Result(newValues); - } - - @Override - public void validateCell() throws InvalidDataException { - super.validateCell(); - Object id=values.get(0); - if ((id!=null)&&!(id instanceof CVMLong)) { - throw new InvalidDataException("Result ID must be a CVM long value",this); - } - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.RESULT; - pos=values.encodeRaw(bs,pos); - return pos; - } - - /** - * Reads a Result from a ByteBuffer encoding. Assumes tag byte already read. - * - * @param bb ByteBuffer to read from - * @return The Result read - * @throws BadFormatException If a Result could not be read - */ - public static Result read(ByteBuffer bb) throws BadFormatException { - AVector v=Vectors.read(bb); - if (v.size()!=RESULT_FORMAT.count()) throw new BadFormatException("Invalid number of fields for Result!"); - - return create(v); - } - - /** - * Tests is the Result represents an Error - * @return True if error, false otherwise - */ - public boolean isError() { - return getErrorCode()!=null; - } - - /** - * Constructs a Result from a Context - * @param id Id for Result - * @param ctx Context - * @return New Result instance - */ - public static Result fromContext(CVMLong id,Context ctx) { - Object result=ctx.getValue(); - ACell errorCode=null; - ACell trace=null; - if (result instanceof AExceptional) { - AExceptional ex=(AExceptional)result; - result=ex.getMessage(); - errorCode=ex.getCode(); - if (ex instanceof ErrorValue) { - trace=Vectors.create(((ErrorValue)ex).getTrace()); - } - } - return create(id,(ACell)result,errorCode,trace); - } - - /** - * Updates result with a given message ID. Used to tag Results for return to Clients - * @param id New Result message ID - * @return Updated Result - */ - public Result withID(ACell id) { - return create(values.assoc(0, id)); - } - - @Override - public byte getTag() { - return Tag.RESULT; - } - - -} diff --git a/convex-core/src/main/java/convex/core/State.java b/convex-core/src/main/java/convex/core/State.java deleted file mode 100644 index b6344d7e6..000000000 --- a/convex-core/src/main/java/convex/core/State.java +++ /dev/null @@ -1,808 +0,0 @@ -package convex.core; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.HashMap; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import convex.core.data.ABlob; -import convex.core.data.ACell; -import convex.core.data.AMap; -import convex.core.data.ARecord; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.core.data.BlobMap; -import convex.core.data.BlobMaps; -import convex.core.data.Format; -import convex.core.data.Hash; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.LongBlob; -import convex.core.data.MapEntry; -import convex.core.data.PeerStatus; -import convex.core.data.Ref; -import convex.core.data.SignedData; -import convex.core.data.Strings; -import convex.core.data.Symbol; -import convex.core.data.Tag; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.BadSignatureException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.RT; -import convex.core.lang.Symbols; -import convex.core.lang.impl.RecordFormat; -import convex.core.transactions.ATransaction; -import convex.core.util.Counters; -import convex.core.util.Utils; - -/** - * Class representing the immutable state of the CVM - * - * State transitions are represented by blocks of transactions, according to the logic: s[n+1] = s[n].applyBlock(b[n]) - * - * State contains the following elements - Map of AccountStatus for every - * Address - Map of PeerStatus for every Peer Address - Global values - Schedule - * data structure - * - * "State. You're doing it wrong" - Rich Hickey - * - */ -public class State extends ARecord { - private static final Keyword[] STATE_KEYS = new Keyword[] { Keywords.ACCOUNTS, Keywords.PEERS, - Keywords.GLOBALS, Keywords.SCHEDULE }; - - private static final RecordFormat FORMAT = RecordFormat.of(STATE_KEYS); - - /** - * Symbols for Globals - */ - static final AVector GLOBAL_SYMBOLS=Vectors.of(Symbols.TIMESTAMP, Symbols.FEES, Symbols.JUICE_PRICE); - - // Indexes for globals in Globals Vector - static final int GLOBAL_TIMESTAMP=0; - static final int GLOBAL_FEES=1; - static final int GLOBAL_JUICE_PRICE=2; - - /** - * An empty State - */ - public static final State EMPTY = create(Vectors.empty(), BlobMaps.empty(), Constants.INITIAL_GLOBALS, - BlobMaps.empty()); - - private static final Logger log = LoggerFactory.getLogger(State.class.getName()); - - - // Note: we are embedding these directly in the State cell. - // TODO: check we aren't at risk of hitting max encoding size limits - - private final AVector accounts; - private final BlobMap peers; - private final AVector globals; - private final BlobMap> schedule; - - private State(AVector accounts, BlobMap peers, - AVector globals, BlobMap> schedule) { - super(FORMAT); - this.accounts = accounts; - this.peers = peers; - this.globals = globals; - this.schedule = schedule; - } - - @Override - public ACell get(ACell k) { - if (Keywords.ACCOUNTS.equals(k)) return accounts; - if (Keywords.PEERS.equals(k)) return peers; - if (Keywords.GLOBALS.equals(k)) return globals; - if (Keywords.SCHEDULE.equals(k)) return schedule; - return null; - } - - @Override - public int getRefCount() { - int rc=accounts.getRefCount(); - rc+=peers.getRefCount(); - rc+=globals.getRefCount(); - rc+=schedule.getRefCount(); - return rc; - } - - public Ref getRef(int i) { - if (i<0) throw new IndexOutOfBoundsException(i); - - { - int c=accounts.getRefCount(); - if (i accounts = (AVector) newVals[0]; - BlobMap peers = (BlobMap) newVals[1]; - AVector globals = (AVector) newVals[2]; - BlobMap> schedule = (BlobMap>) newVals[3]; - if ((this.accounts == accounts) && (this.peers == peers) && (this.globals == globals) - && (this.schedule == schedule)) { - return this; - } - return new State(accounts, peers, globals, schedule); - } - - /** - * Create a State - * @param accounts Accounts - * @param peers Peers - * @param globals Globals - * @param schedule Schedule (may be null) - * @return New State instance - */ - public static State create(AVector accounts, BlobMap peers, - AVector globals, BlobMap> schedule) { - return new State(accounts, peers, globals, schedule); - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=getTag(); - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = accounts.encode(bs,pos); - pos = peers.encode(bs,pos); - pos = globals.encode(bs,pos); - pos = schedule.encode(bs,pos); - return pos; - } - - @Override - public long getEncodingLength() { - long length=1; - length+=accounts.getEncodingLength(); - length+=peers.getEncodingLength(); - length+=globals.getEncodingLength(); - length+=schedule.getEncodingLength(); - return length; - } - - @Override - public int estimatedEncodingSize() { - int est=1; - est+=accounts.estimatedEncodingSize(); - est+=peers.estimatedEncodingSize(); - est+=globals.estimatedEncodingSize(); - est+=schedule.estimatedEncodingSize(); - return est; - } - - /** - * Reads a State from a ByteBuffer encoding. Assumes tag byte already read. - * - * @param bb ByteBuffer to decode from - * @return The State decoded - * @throws BadFormatException If a State could not be read - */ - public static State read(ByteBuffer bb) throws BadFormatException { - try { - AVector accounts = Format.read(bb); - BlobMap peers = Format.read(bb); - AVector globals = Format.read(bb); - BlobMap> schedule = Format.read(bb); - return create(accounts, peers, globals, schedule); - } catch (ClassCastException ex) { - throw new BadFormatException("Can't read state", ex); - } - } - - /** - * Get all Accounts in this State - * @return Vector of Accounts - */ - public AVector getAccounts() { - return accounts; - } - - /** - * Gets the map of Peers for this State - * - * @return A map of addresses to PeerStatus records - */ - public BlobMap getPeers() { - return peers; - } - - /** - * Gets the balance of a specific address, or null if the Address does not exist - * @param address Address to check - * @return Long balance, or null if Account does not exist - */ - public Long getBalance(Address address) { - AccountStatus acc = getAccount(address); - if (acc == null) return null; - return acc.getBalance(); - } - - public State withBalance(Address address, long newBalance) { - AccountStatus acc = getAccount(address); - if (acc == null) { - throw new Error("No account for " + address); - } else { - acc = acc.withBalance(newBalance); - } - return putAccount(address, acc); - } - - /** - * Block level state transition function - * - * Updates the state by applying a given block of transactions - * - * @param block Block to Apply - * @return The BlockResult from applying the given Block to this State - */ - public BlockResult applyBlock(Block block) { - Counters.applyBlock++; - State state = prepareBlock(block); - return state.applyTransactions(block); - } - - /** - * Apply state updates consistent with time advancing to a given timestamp - * @param b - * @return - */ - private State prepareBlock(Block b) { - State state = this; - AVector glbs = state.globals; - long ts=((CVMLong)glbs.get(0)).longValue(); - long bts = b.getTimeStamp(); - if (bts > ts) { - AVector newGlbs=glbs.assoc(0,CVMLong.create(bts)); - state = state.withGlobals(newGlbs); - } - - state = state.applyScheduledTransactions(b); - - return state; - } - - @SuppressWarnings("unchecked") - private State applyScheduledTransactions(Block b) { - long tcount = 0; - BlobMap> sched = this.schedule; - CVMLong timestamp = this.getTimeStamp(); - - // ArrayList to accumulate the transactions to apply. Null until we need it - ArrayList al = null; - - // walk schedule entries to determine how many there are - // and remove from the current schedule - // we can optimise bulk removal later - while (tcount < Constants.MAX_SCHEDULED_TRANSACTIONS_PER_BLOCK) { - if (sched.isEmpty()) break; - MapEntry> me = sched.entryAt(0); - ABlob key = me.getKey(); - long time = key.longValue(); - if (time > timestamp.longValue()) break; // exit if we are still in the future - AVector trans = me.getValue(); - long numScheduled = trans.count(); // number scheduled at this schedule timestamp - long take = Math.min(numScheduled, Constants.MAX_SCHEDULED_TRANSACTIONS_PER_BLOCK - tcount); - - // add scheduled transactions to arraylist - if (al == null) al = new ArrayList<>(); - for (long i = 0; i < take; i++) { - al.add(trans.get(i)); - } - // remove schedule entries taken. Delete key if no more entries remaining - trans = trans.subVector(take, numScheduled - take); - if (trans.isEmpty()) sched = sched.dissoc(key); - } - if (al==null) return this; // nothing to do if no transactions to execute - - // update state with amended schedule - State state = this.withSchedule(sched); - - // now apply the transactions! - int n = al.size(); - log.debug("Applying {} scheduled transactions",n); - for (int i = 0; i < n; i++) { - AVector st = (AVector) al.get(i); - Address origin = (Address) st.get(0); - AOp op = (AOp) st.get(1); - Context ctx; - try { - // TODO juice refund? - ctx = Context.createInitial(state, origin, Constants.MAX_TRANSACTION_JUICE); - ctx = ctx.run(op); - if (ctx.isExceptional()) { - // TODO: what to do here? probably ignore - // we maybe need to think about reporting scheduled results? - log.trace("Scheduled transaction error: {}", ctx.getExceptional()); - } else { - state = ctx.getState(); - log.trace("Scheduled transaction succeeded"); - } - } catch (Exception e) { - log.trace("Scheduled transaction failed: {}",e); - e.printStackTrace(); - } - - } - - return state; - } - - private State withSchedule(BlobMap> newSchedule) { - if (schedule == newSchedule) return this; - return new State(accounts, peers, globals, newSchedule); - } - - private State withGlobals(AVector newGlobals) { - if (newGlobals == globals) return this; - return new State(accounts, peers, newGlobals, schedule); - } - - private BlockResult applyTransactions(Block block) { - State state = this; - int blockLength = block.length(); - Result[] results = new Result[blockLength]; - - AVector> transactions = block.getTransactions(); - for (int i = 0; i < blockLength; i++) { - // SECURITY: catch-all exception handler. - try { - // extract the signed transaction from the block - SignedData signed = transactions.get(i); - - // execute the transaction using the *latest* state (not necessarily "this") - Context ctx = state.applyTransaction(signed); - - // record results and state update - results[i] = Result.fromContext(CVMLong.create(i),ctx); - state = ctx.getState(); - } catch (Throwable t) { - String msg= "Unexpected fatal exception applying transaction: "+t.toString(); - results[i] = Result.create(CVMLong.create(i), Strings.create(msg),ErrorCodes.UNEXPECTED); - t.printStackTrace(); - log.error(msg); - } - } - - // TODO: changes for complete block? - return BlockResult.create(state, results); - } - - - /** - * Applies a signed transaction to the State. - * - * SECURITY: Checks digital signature and correctness of account key - * - * @return Context containing the updated chain State (may be exceptional) - */ - private Context applyTransaction(SignedData signedTransaction) throws BadSignatureException { - // Extract transaction, performs signature check - ATransaction t=signedTransaction.getValue(); - Address addr=t.getAddress(); - AccountStatus as = getAccount(addr); - if (as==null) { - return Context.createFake(this).withError(ErrorCodes.NOBODY,"Transaction for non-existent Account: "+addr); - } else { - AccountKey key=as.getAccountKey(); - if (key==null) return Context.createFake(this).withError(ErrorCodes.NOBODY,"Transaction for account that is an Actor: "+addr); - if (!Utils.equals(key, signedTransaction.getAccountKey())) { - return Context.createFake(this).withError(ErrorCodes.SIGNATURE,"Signature not valid for Account: "+addr+" expected public key: "+key); - } - } - - Context ctx=applyTransaction(t); - return ctx; - } - - /** - * Applies a transaction to the State. - * - * There are three phases in application of a transaction: - *
    - *
  1. Preparation for accounting, with {@link #prepareTransaction(Address, ATransaction) prepareTransaction}
  2. - *
  3. Functional application of the transaction with ATransaction.apply(....)
  4. - *
  5. Completion of accounting, with completeTransaction
  6. - *
- * - * SECURITY: Assumes digital signature already checked. - * - * @param Type of transaction result - * @param t Transaction to apply - * @return Context containing the updated chain State (may be exceptional) - */ - public Context applyTransaction(ATransaction t) { - Address origin = t.getAddress(); - - try { - // Create prepared context (juice subtracted, sequence updated, transaction entry checks) - Context ctx = prepareTransaction(origin,t); - if (ctx.isExceptional()) { - // We hit some error while preparing transaction. Return context with no state change, - // i.e. before executing the transaction - return ctx; - } - - final long totalJuice = ctx.getJuice(); - - State preparedState=ctx.getState(); - - - // apply transaction. This may result in an error! - ctx = t.apply(ctx); - - // complete transaction - // NOTE: completeTransaction handles error cases as well - ctx = ctx.completeTransaction(preparedState, totalJuice); - - return ctx; - } catch (Throwable ex) { - // SECURITY: This should never happen! - // But catching right now to prevent CVM overall crash - StringWriter s=new StringWriter(); - ex.printStackTrace(new PrintWriter(s)); - String message=s.toString(); - Context fCtx=Context.createInitial(this, origin, 0); - fCtx=fCtx.withError(ErrorCodes.FATAL, message); - return fCtx; - } - } - - @SuppressWarnings("unchecked") - private Context prepareTransaction(Address origin,ATransaction t) { - // Pre-transaction state updates (persisted even if transaction fails) - AccountStatus account = getAccount(origin); - if (account == null) { - return (Context) Context.createFake(this).withError(ErrorCodes.NOBODY); - } - - // Update sequence number for target account - long sequence=t.getSequence(); - AccountStatus newAccount = account.updateSequence(sequence); - if (newAccount == null) { - return Context.createFake(this,origin).withError(ErrorCodes.SEQUENCE, "Received = "+sequence+" & Expected = "+(account.getSequence()+1)); - } - State preparedState = this.putAccount(origin, newAccount); - - // Create context with juice subtracted - Long maxJuice=t.getMaxJuice(); - long juiceLimit=Math.min(Constants.MAX_TRANSACTION_JUICE,(maxJuice==null)?account.getBalance():maxJuice); - Context ctx = Context.createInitial(preparedState, origin, juiceLimit); - return ctx; - } - - @Override - public boolean isCanonical() { - return true; - } - - /** - * Computes the weighted stake for each peer. Adds a single entry for the null - * key, containing the total stake - * - * @return Map of Stakes - */ - public HashMap computeStakes() { - HashMap hm = new HashMap<>(peers.size()); - Double totalStake = peers.reduceEntries((acc, e) -> { - double stake = (double) (e.getValue().getTotalStake()); - - // TODO: potential performance bottleneck from hashing? - hm.put(RT.ensureAccountKey(e.getKey()), stake); - return stake + acc; - }, 0.0); - hm.put(null, totalStake); - return hm; - } - - /** - * Updates the Accounts in this State - * @param newAccounts New Accounts vector - * @return Updated State - */ - public State withAccounts(AVector newAccounts) { - if (newAccounts == accounts) return this; - return create(newAccounts, peers,globals, schedule); - } - - /** - * Returns this state after updating the given account - * - * @param address Address of Account to update - * @param accountStatus New Account Status - * @return Updates State, or this state if Account was unchanged - */ - public State putAccount(Address address, AccountStatus accountStatus) { - long ix=address.longValue(); - long n=accounts.count(); - if (ix>n) { - throw new IndexOutOfBoundsException("Trying to add an account beyond accounts array at position: "+ix); - } - - AVector newAccounts; - if (ix==n) { - // adding a new account in next position - newAccounts=accounts.conj(accountStatus); - } else { - newAccounts = accounts.assoc(ix, accountStatus); - } - - return withAccounts(newAccounts); - } - - /** - * Gets the AccountStatus for a given account, or null if not found. - * - * @param target Address to look up. Must not be null - * @return The AccountStatus for the given account, or null. - */ - public AccountStatus getAccount(Address target) { - long ix=target.longValue(); - if ((ix<0)||(ix>=accounts.count())) return null; - return accounts.get(ix); - } - - /** - * Gets the environment for a given account, or null if not found. - * - * @param addr Address of account to obtain - * @return The environment of the given account, or null if not found. - */ - public AMap getEnvironment(Address addr) { - AccountStatus as = getAccount(addr); - if (as == null) return null; - return as.getEnvironment(); - } - - /** - * Updates the Peers in this State - * @param newPeers New Peer Map - * @return Updated State - */ - public State withPeers(BlobMap newPeers) { - if (peers == newPeers) return this; - return create(accounts, newPeers, globals, schedule); - } - - @Override - public byte getTag() { - return Tag.STATE; - } - - /** - * Deploys a new Actor in the current state. - * - * Returns the updated state. The actor will be the last Account. - * - * @return The updated state with the Actor deployed. - */ - public State tryAddActor() { - AccountStatus as = AccountStatus.createActor(); - AVector newAccounts = accounts.conj(as); - return withAccounts(newAccounts); - } - - /** - * Compute the total funds existing within this state. - * - * Should be constant! 1,000,000,000,000,000,000 in full deployment mode - * - * @return The total value of all funds - */ - public long computeTotalFunds() { - long total = accounts.reduce((Long acc,AccountStatus as) -> acc + as.getBalance(), (Long)0L); - total += peers.reduceValues((Long acc, PeerStatus ps) -> acc + ps.getTotalStake(), 0L); - total += getGlobalFees().longValue(); - return total; - } - - @Override - public void validate() throws InvalidDataException { - super.validate(); - } - - @Override - public void validateCell() throws InvalidDataException { - accounts.validateCell(); - peers.validateCell(); - globals.validateCell(); - schedule.validateCell(); - } - - /** - * Gets the current global timestamp from this state. - * - * @return The timestamp from this state. - */ - public CVMLong getTimeStamp() { - return (CVMLong) globals.get(GLOBAL_TIMESTAMP); - } - - /** - * Gets the current Juice price - * - * @return Juice Price - */ - public CVMLong getJuicePrice() { - return (CVMLong) globals.get(GLOBAL_JUICE_PRICE); - } - - /** - * Schedules an operation with the given timestamp and Op in this state - * - * @param time Timestamp at which to execute the scheduled op - * - * @param address AccountAddress to schedule op for - * @param op Op to execute in schedule - * @return The updated State - */ - public State scheduleOp(long time, Address address, AOp op) { - AVector v = Vectors.of(address, op); - - LongBlob key = LongBlob.create(time); - AVector list = schedule.get(key); - if (list == null) { - list = Vectors.of(v); - } else { - list = list.append(v); - } - BlobMap> newSchedule = schedule.assoc(key, list); - - return this.withSchedule(newSchedule); - } - - /** - * Gets the current schedule data structure for this state - * - * @return The schedule data structure. - */ - public BlobMap> getSchedule() { - return schedule; - } - - /** - * Gets the Global Fees accumulated in the State - * @return Global Fees - */ - public CVMLong getGlobalFees() { - return (CVMLong) globals.get(GLOBAL_FEES); - } - - /** - * Update Global Fees - * @param newFees New Fees - * @return Updated State - */ - public State withGlobalFees(CVMLong newFees) { - return withGlobals(globals.assoc(GLOBAL_FEES,newFees)); - } - - - /** - * Gets the PeerStatus record for the given Address, or null if it does not - * exist - * - * @param peerAddress Address of Peer to check - * @return PeerStatus - */ - public PeerStatus getPeer(AccountKey peerAddress) { - return getPeers().get(peerAddress); - } - - /** - * Updates the specified peer status - * - * @param peerKey Peer Key - * @param updatedPeer New Peer Status - * @return Updated state - */ - public State withPeer(AccountKey peerKey, PeerStatus updatedPeer) { - return withPeers(peers.assoc(peerKey, updatedPeer)); - } - - /** - * Gets the next available address for allocation, i.e. the lowest Address - * that does not yet exist in this State. - * - * @return Next address available - */ - public Address nextAddress() { - return Address.create(accounts.count()); - } - - /** - * Look up an Address from CNS - * @param name CNS name String - * @return Address from CNS, or null if not found - */ - public Address lookupCNS(String name) { - Context ctx=Context.createFake(this); - return (Address) ctx.lookupCNS(name).getResult(); - } - - /** - * Gets globals. - * - * @return Vector of global values - */ - public AVector getGlobals() { - return globals; - } - - /** - * Updates the State with a new timestamp - * @param timestamp New timestamp - * @return Updated State - */ - public State withTimestamp(long timestamp) { - return withGlobals(globals.assoc(GLOBAL_TIMESTAMP, CVMLong.create(timestamp))); - } - - @Override - public boolean equals(AMap a) { - if (this == a) return true; // important optimisation for e.g. hashmap equality - if (a == null) return false; - if (a.getTag()!=getTag()) return false; - State as=(State)a; - return equals(as); - } - - /** - * Tests if this State is equal to another - * @param a State to compare with - * @return true if equal, false otherwise - */ - public boolean equals(State a) { - if (a == null) return false; - Hash h=this.cachedHash(); - if (h!=null) { - Hash ha=a.cachedHash(); - if (ha!=null) return Utils.equals(h, ha); - } - - if (!(Utils.equals(accounts, a.accounts))) return false; - if (!(Utils.equals(globals, a.globals))) return false; - if (!(Utils.equals(peers, a.peers))) return false; - if (!(Utils.equals(schedule, a.schedule))) return false; - return true; - } - -} diff --git a/convex-core/src/main/java/convex/core/crypto/AKeyPair.java b/convex-core/src/main/java/convex/core/crypto/AKeyPair.java deleted file mode 100644 index 8e0777c5a..000000000 --- a/convex-core/src/main/java/convex/core/crypto/AKeyPair.java +++ /dev/null @@ -1,117 +0,0 @@ -package convex.core.crypto; - -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.PublicKey; - -import convex.core.data.ACell; -import convex.core.data.AccountKey; -import convex.core.data.Blob; -import convex.core.data.Hash; -import convex.core.data.SignedData; - -/** - * Abstract base class for key pairs in Convex. - * - * Intended as a lightweight container for underlying crypto primitives. - */ -public abstract class AKeyPair { - - /** - * Gets the Account Public Key of this KeyPair - * @return AccountKey for this KeyPair - */ - public abstract AccountKey getAccountKey(); - - /** - * Gets the Private key encoded as a Blob - * @return Blob Private key data encoding - */ - public abstract Blob getEncodedPrivateKey(); - - /** - * Signs a value with this key pair - * @param Type of Value - * @param value Value to sign. Can be any valid CVM value. - * @return Signed Data Object - */ - public abstract SignedData signData(R value); - - @Override - public abstract boolean equals(Object a); - - /** - * Signs a hash value with this key pair, producing a signature of the appropriate type. - * @param hash Hash of value to sign - * @return A Signature compatible with the key pair. - */ - public abstract ASignature sign(Hash hash); - - /** - * Create a deterministic key pair with the given seed. - * - * SECURITY: Never use this for valuable keys or real assets: intended for deterministic testing only. - * @param seed Any long value. The same seed will produce the same key pair. - * @return New key pair - */ - public static AKeyPair createSeeded(long seed) { - return Ed25519KeyPair.createSeeded(seed); - } - - /** - * Create a key pair with the given Address and encoded private key - * - * @param publicKey Public Key - * @param encodedPrivateKey Encoded private key - * @return New key pair - */ - public static AKeyPair create(AccountKey publicKey, Blob encodedPrivateKey) { - return Ed25519KeyPair.create(publicKey,encodedPrivateKey); - } - - static { - Providers.init(); - } - - /** - * Generates a new, secure random key pair. Uses a Java SecureRandom instance. - * - * @return New Key Pair instance. - */ - public static AKeyPair generate() { - return Ed25519KeyPair.generate(); - } - - /** - * Creates a key pair using specific key material. - * - * @param keyMaterial Bytes to use as key - * @return New key pair - */ - public static AKeyPair create(byte[] keyMaterial) { - return Ed25519KeyPair.create(keyMaterial); - } - - /** - * Gets the JCA PrivateKey - * @return Private Key - */ - public abstract PrivateKey getPrivate(); - - /** - * Gets the JCA PublicKey - * @return Public Key - */ - public abstract PublicKey getPublic(); - - @Override - public String toString() { - return getAccountKey()+":"+getEncodedPrivateKey(); - } - - /** - * Gets the JCA representation of this Key Pair - * @return JCA KepPair - */ - public abstract KeyPair getJCAKeyPair(); -} diff --git a/convex-core/src/main/java/convex/core/crypto/ASignature.java b/convex-core/src/main/java/convex/core/crypto/ASignature.java deleted file mode 100644 index 6247c727b..000000000 --- a/convex-core/src/main/java/convex/core/crypto/ASignature.java +++ /dev/null @@ -1,90 +0,0 @@ -package convex.core.crypto; - -import java.nio.ByteBuffer; - -import convex.core.data.ABlob; -import convex.core.data.ACell; -import convex.core.data.AccountKey; -import convex.core.data.Blob; -import convex.core.data.Tag; -import convex.core.exceptions.BadFormatException; -import convex.core.util.Utils; - -/** - * Class representing a cryptographic signature - */ -public abstract class ASignature extends ACell { - - /** - * Checks if the signature is valid for a given message hash - * @param message Message to verify - * @param publicKey Public key of signer - * @return True if signature is valid, false otherwise - */ - public abstract boolean verify(ABlob message, AccountKey publicKey); - - /** - * Reads a Signature from the given ByteBuffer. Assumes tag byte already read. - * - * Uses Ed25519 - * - * @param bb ByteBuffer to read from - * @return Signature instance - * @throws BadFormatException If encoding is invalid - */ - public static ASignature read(ByteBuffer bb) throws BadFormatException { - return Ed25519Signature.read(bb); - } - - /** - * Gets the content of this Signature as a hex string - * @return Hex String representation of Signature - */ - public abstract String toHexString(); - - /** - * Construct a Signature from a hex string - * - * Uses Ed25519 - * - * @param hex Hex String to read from - * @return Signature instance - */ - public static ASignature fromHex(String hex) { - byte[] bs=Utils.hexToBytes(hex); - return Ed25519Signature.wrap(bs); - } - - /** - * Construct a Signature from a Blob - * - * Uses Ed25519 - * - * @param sigData Blob of data representing raw signature - * @return Signature instance - */ - public static ASignature fromBlob(Blob sigData) { - byte[] bs=sigData.getBytes(); - return Ed25519Signature.wrap(bs); - } - - @Override - public boolean isEmbedded() { - return true; - } - - @Override - public byte getTag() { - return Tag.SIGNATURE; - } - - /** - * Gets a Blob containing the raw bytes of this digital signature - * - * @return Blob containing signature bytes - */ - protected abstract ABlob getSignatureBlob(); - - - -} diff --git a/convex-core/src/main/java/convex/core/crypto/Ed25519KeyPair.java b/convex-core/src/main/java/convex/core/crypto/Ed25519KeyPair.java deleted file mode 100644 index 111f042f0..000000000 --- a/convex-core/src/main/java/convex/core/crypto/Ed25519KeyPair.java +++ /dev/null @@ -1,336 +0,0 @@ -package convex.core.crypto; - -import java.io.IOException; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.SecureRandom; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; - -import org.bouncycastle.asn1.DEROctetString; -import org.bouncycastle.asn1.edec.EdECObjectIdentifiers; -import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; -import org.bouncycastle.asn1.x509.AlgorithmIdentifier; -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; -import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters; -import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; - -import convex.core.data.ACell; -import convex.core.data.AccountKey; -import convex.core.data.Blob; -import convex.core.data.Hash; -import convex.core.data.SignedData; -import convex.core.util.Utils; - -/** - * Class representing an Ed25519 Key Pair - */ -public class Ed25519KeyPair extends AKeyPair { - private static final int SECRET_LENGTH=64; - private static final int SEED_LENGTH=32; - - private final AccountKey publicKey; - private KeyPair keyPair=null; - private final Blob seed; - - private final byte[] secretKeyBytes; - - private static final String ED25519 = "Ed25519"; - - private Ed25519KeyPair(AccountKey pk, Blob seed, byte[] skBytes) { - this.publicKey=pk; - this.seed=seed; - this.secretKeyBytes=skBytes; - } - - public static Ed25519KeyPair create(Blob seed) { - if (seed.count() != SEED_LENGTH) throw new IllegalArgumentException("256 bit private key material expected as seed!"); - - byte[] secretKeyBytes=new byte[SECRET_LENGTH]; - byte[] pkBytes=new byte[AccountKey.LENGTH]; - Providers.SODIUM_SIGN.cryptoSignSeedKeypair(pkBytes, secretKeyBytes, seed.getBytes()); - AccountKey publicKey=AccountKey.wrap(pkBytes); - return new Ed25519KeyPair(publicKey,seed,secretKeyBytes); - } - - /** - * Generates a new, secure random key pair. Uses a Java SecureRandom instance. - * - * @return New Key Pair instance. - */ - public static Ed25519KeyPair generate() { - return generate(new SecureRandom()); - } - - /** - * Create a KeyPair from a JCA KeyPair - * @param keyPair JCA KeyPair - * @return AKeyPair instance - */ - protected static Ed25519KeyPair create(KeyPair keyPair) { - Blob seed=extractSeed(keyPair.getPrivate()); - return create(seed); - } - - private static Blob extractSeed(PrivateKey private1) { - byte[] data=private1.getEncoded(); - int n=data.length; - Blob seed=Blob.wrap(data,n-SEED_LENGTH,SEED_LENGTH); - return seed; - } - - /** - * Creates an Ed25519 Key Pair with the specified keys - * @param publicKey Public key - * @param privateKey Private key - * @return Key Pair instance - */ - public static Ed25519KeyPair create(PublicKey publicKey, PrivateKey privateKey) { - KeyPair keyPair=new KeyPair(publicKey,privateKey); - return create(keyPair); - } - - /** - * Create a key pair given a public AccountKey and a encoded Blob - * @param accountKey Public Key - * @param encodedPrivateKey Encoded PKCS8 Private key - * @return AKeyPair instance - */ - public static Ed25519KeyPair create(AccountKey accountKey, Blob encodedPrivateKey) { - PublicKey publicKey= publicKeyFromBytes(accountKey.getBytes()); - PrivateKey privateKey=privateKeyFromBlob(encodedPrivateKey); - return create(publicKey,privateKey); - } - - public Blob getSeed() { - return seed; - } - - /** - * Generates a secure random key pair - * @param random A secure random instance - * @return New key pair - */ - public static Ed25519KeyPair generate(SecureRandom random) { - Blob seed=Blob.createRandom(random, 32); - return create(seed); - } - - /** - * Create a deterministic key pair with a specified seed. - * - * SECURITY: Use for testing purpose only - * @param seed See to use for generation - * @return Key Pair instance - */ - public static Ed25519KeyPair createSeeded(long seed) { - SecureRandom r = new InsecureRandom(seed); - Blob seedBlob=Blob.createRandom(r, 32); - return create(seedBlob); - } - - /** - * Create a SignKeyPair from given private key material. Public key is generated - * automatically from the private key - * - * @param keyMaterial An array of 32 bytes of random material to use for private key - * @return A new key pair using the given private key - */ - public static Ed25519KeyPair create(byte[] keyMaterial) { - return create(Blob.create(keyMaterial)); - } - - /** - * Create a KeyPair from given private key. Public key is generated - * automatically from the private key - * - * @param privateKey An PrivateKey item for private key - * @return A new key pair using the given private key - */ - public static Ed25519KeyPair create(PrivateKey privateKey) { - Ed25519PrivateKeyParameters privateKeyParam = new Ed25519PrivateKeyParameters(privateKey.getEncoded(), 16); - Ed25519PublicKeyParameters publicKeyParam = privateKeyParam.generatePublicKey(); - PublicKey generatedPublicKey = publicKeyFromBytes(publicKeyParam.getEncoded()); - // PrivateKey generatedPrivateKey = privateFromBytes(privateKeyParam.getEncoded()); - return create(generatedPublicKey, privateKey); - } - - /** - * Extracts an Address from an Ed25519 public key - * @param publicKey Public key - * @return - */ - static AccountKey extractAccountKey(PublicKey publicKey) { - byte[] bytes=publicKey.getEncoded(); - int n=bytes.length; - // take the bytes at the end of the encoding - return AccountKey.wrap(bytes,n-AccountKey.LENGTH); - } - - /** - * Gets a Ed25519 Private Key from a 32-byte array. - * @param privKey - * @return - */ - static PrivateKey privateFromBytes(byte[] privKey) { - try { - KeyFactory keyFactory = KeyFactory.getInstance(ED25519); - PrivateKeyInfo privKeyInfo = new PrivateKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), new DEROctetString(privKey)); - - var pkcs8KeySpec = new PKCS8EncodedKeySpec(privKeyInfo.getEncoded()); - - PrivateKey result = keyFactory.generatePrivate(pkcs8KeySpec); - return result; - } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) { - throw Utils.sneakyThrow(e); - } - } - - @Override - public Blob getEncodedPrivateKey() { - return extractPrivateKey(getPrivate()); - } - - /** - * Extracts an Blob containing the private key data from an Ed25519 private key - * - * SECURITY: Be careful with this Blob! - * - * @param publicKey Public key - * @return - */ - static Blob extractPrivateKey(PrivateKey privateKey) { - byte[] bytes=privateKey.getEncoded(); - return Blob.wrap(bytes); - } - - /** - * Gets a byte array representation of the public key - * @return Bytes of public key - */ - public byte[] getPublicKeyBytes() { - return getAccountKey().getBytes(); - } - - private static PrivateKey privateKeyFromBlob(Blob encodedKey) { - try { - KeyFactory keyFactory = KeyFactory.getInstance(ED25519); - PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(encodedKey.getBytes()); - PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec); - return privateKey; - } catch (Exception e) { - throw Utils.sneakyThrow(e); - } - } - - /** - * Creates a private key using the given raw bytes. - * @param key 32 bytes private key data - * @return Ed25519 Private Key instance - */ - public static PrivateKey privateKeyFromBytes(byte[] key) { - try { - KeyFactory keyFactory = KeyFactory.getInstance(ED25519); - PrivateKeyInfo privKeyInfo = new PrivateKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), - new DEROctetString(key)); - PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(privKeyInfo.getEncoded()); - PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec); - return privateKey; - } catch (Exception e) { - throw new Error(e); - } - } - - static PublicKey publicKeyFromBytes(byte[] key) { - try { - KeyFactory keyFactory = KeyFactory.getInstance(ED25519); - SubjectPublicKeyInfo pubKeyInfo = new SubjectPublicKeyInfo( - new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), key); - X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pubKeyInfo.getEncoded()); - PublicKey publicKey = keyFactory.generatePublic(x509KeySpec); - return publicKey; - } catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) { - throw new Error(e); - } - } - - @Override - public PublicKey getPublic() { - return getJCAKeyPair().getPublic(); - } - - @Override - public KeyPair getJCAKeyPair() { - if (keyPair==null) { - PublicKey pub=publicKeyFromBytes(publicKey.getBytes()); - PrivateKey priv=privateKeyFromBytes(seed.getBytes()); - keyPair=new KeyPair(pub,priv); - } - return keyPair; - } - - @Override - public PrivateKey getPrivate() { - return getJCAKeyPair().getPrivate(); - } - - @Override - public AccountKey getAccountKey() { - return publicKey; - } - - - @Override - public SignedData signData(R value) { - return SignedData.create(this, value); - } - - @Override - public ASignature sign(Hash hash) { - byte[] signature=new byte[Ed25519Signature.SIGNATURE_LENGTH]; - if (Providers.SODIUM_SIGN.cryptoSignDetached( - signature, - hash.getBytes(), - Hash.LENGTH, - getSecretKeyBytes())) {; - return Ed25519Signature.wrap(signature); - } else { - throw new Error("Signing failed!"); - } - -// try { -// Signature signer = Signature.getInstance(ED25519); -// signer.initSign(getPrivate()); -// signer.update(hash.getInternalArray(), hash.getInternalOffset(), Hash.LENGTH); -// byte[] signature = signer.sign(); -// return Ed25519Signature.wrap(signature); -// } catch (GeneralSecurityException e) { -// throw new Error(e); -// } - } - - /** - * Secret key bytes for LazySodium - * @return Private key byte array - */ - byte[] getSecretKeyBytes() { - return secretKeyBytes; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof Ed25519KeyPair)) return false; - return equals((Ed25519KeyPair) o); - } - - boolean equals(Ed25519KeyPair other) { - if (!this.seed.equals(other.seed)) return false; - if (!this.publicKey.equals(other.publicKey)) return false; - return true; - } - -} diff --git a/convex-core/src/main/java/convex/core/crypto/Ed25519Signature.java b/convex-core/src/main/java/convex/core/crypto/Ed25519Signature.java deleted file mode 100644 index a39234d34..000000000 --- a/convex-core/src/main/java/convex/core/crypto/Ed25519Signature.java +++ /dev/null @@ -1,141 +0,0 @@ -package convex.core.crypto; - -import java.nio.ByteBuffer; - -import convex.core.data.ABlob; -import convex.core.data.ACell; -import convex.core.data.AccountKey; -import convex.core.data.Blob; -import convex.core.data.Tag; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.util.Utils; - -/** - * Immutable dtata value class representing an Ed25519 digital signature. - */ -public class Ed25519Signature extends ASignature { - - /** - * Length in bytes of an Ed25519 signature - */ - public static final int SIGNATURE_LENGTH = 64; - - /** - * A Signature containing zerod bytes (not valid) - */ - public static final ASignature ZERO = wrap(new byte[SIGNATURE_LENGTH]); - - private final byte[] signatureBytes; - - private Ed25519Signature(byte[] signature) { - this.signatureBytes=signature; - } - - /** - * Creates a Signature instance with specific bytes - * @param signature Bytes for signature - * @return Signature instance - */ - public static Ed25519Signature wrap(byte[] signature) { - if (signature.length!=SIGNATURE_LENGTH) throw new IllegalArgumentException("Bsd signature length for ED25519"); - return new Ed25519Signature(signature); - } - - @Override - public boolean isCanonical() { - return true; - } - - @Override - public ACell toCanonical() { - return this; - } - - @Override public final boolean isCVMValue() { - return false; - } - - /** - * Read a signature from a ByteBuffer. Assumes tag already read. - * @param bb ByteBuffer to read from - * @return Signature instance - * @throws BadFormatException If encoding is invalid - */ - public static Ed25519Signature read(ByteBuffer bb) throws BadFormatException { - byte[] sigData=new byte[SIGNATURE_LENGTH]; - bb.get(sigData); - return wrap(sigData); - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.SIGNATURE; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - System.arraycopy(signatureBytes, 0, bs, pos, SIGNATURE_LENGTH); - return pos+SIGNATURE_LENGTH; - } - - @Override - public void print(StringBuilder sb) { - sb.append("{:signature 0x"+Utils.toHexString(signatureBytes)+"}"); - } - - //@Override - //public boolean verify(Hash hash, AccountKey address) { - // PublicKey pk=Ed25519KeyPair.publicKeyFromBytes(address.getBytes()); - // return verify(hash,pk); - //} - - @Override - public boolean verify(ABlob message, AccountKey address) { - boolean verified = Providers.SODIUM_SIGN.cryptoSignVerifyDetached(signatureBytes, message.getBytes(), (int)message.count(), address.getBytes()); - return verified; - } - -// private boolean verify(Hash hash, PublicKey publicKey) { -// try { -// Signature verifier = Signature.getInstance("Ed25519"); -// verifier.initVerify(publicKey); -// verifier.update(hash.getInternalArray(),hash.getOffset(),Hash.LENGTH); -// return verifier.verify(signatureBytes); -// } catch (SignatureException | InvalidKeyException e) { -// return false; -// } catch (NoSuchAlgorithmException e) { -// throw new Error(e); -// } -// } - - @Override - public void validateCell() throws InvalidDataException { - // TODO Auto-generated method stub - - } - - @Override - public int estimatedEncodingSize() { - return 1+SIGNATURE_LENGTH; - } - - @Override - public int getRefCount() { - return 0; - } - - @Override - public String toHexString() { - return Utils.toHexString(signatureBytes); - } - - @Override - public Blob getSignatureBlob() { - return Blob.wrap(signatureBytes); - } - - - -} diff --git a/convex-core/src/main/java/convex/core/crypto/Encoding.java b/convex-core/src/main/java/convex/core/crypto/Encoding.java deleted file mode 100644 index 0dd2d20d1..000000000 --- a/convex-core/src/main/java/convex/core/crypto/Encoding.java +++ /dev/null @@ -1,12 +0,0 @@ -package convex.core.crypto; - -/** - * Class for crypto encoding constants - */ -public class Encoding { - - /** - * Format for private keys - */ - public static final String PRIVATE_KEY_FORMAT="PKCS#8"; -} diff --git a/convex-core/src/main/java/convex/core/crypto/Hashing.java b/convex-core/src/main/java/convex/core/crypto/Hashing.java deleted file mode 100644 index 5b46c6b14..000000000 --- a/convex-core/src/main/java/convex/core/crypto/Hashing.java +++ /dev/null @@ -1,143 +0,0 @@ -package convex.core.crypto; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import org.bouncycastle.jcajce.provider.digest.Keccak; - -import convex.core.data.Hash; - -/** - * Class for static Hashing functionality - */ -public class Hashing { - - /** - * Computes the SHA-256 hash of a string - * - * @param message Message to hash (in UTF-8 encoding) - * @return Hash of UTF-8 encoded string - */ - public static Hash sha3(String message) { - return sha3(message.getBytes(StandardCharsets.UTF_8)); - } - - /** - * Computes the SHA3-256 hash of byte data - * - * @param data Byte array to hash - * @return SHA3-256 Hash value - */ - public static Hash sha3(byte[] data) { - MessageDigest md = getSHA3Digest(); - byte[] hash = md.digest(data); - return Hash.wrap(hash); - } - - /** - * Gets a thread-local instance of a SHA3-256 MessageDigest - * - * @return MessageDigest instance - */ - public static MessageDigest getSHA3Digest() { - return sha3Store.get(); - } - - /** - * Gets the Convex default MessageDigest. - * - * Guaranteed thread safe, will be either a new or ThreadLocal instance. - * - * @return MessageDigest - */ - public static MessageDigest getDigest() { - return getSHA3Digest(); - } - - /** - * Gets a MessageDigest for Keccak256. - * - * Guaranteed thread safe, will be either a new or ThreadLocal instance. - * - * @return MessageDigest - */ - public static MessageDigest getKeccak256Digest() { - // MessageDigest md= KECCAK_DIGEST.get(); - // md.reset(); - MessageDigest md = new Keccak.Digest256(); - return md; - } - - /** - * Gets a thread-local instance of a SHA256 MessageDigest - * - * @return MessageDigest instance - */ - public static MessageDigest getSHA256Digest() { - return sha256Store.get(); - } - - /** - * Computes the SHA3-256 hash of byte data - * - * @param data Byte array to hash - * @return SHA3-256 Hash value - */ - public static Hash sha256(byte[] data) { - MessageDigest md = getSHA256Digest(); - byte[] hash = md.digest(data); - return Hash.wrap(hash); - } - - /** - * Computes the SHA-256 hash of a string - * - * @param message Message to Hash (in UTF8 encoding) - * @return Hash of UTF-8 encoded string - */ - public static Hash sha256(String message) { - return sha256(message.getBytes(StandardCharsets.UTF_8)); - } - - /** - * Private store for thread-local MessageDigent objects. Avoids cost of - * recreating these every time they are needed. - */ - private static final ThreadLocal sha256Store; - /** - * Private store for thread-local MessageDigent objects. Avoids cost of - * recreating these every time they are needed. - */ - private static final ThreadLocal sha3Store; - static { - sha256Store = ThreadLocal.withInitial(() -> { - try { - return MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new Error("SHA-256 algorithm not available", e); - } - }); - - sha3Store = ThreadLocal.withInitial(() -> { - try { - return MessageDigest.getInstance("SHA3-256"); - } catch (NoSuchAlgorithmException e) { - throw new Error("SHA3-256 algorithm not available", e); - } - }); - } - /** - * Threadlocal store for MessageDigets instances. TODO: figure out if this is - * useful for performance. Probably not since digest initialisation is the - * bottleneck anyway? - */ - @SuppressWarnings("unused") - private static final ThreadLocal KECCAK_DIGEST = new ThreadLocal() { - @Override - protected MessageDigest initialValue() { - return new Keccak.Digest256(); - } - }; - -} diff --git a/convex-core/src/main/java/convex/core/crypto/InsecureRandom.java b/convex-core/src/main/java/convex/core/crypto/InsecureRandom.java deleted file mode 100644 index 5281cb816..000000000 --- a/convex-core/src/main/java/convex/core/crypto/InsecureRandom.java +++ /dev/null @@ -1,83 +0,0 @@ -package convex.core.crypto; - -import java.security.Provider; -import java.security.SecureRandom; -import java.security.SecureRandomSpi; -import java.util.Arrays; - -/** - * A SecureRandom instance that returns deterministic values given an initial seed. - * - * SECURITY: Neither secure nor truly random, but useful for testing. Please don't use for protecting real assets.... - * - */ -@SuppressWarnings("serial") -public class InsecureRandom extends SecureRandom { - - /** - * Create an InsecureRandom instance with a specified seed - * @param seed Seed value to use - */ - public InsecureRandom(long seed) { - super(new InsecureRandomSpi(seed), SECURITY_PROVIDER); - } - - private static final Provider SECURITY_PROVIDER = new InsecureRandomProvider(); - - /** - * Security provider instance used to register this random provider. - */ - private static class InsecureRandomProvider extends Provider { - private InsecureRandomProvider() { - super("InsecureRandom","0.01","Random number generator with deterministic values"); - } - } - - /** - * Actual work done with a SPI that extends java.security.SecureRandomSpi - */ - private static class InsecureRandomSpi extends SecureRandomSpi { - private long seed; - - private InsecureRandomSpi(long seed) { - this.seed=seed; - } - - private static long nextLong(long x) { - // This is a basic XORShift PRNG - x ^= (x << 21); - x ^= (x >>> 35); - x ^= (x << 4); - return x; - } - - private void initialise(byte[] seedBytes) { - this.seed=nextLong(Arrays.hashCode(seedBytes)); - } - - @Override - protected void engineSetSeed(byte[] seedBytes) { - initialise(seedBytes); - } - - @Override - protected void engineNextBytes(byte[] out) { - int n=out.length; - long x=seed; - for (int i=0; i>32); - x=nextLong(x); - } - seed=x; - } - - @Override - protected byte[] engineGenerateSeed(int length) { - byte[] newSeed = new byte[length]; - engineNextBytes(newSeed); - return newSeed; - } - } - - -} diff --git a/convex-core/src/main/java/convex/core/crypto/Mnemonic.java b/convex-core/src/main/java/convex/core/crypto/Mnemonic.java deleted file mode 100644 index 1b346777f..000000000 --- a/convex-core/src/main/java/convex/core/crypto/Mnemonic.java +++ /dev/null @@ -1,257 +0,0 @@ -package convex.core.crypto; - -import java.math.BigInteger; -import java.security.SecureRandom; -import java.util.HashMap; - -import convex.core.data.Blob; -import convex.core.data.Hash; -import convex.core.util.Utils; - -/** - * Static utility functions for Mnemonic encoding - */ -public class Mnemonic { - - // word list from https://tools.ietf.org/html/rfc1751 - private static final String[] WORDS = { "a", "abe", "ace", "act", "ad", "ada", "add", "ago", "aid", "aim", "air", - "all", "alp", "am", "amy", "an", "ana", "and", "ann", "ant", "any", "ape", "aps", "apt", "arc", "are", - "ark", "arm", "art", "as", "ash", "ask", "at", "ate", "aug", "auk", "ave", "awe", "awk", "awl", "awn", "ax", - "aye", "bad", "bag", "bah", "bam", "ban", "bar", "bat", "bay", "be", "bed", "bee", "beg", "ben", "bet", - "bey", "bib", "bid", "big", "bin", "bit", "bob", "bog", "bon", "boo", "bop", "bow", "boy", "bub", "bud", - "bug", "bum", "bun", "bus", "but", "buy", "by", "bye", "cab", "cal", "cam", "can", "cap", "car", "cat", - "caw", "cod", "cog", "col", "con", "coo", "cop", "cot", "cow", "coy", "cry", "cub", "cue", "cup", "cur", - "cut", "dab", "dad", "dam", "dan", "dar", "day", "dee", "del", "den", "des", "dew", "did", "die", "dig", - "din", "dip", "do", "doe", "dog", "don", "dot", "dow", "dry", "dub", "dud", "due", "dug", "dun", "ear", - "eat", "ed", "eel", "egg", "ego", "eli", "elk", "elm", "ely", "em", "end", "est", "etc", "eva", "eve", - "ewe", "eye", "fad", "fan", "far", "fat", "fay", "fed", "fee", "few", "fib", "fig", "fin", "fir", "fit", - "flo", "fly", "foe", "fog", "for", "fry", "fum", "fun", "fur", "gab", "gad", "gag", "gal", "gam", "gap", - "gas", "gay", "gee", "gel", "gem", "get", "gig", "gil", "gin", "go", "got", "gum", "gun", "gus", "gut", - "guy", "gym", "gyp", "ha", "had", "hal", "ham", "han", "hap", "has", "hat", "haw", "hay", "he", "hem", - "hen", "her", "hew", "hey", "hi", "hid", "him", "hip", "his", "hit", "ho", "hob", "hoc", "hoe", "hog", - "hop", "hot", "how", "hub", "hue", "hug", "huh", "hum", "hut", "i", "icy", "ida", "if", "ike", "ill", "ink", - "inn", "io", "ion", "iq", "ira", "ire", "irk", "is", "it", "its", "ivy", "jab", "jag", "jam", "jan", "jar", - "jaw", "jay", "jet", "jig", "jim", "jo", "job", "joe", "jog", "jot", "joy", "jug", "jut", "kay", "keg", - "ken", "key", "kid", "kim", "kin", "kit", "la", "lab", "lac", "lad", "lag", "lam", "lap", "law", "lay", - "lea", "led", "lee", "leg", "len", "leo", "let", "lew", "lid", "lie", "lin", "lip", "lit", "lo", "lob", - "log", "lop", "los", "lot", "lou", "low", "loy", "lug", "lye", "ma", "mac", "mad", "mae", "man", "mao", - "map", "mat", "maw", "may", "me", "meg", "mel", "men", "met", "mew", "mid", "min", "mit", "mob", "mod", - "moe", "moo", "mop", "mos", "mot", "mow", "mud", "mug", "mum", "my", "nab", "nag", "nan", "nap", "nat", - "nay", "ne", "ned", "nee", "net", "new", "nib", "nil", "nip", "nit", "no", "nob", "nod", "non", "nor", - "not", "nov", "now", "nu", "nun", "nut", "o", "oaf", "oak", "oar", "oat", "odd", "ode", "of", "off", "oft", - "oh", "oil", "ok", "old", "on", "one", "or", "orb", "ore", "orr", "os", "ott", "our", "out", "ova", "ow", - "owe", "owl", "own", "ox", "pa", "pad", "pal", "pam", "pan", "pap", "par", "pat", "paw", "pay", "pea", - "peg", "pen", "pep", "per", "pet", "pew", "phi", "pi", "pie", "pin", "pit", "ply", "po", "pod", "poe", - "pop", "pot", "pow", "pro", "pry", "pub", "pug", "pun", "pup", "put", "quo", "rag", "ram", "ran", "rap", - "rat", "raw", "ray", "reb", "red", "rep", "ret", "rib", "rid", "rig", "rim", "rio", "rip", "rob", "rod", - "roe", "ron", "rot", "row", "roy", "rub", "rue", "rug", "rum", "run", "rye", "sac", "sad", "sag", "sal", - "sam", "san", "sap", "sat", "saw", "say", "sea", "sec", "see", "sen", "set", "sew", "she", "shy", "sin", - "sip", "sir", "sis", "sit", "ski", "sky", "sly", "so", "sob", "sod", "son", "sop", "sow", "soy", "spa", - "spy", "sub", "sud", "sue", "sum", "sun", "sup", "tab", "tad", "tag", "tan", "tap", "tar", "tea", "ted", - "tee", "ten", "the", "thy", "tic", "tie", "tim", "tin", "tip", "to", "toe", "tog", "tom", "ton", "too", - "top", "tow", "toy", "try", "tub", "tug", "tum", "tun", "two", "un", "up", "us", "use", "van", "vat", "vet", - "vie", "wad", "wag", "war", "was", "way", "we", "web", "wed", "wee", "wet", "who", "why", "win", "wit", - "wok", "won", "woo", "wow", "wry", "wu", "yam", "yap", "yaw", "ye", "yea", "yes", "yet", "you", "abed", - "abel", "abet", "able", "abut", "ache", "acid", "acme", "acre", "acta", "acts", "adam", "adds", "aden", - "afar", "afro", "agee", "ahem", "ahoy", "aida", "aide", "aids", "airy", "ajar", "akin", "alan", "alec", - "alga", "alia", "ally", "alma", "aloe", "also", "alto", "alum", "alva", "amen", "ames", "amid", "ammo", - "amok", "amos", "amra", "andy", "anew", "anna", "anne", "ante", "anti", "aqua", "arab", "arch", "area", - "argo", "arid", "army", "arts", "arty", "asia", "asks", "atom", "aunt", "aura", "auto", "aver", "avid", - "avis", "avon", "avow", "away", "awry", "babe", "baby", "bach", "back", "bade", "bail", "bait", "bake", - "bald", "bale", "bali", "balk", "ball", "balm", "band", "bane", "bang", "bank", "barb", "bard", "bare", - "bark", "barn", "barr", "base", "bash", "bask", "bass", "bate", "bath", "bawd", "bawl", "bead", "beak", - "beam", "bean", "bear", "beat", "beau", "beck", "beef", "been", "beer", "beet", "bela", "bell", "belt", - "bend", "bent", "berg", "bern", "bert", "bess", "best", "beta", "beth", "bhoy", "bias", "bide", "bien", - "bile", "bilk", "bill", "bind", "bing", "bird", "bite", "bits", "blab", "blat", "bled", "blew", "blob", - "bloc", "blot", "blow", "blue", "blum", "blur", "boar", "boat", "boca", "bock", "bode", "body", "bogy", - "bohr", "boil", "bold", "bolo", "bolt", "bomb", "bona", "bond", "bone", "bong", "bonn", "bony", "book", - "boom", "boon", "boot", "bore", "borg", "born", "bose", "boss", "both", "bout", "bowl", "boyd", "brad", - "brae", "brag", "bran", "bray", "bred", "brew", "brig", "brim", "brow", "buck", "budd", "buff", "bulb", - "bulk", "bull", "bunk", "bunt", "buoy", "burg", "burl", "burn", "burr", "burt", "bury", "bush", "buss", - "bust", "busy", "byte", "cady", "cafe", "cage", "cain", "cake", "calf", "call", "calm", "came", "cane", - "cant", "card", "care", "carl", "carr", "cart", "case", "cash", "cask", "cast", "cave", "ceil", "cell", - "cent", "cern", "chad", "char", "chat", "chaw", "chef", "chen", "chew", "chic", "chin", "chou", "chow", - "chub", "chug", "chum", "cite", "city", "clad", "clam", "clan", "claw", "clay", "clod", "clog", "clot", - "club", "clue", "coal", "coat", "coca", "cock", "coco", "coda", "code", "cody", "coed", "coil", "coin", - "coke", "cola", "cold", "colt", "coma", "comb", "come", "cook", "cool", "coon", "coot", "cord", "core", - "cork", "corn", "cost", "cove", "cowl", "crab", "crag", "cram", "cray", "crew", "crib", "crow", "crud", - "cuba", "cube", "cuff", "cull", "cult", "cuny", "curb", "curd", "cure", "curl", "curt", "cuts", "dade", - "dale", "dame", "dana", "dane", "dang", "dank", "dare", "dark", "darn", "dart", "dash", "data", "date", - "dave", "davy", "dawn", "days", "dead", "deaf", "deal", "dean", "dear", "debt", "deck", "deed", "deem", - "deer", "deft", "defy", "dell", "dent", "deny", "desk", "dial", "dice", "died", "diet", "dime", "dine", - "ding", "dint", "dire", "dirt", "disc", "dish", "disk", "dive", "dock", "does", "dole", "doll", "dolt", - "dome", "done", "doom", "door", "dora", "dose", "dote", "doug", "dour", "dove", "down", "drab", "drag", - "dram", "draw", "drew", "drub", "drug", "drum", "dual", "duck", "duct", "duel", "duet", "duke", "dull", - "dumb", "dune", "dunk", "dusk", "dust", "duty", "each", "earl", "earn", "ease", "east", "easy", "eben", - "echo", "eddy", "eden", "edge", "edgy", "edit", "edna", "egan", "elan", "elba", "ella", "else", "emil", - "emit", "emma", "ends", "eric", "eros", "even", "ever", "evil", "eyed", "face", "fact", "fade", "fail", - "fain", "fair", "fake", "fall", "fame", "fang", "farm", "fast", "fate", "fawn", "fear", "feat", "feed", - "feel", "feet", "fell", "felt", "fend", "fern", "fest", "feud", "fief", "figs", "file", "fill", "film", - "find", "fine", "fink", "fire", "firm", "fish", "fisk", "fist", "fits", "five", "flag", "flak", "flam", - "flat", "flaw", "flea", "fled", "flew", "flit", "floc", "flog", "flow", "flub", "flue", "foal", "foam", - "fogy", "foil", "fold", "folk", "fond", "font", "food", "fool", "foot", "ford", "fore", "fork", "form", - "fort", "foss", "foul", "four", "fowl", "frau", "fray", "fred", "free", "fret", "frey", "frog", "from", - "fuel", "full", "fume", "fund", "funk", "fury", "fuse", "fuss", "gaff", "gage", "gail", "gain", "gait", - "gala", "gale", "gall", "galt", "game", "gang", "garb", "gary", "gash", "gate", "gaul", "gaur", "gave", - "gawk", "gear", "geld", "gene", "gent", "germ", "gets", "gibe", "gift", "gild", "gill", "gilt", "gina", - "gird", "girl", "gist", "give", "glad", "glee", "glen", "glib", "glob", "glom", "glow", "glue", "glum", - "glut", "goad", "goal", "goat", "goer", "goes", "gold", "golf", "gone", "gong", "good", "goof", "gore", - "gory", "gosh", "gout", "gown", "grab", "grad", "gray", "greg", "grew", "grey", "grid", "grim", "grin", - "grit", "grow", "grub", "gulf", "gull", "gunk", "guru", "gush", "gust", "gwen", "gwyn", "haag", "haas", - "hack", "hail", "hair", "hale", "half", "hall", "halo", "halt", "hand", "hang", "hank", "hans", "hard", - "hark", "harm", "hart", "hash", "hast", "hate", "hath", "haul", "have", "hawk", "hays", "head", "heal", - "hear", "heat", "hebe", "heck", "heed", "heel", "heft", "held", "hell", "helm", "herb", "herd", "here", - "hero", "hers", "hess", "hewn", "hick", "hide", "high", "hike", "hill", "hilt", "hind", "hint", "hire", - "hiss", "hive", "hobo", "hock", "hoff", "hold", "hole", "holm", "holt", "home", "hone", "honk", "hood", - "hoof", "hook", "hoot", "horn", "hose", "host", "hour", "hove", "howe", "howl", "hoyt", "huck", "hued", - "huff", "huge", "hugh", "hugo", "hulk", "hull", "hunk", "hunt", "hurd", "hurl", "hurt", "hush", "hyde", - "hymn", "ibis", "icon", "idea", "idle", "iffy", "inca", "inch", "into", "ions", "iota", "iowa", "iris", - "irma", "iron", "isle", "itch", "item", "ivan", "jack", "jade", "jail", "jake", "jane", "java", "jean", - "jeff", "jerk", "jess", "jest", "jibe", "jill", "jilt", "jive", "joan", "jobs", "jock", "joel", "joey", - "john", "join", "joke", "jolt", "jove", "judd", "jude", "judo", "judy", "juju", "juke", "july", "june", - "junk", "juno", "jury", "just", "jute", "kahn", "kale", "kane", "kant", "karl", "kate", "keel", "keen", - "keno", "kent", "kern", "kerr", "keys", "kick", "kill", "kind", "king", "kirk", "kiss", "kite", "klan", - "knee", "knew", "knit", "knob", "knot", "know", "koch", "kong", "kudo", "kurd", "kurt", "kyle", "lace", - "lack", "lacy", "lady", "laid", "lain", "lair", "lake", "lamb", "lame", "land", "lane", "lang", "lard", - "lark", "lass", "last", "late", "laud", "lava", "lawn", "laws", "lays", "lead", "leaf", "leak", "lean", - "lear", "leek", "leer", "left", "lend", "lens", "lent", "leon", "lesk", "less", "lest", "lets", "liar", - "lice", "lick", "lied", "lien", "lies", "lieu", "life", "lift", "like", "lila", "lilt", "lily", "lima", - "limb", "lime", "lind", "line", "link", "lint", "lion", "lisa", "list", "live", "load", "loaf", "loam", - "loan", "lock", "loft", "loge", "lois", "lola", "lone", "long", "look", "loon", "loot", "lord", "lore", - "lose", "loss", "lost", "loud", "love", "lowe", "luck", "lucy", "luge", "luke", "lulu", "lund", "lung", - "lura", "lure", "lurk", "lush", "lust", "lyle", "lynn", "lyon", "lyra", "mace", "made", "magi", "maid", - "mail", "main", "make", "male", "mali", "mall", "malt", "mana", "mann", "many", "marc", "mare", "mark", - "mars", "mart", "mary", "mash", "mask", "mass", "mast", "mate", "math", "maul", "mayo", "mead", "meal", - "mean", "meat", "meek", "meet", "meld", "melt", "memo", "mend", "menu", "mert", "mesh", "mess", "mice", - "mike", "mild", "mile", "milk", "mill", "milt", "mimi", "mind", "mine", "mini", "mink", "mint", "mire", - "miss", "mist", "mite", "mitt", "moan", "moat", "mock", "mode", "mold", "mole", "moll", "molt", "mona", - "monk", "mont", "mood", "moon", "moor", "moot", "more", "morn", "mort", "moss", "most", "moth", "move", - "much", "muck", "mudd", "muff", "mule", "mull", "murk", "mush", "must", "mute", "mutt", "myra", "myth", - "nagy", "nail", "nair", "name", "nary", "nash", "nave", "navy", "neal", "near", "neat", "neck", "need", - "neil", "nell", "neon", "nero", "ness", "nest", "news", "newt", "nibs", "nice", "nick", "nile", "nina", - "nine", "noah", "node", "noel", "noll", "none", "nook", "noon", "norm", "nose", "note", "noun", "nova", - "nude", "null", "numb", "oath", "obey", "oboe", "odin", "ohio", "oily", "oint", "okay", "olaf", "oldy", - "olga", "olin", "oman", "omen", "omit", "once", "ones", "only", "onto", "onus", "oral", "orgy", "oslo", - "otis", "otto", "ouch", "oust", "outs", "oval", "oven", "over", "owly", "owns", "quad", "quit", "quod", - "race", "rack", "racy", "raft", "rage", "raid", "rail", "rain", "rake", "rank", "rant", "rare", "rash", - "rate", "rave", "rays", "read", "real", "ream", "rear", "reck", "reed", "reef", "reek", "reel", "reid", - "rein", "rena", "rend", "rent", "rest", "rice", "rich", "rick", "ride", "rift", "rill", "rime", "ring", - "rink", "rise", "risk", "rite", "road", "roam", "roar", "robe", "rock", "rode", "roil", "roll", "rome", - "rood", "roof", "rook", "room", "root", "rosa", "rose", "ross", "rosy", "roth", "rout", "rove", "rowe", - "rows", "rube", "ruby", "rude", "rudy", "ruin", "rule", "rung", "runs", "runt", "ruse", "rush", "rusk", - "russ", "rust", "ruth", "sack", "safe", "sage", "said", "sail", "sale", "salk", "salt", "same", "sand", - "sane", "sang", "sank", "sara", "saul", "save", "says", "scan", "scar", "scat", "scot", "seal", "seam", - "sear", "seat", "seed", "seek", "seem", "seen", "sees", "self", "sell", "send", "sent", "sets", "sewn", - "shag", "sham", "shaw", "shay", "shed", "shim", "shin", "shod", "shoe", "shot", "show", "shun", "shut", - "sick", "side", "sift", "sigh", "sign", "silk", "sill", "silo", "silt", "sine", "sing", "sink", "sire", - "site", "sits", "situ", "skat", "skew", "skid", "skim", "skin", "skit", "slab", "slam", "slat", "slay", - "sled", "slew", "slid", "slim", "slit", "slob", "slog", "slot", "slow", "slug", "slum", "slur", "smog", - "smug", "snag", "snob", "snow", "snub", "snug", "soak", "soar", "sock", "soda", "sofa", "soft", "soil", - "sold", "some", "song", "soon", "soot", "sore", "sort", "soul", "sour", "sown", "stab", "stag", "stan", - "star", "stay", "stem", "stew", "stir", "stow", "stub", "stun", "such", "suds", "suit", "sulk", "sums", - "sung", "sunk", "sure", "surf", "swab", "swag", "swam", "swan", "swat", "sway", "swim", "swum", "tack", - "tact", "tail", "take", "tale", "talk", "tall", "tank", "task", "tate", "taut", "teal", "team", "tear", - "tech", "teem", "teen", "teet", "tell", "tend", "tent", "term", "tern", "tess", "test", "than", "that", - "thee", "them", "then", "they", "thin", "this", "thud", "thug", "tick", "tide", "tidy", "tied", "tier", - "tile", "till", "tilt", "time", "tina", "tine", "tint", "tiny", "tire", "toad", "togo", "toil", "told", - "toll", "tone", "tong", "tony", "took", "tool", "toot", "tore", "torn", "tote", "tour", "tout", "town", - "trag", "tram", "tray", "tree", "trek", "trig", "trim", "trio", "trod", "trot", "troy", "true", "tuba", - "tube", "tuck", "tuft", "tuna", "tune", "tung", "turf", "turn", "tusk", "twig", "twin", "twit", "ulan", - "unit", "urge", "used", "user", "uses", "utah", "vail", "vain", "vale", "vary", "vase", "vast", "veal", - "veda", "veil", "vein", "vend", "vent", "verb", "very", "veto", "vice", "view", "vine", "vise", "void", - "volt", "vote", "wack", "wade", "wage", "wail", "wait", "wake", "wale", "walk", "wall", "walt", "wand", - "wane", "wang", "want", "ward", "warm", "warn", "wart", "wash", "wast", "wats", "watt", "wave", "wavy", - "ways", "weak", "weal", "wean", "wear", "weed", "week", "weir", "weld", "well", "welt", "went", "were", - "wert", "west", "wham", "what", "whee", "when", "whet", "whoa", "whom", "wick", "wife", "wild", "will", - "wind", "wine", "wing", "wink", "wino", "wire", "wise", "wish", "with", "wolf", "wont", "wood", "wool", - "word", "wore", "work", "worm", "worn", "wove", "writ", "wynn", "yale", "yang", "yank", "yard", "yarn", - "yawl", "yawn", "yeah", "year", "yell", "yoga", "yoke" }; - - private static final HashMap CODES = buildCodes(); - - /** - * Encode bytes as a mnemonic string - * - * @param data Byte array to encode - * @return Mnemonic String - */ - public static String encode(byte[] data) { - int bitLength = data.length * 8; - int n = (bitLength + 10) / 11; - String[] words = new String[n]; - for (int i = 0; i < n; i++) { - // extract 11 bits for each word - int bits = 0x7FF & Utils.extractBits(data, 11, i * 11); - words[i] = WORDS[bits]; - } - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < n; i++) { - if (i > 0) sb.append(' '); - sb.append(words[i]); - } - return sb.toString(); - } - - /** - * Encode bytes as a mnemonic string - * - * @param x Bytes to encode - * @param bitLength Length of key to encode - * @return Mnemonic String - */ - public static String encode(BigInteger x, int bitLength) { - return Mnemonic.encode(Utils.hexToBytes(Utils.toHexString(x, bitLength >> 2))); - } - - private static HashMap buildCodes() { - HashMap hm = new HashMap<>(3000); // big enough to avoid resize - for (int i = 0; i < WORDS.length; i++) { - hm.put(WORDS[i], i); - } - return hm; - } - - /** - * Decode from a Mnemonic string - * @param phrase Mnemonic string - * @param bitLength Bits to extract - * @return Decoded byte array - */ - public static byte[] decode(String phrase, int bitLength) { - int nByte = (bitLength + 7) / 8; - byte[] result = new byte[nByte]; - - phrase = phrase.trim().toLowerCase(); - String[] words = phrase.split("\\s+"); - int n = words.length; - if (n * 11 < bitLength) - throw new IllegalArgumentException("Insufficient words (" + n + ") to cover bitlength of " + bitLength); - - for (int i = 0; i < n; i++) { - String word = words[i]; - Integer x = CODES.get(word); - if (x == null) throw new IllegalArgumentException( - "Can't find word (" + word + ") in mnemonic dictionary for phrase " + phrase); - Utils.setBits(result, 11, i * 11, x); - } - - return result; - } - - /** - * Create a secure random mnemonic string - * - * @return Mnemonic String - */ - public static String createSecureRandom() { - byte[] bs = Blob.createRandom(new SecureRandom(), 16).getBytes(); - return encode(bs); - } - - public static AKeyPair decodeKeyPair(String s) { - byte[] bs = Mnemonic.decode(s, 128); - Hash h = Blob.wrap(bs).getContentHash(); - Ed25519KeyPair kp = Ed25519KeyPair.create(h.getBytes()); - return kp; - } -} diff --git a/convex-core/src/main/java/convex/core/crypto/PBE.java b/convex-core/src/main/java/convex/core/crypto/PBE.java deleted file mode 100644 index 3cb3ebaff..000000000 --- a/convex-core/src/main/java/convex/core/crypto/PBE.java +++ /dev/null @@ -1,34 +0,0 @@ -package convex.core.crypto; - -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; - -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; - -public class PBE { - - /** - * Gets a key of the given length (in bits) from a password using key derivation - * function - * - * @param password Password stored in a char array. - * @param salt Salt bytes - * @param bitLength Bit length - * @return Decrypted key - */ - public static byte[] deriveKey(char[] password, byte[] salt, int bitLength) { - try { - SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - PBEKeySpec pbeKeySpec = new PBEKeySpec(password, salt, 1000, bitLength); - SecretKey secretKey = factory.generateSecret(pbeKeySpec); - int byteLen = bitLength / 8; - byte[] key = new byte[byteLen]; - System.arraycopy(secretKey.getEncoded(), 0, key, 0, byteLen); - return key; - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - throw new Error(e); - } - } -} diff --git a/convex-core/src/main/java/convex/core/crypto/PEMTools.java b/convex-core/src/main/java/convex/core/crypto/PEMTools.java deleted file mode 100644 index d817ceabc..000000000 --- a/convex-core/src/main/java/convex/core/crypto/PEMTools.java +++ /dev/null @@ -1,163 +0,0 @@ -package convex.core.crypto; - -import java.io.IOException; -import java.io.StringReader; -import java.io.StringWriter; -import java.security.GeneralSecurityException; -import java.security.KeyFactory; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.Security; -import java.security.spec.PKCS8EncodedKeySpec; -import java.util.Base64; - -import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.openssl.PKCS8Generator; -import org.bouncycastle.openssl.PEMParser; -import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator; -import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder; -import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder; -import org.bouncycastle.openssl.jcajce.JcaPEMWriter; -import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; -import org.bouncycastle.operator.InputDecryptorProvider; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.operator.OutputEncryptor; -import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; -import org.bouncycastle.pkcs.PKCSException; -import org.bouncycastle.util.io.pem.PemObject; - -public class PEMTools { - // private static String encryptionAlgorithm="AES-128-CBC"; - - /** - * Writes a key pair to a String - * @param kp Key pair to write - * @return PEM String representation of key pair - */ - public static String writePEM(AKeyPair kp) { - - PrivateKey priv=kp.getPrivate(); - // PublicKey pub=kp.getPublic(); - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(priv.getEncoded()); - - byte[] encoded=keySpec.getEncoded(); - String base64=Base64.getEncoder().encodeToString(encoded); - - StringBuilder sb=new StringBuilder(); - sb.append("-----BEGIN PRIVATE KEY-----"); - sb.append(System.lineSeparator()); - sb.append(base64); - sb.append(System.lineSeparator()); - sb.append("-----END PRIVATE KEY-----"); - String pem=sb.toString(); - return pem; - } - - /** - * Read a key pair from a PEM String - * @param pem PEM String - * @return Key pair instance - * @throws GeneralSecurityException If a security error occurs - */ - public static AKeyPair readPEM(String pem) throws GeneralSecurityException { - String publicKeyPEM = pem.trim() - .replace("-----BEGIN PRIVATE KEY-----", "") - .replaceAll(System.lineSeparator(), "") - .replace("-----END PRIVATE KEY-----", ""); - - byte[] bs = Base64.getDecoder().decode(publicKeyPEM); - - KeyFactory keyFactory = KeyFactory.getInstance("Ed25519"); - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bs); - PrivateKey priv=keyFactory.generatePrivate(keySpec); - PublicKey pub=keyFactory.generatePublic(keySpec); - return Ed25519KeyPair.create(pub, priv); - } - - /** - * Encrypt a priavte key into a PEM formated text - * - * @param privateKey Private key to encrypt - * - * @param password Password to use for encryption - * - * @return PEM text that can be saved or sent to another keystore - * - * @throws Error Any encryption error that occurs - */ - public static String encryptPrivateKeyToPEM(PrivateKey privateKey, char[] password) throws Error { - StringWriter stringWriter = new StringWriter(); - JcaPEMWriter writer = new JcaPEMWriter(stringWriter); - JceOpenSSLPKCS8EncryptorBuilder builder = new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.AES_256_CBC); - builder.setPassword(password); - try { - OutputEncryptor encryptor = builder.build(); - JcaPKCS8Generator generator = new JcaPKCS8Generator(privateKey, encryptor); - writer.writeObject(generator); - writer.close(); - } catch (IOException | OperatorCreationException e) { - throw new Error("cannot encrypt private key to PEM: " + e); - } - return stringWriter.toString(); - } - - /** - * Decrypt a PEM string to a private key. The PEM string must contain the "ENCRYPTED PRIVATE KEY" type. - * - * @param PEM PEM string to decode - * - * @param password Password that was used to encrypt the priavte key - * - * @return PrivateKey stored in the PEM - * - * @throws Error on reading the PEM, decryption and decoding the private key - */ - public static PrivateKey decryptPrivateKeyFromPEM(String pemText, char[] password) throws Error { - PrivateKey privateKey = null; - StringReader stringReader = new StringReader(pemText); - PEMParser pemParser = new PEMParser(stringReader); - PemObject pemObject = null; - Security.addProvider(new BouncyCastleProvider()); - JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); - try { - pemObject = pemParser.readPemObject(); - while (pemObject != null) { - if (pemObject.getType().equals("ENCRYPTED PRIVATE KEY")) { - break; - } - pemObject = pemParser.readPemObject(); - } - - } catch (IOException e) { - throw new Error("cannot read PEM " + e); - } - - if (pemObject == null) { - throw new Error("no encrypted private key found in pem text"); - } - try { - PKCS8EncryptedPrivateKeyInfo encryptedInfo = new PKCS8EncryptedPrivateKeyInfo(pemObject.getContent()); - - JceOpenSSLPKCS8DecryptorProviderBuilder inputBuilder = new JceOpenSSLPKCS8DecryptorProviderBuilder(); - inputBuilder.setProvider("BC"); - InputDecryptorProvider decryptor = inputBuilder.build(password); - - PrivateKeyInfo privateKeyInfo = encryptedInfo.decryptPrivateKeyInfo(decryptor); - privateKey = converter.getPrivateKey(privateKeyInfo); - } catch (IOException | OperatorCreationException | PKCSException e) { - throw new Error("cannot decrypt password from PEM " + e); - } - return privateKey; - } - - public static void main(String[] args) throws Exception { - AKeyPair kp=AKeyPair.createSeeded(1337); - String pem=writePEM(kp); - System.out.println(pem); - - AKeyPair kp2=readPEM(pem); - System.out.println(kp2); - } - -} diff --git a/convex-core/src/main/java/convex/core/crypto/PFXTools.java b/convex-core/src/main/java/convex/core/crypto/PFXTools.java deleted file mode 100644 index 15ebe4443..000000000 --- a/convex-core/src/main/java/convex/core/crypto/PFXTools.java +++ /dev/null @@ -1,170 +0,0 @@ -package convex.core.crypto; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.math.BigInteger; -import java.security.GeneralSecurityException; -import java.security.Key; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.SecureRandom; -import java.security.UnrecoverableKeyException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.util.Date; - -import org.bouncycastle.jce.X509Principal; -import org.bouncycastle.x509.X509V3CertificateGenerator; - -@SuppressWarnings("deprecation") -public class PFXTools { - public static final String KEYSTORE_TYPE="PKCS12"; - - public static final String CERTIFICATE_ALGORITHM = "RSA"; - - /** - * Creates a new PKCS12 key store. - * @param keyFile File to use for creating the key store - * @param passPhrase Passphrase used to protect the key store, may be null - * @return New KeyStore instance - */ - @SuppressWarnings("javadoc") - public static KeyStore createStore(File keyFile, String passPhrase) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { - KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE); - - // need to load in bouncy castle crypto providers to set/get keys from the keystore - Providers.init(); - - char[] pwdArray = (passPhrase==null)?null:passPhrase.toCharArray(); - ks.load(null, pwdArray); - - try (FileOutputStream fos = new FileOutputStream(keyFile)) { - ks.store(fos, pwdArray); - } - return ks; - } - - /** - * Loads an existing PKCS12 Key store. - * @param keyFile File for the existing key store - * @param passPhrase Passphrase for decrypting the key store. May be blank or null if not encrypted. - * @return Found key store - * @throws IOException If an IO error occurs - * @throws GeneralSecurityException If a security error occurs - */ - public static KeyStore loadStore(File keyFile, String passPhrase) throws IOException,GeneralSecurityException { - - // Need to load in bouncy castle crypto providers to set/get keys from the keystore. - Providers.init(); - - KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE); - - char[] pwdArray = (passPhrase==null)?null:passPhrase.toCharArray(); - try (FileInputStream fis = new FileInputStream(keyFile)) { - ks.load(fis, pwdArray); - } - return ks; - } - - /** - * Saves a PKCS12 Key store to disk. - * @param ks Key store to save - * @param keyFile Target file - * @param passPhrase Passphrase for encrypting the key store. May be blank or null if not need for encryption. - * @return Same key store instance. - * @throws IOException If an IO error occurs accessing the key store - * @throws GeneralSecurityException if a security exception occurs - */ - public static KeyStore saveStore(KeyStore ks, File keyFile, String passPhrase) throws GeneralSecurityException, IOException { - - char[] pwdArray = (passPhrase==null)?null:passPhrase.toCharArray(); - try (FileOutputStream fos = new FileOutputStream(keyFile)) { - ks.store(fos, pwdArray); - } - - return ks; - } - - /** - * Generates a self-signed certificate. - * @param kpToSign Key pair - * @return New certificate - * @throws GeneralSecurityException If a security exception occurs - */ - public static Certificate createSelfSignedCertificate(AKeyPair kpToSign) throws GeneralSecurityException { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(CERTIFICATE_ALGORITHM); - KeyPair kp=keyPairGenerator.generateKeyPair(); - - X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator(); - - String domainName="convex.world"; - v3CertGen.setSerialNumber(BigInteger.valueOf(new SecureRandom().nextInt(Integer.MAX_VALUE))); - v3CertGen.setIssuerDN(new X509Principal("CN=" + domainName + ", OU=None, O=None L=None, C=None")); - v3CertGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60 * 60 * 24 * 30)); - v3CertGen.setNotAfter(new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 365*10))); - v3CertGen.setSubjectDN(new X509Principal("CN=" + domainName + ", OU=None, O=None L=None, C=None")); - - v3CertGen.setPublicKey(kpToSign.getPublic()); - v3CertGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); - - Certificate cert = v3CertGen.generateX509Certificate(kp.getPrivate()); - return cert; - } - - /** - * Retrieves a key pair from a key store. - * @param ks Key store - * @param alias Alias used for finding the key pair in the store - * @param passphrase Passphrase used for decrypting the key pair. Mandatory. - * @return Found key pair - */ - public static AKeyPair getKeyPair(KeyStore ks, String alias, String passPhrase) throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException { - char[] pwdArray = passPhrase.toCharArray(); - - Certificate cert = ks.getCertificate(alias); - if (cert == null) return null; - Key sk=ks.getKey(alias,pwdArray); - return Ed25519KeyPair.create(cert.getPublicKey(),(PrivateKey) sk); - } - - /** - * Adds a key pair to a key store. - * @param ks Key store - * @param kp Key pair - * @param passPhrase Passphrase for encrypting the key pair. Mandatory. - * @return Updated key store. - * @throws IOException If an IO error occurs accessing the key store - * @throws GeneralSecurityException if a security exception occurs - */ - public static KeyStore setKeyPair(KeyStore ks, AKeyPair kp, String passPhrase) throws IOException, GeneralSecurityException { - - return setKeyPair(ks, kp.getAccountKey().toHexString(), kp, passPhrase); - } - - /** - * Adds a key pair to a key store. - * @param ks Key store - * @param kp Key pair - * @param passPhrase Passphrase for encrypting the key pair. Mandatory. - * @return Updated key store. - * @throws IOException If an IO error occurs accessing the key store - * @throws GeneralSecurityException if a security exception occurs - */ - public static KeyStore setKeyPair(KeyStore ks, String alias, AKeyPair kp, String passPhrase) throws IOException, GeneralSecurityException { - - if (passPhrase == null) throw new IllegalArgumentException("Password is mandatory for private key"); - char[] pwdArray = passPhrase.toCharArray(); - - Certificate cert = createSelfSignedCertificate(kp); - ks.setKeyEntry(alias, kp.getPrivate(), pwdArray, new Certificate[] {cert}); - - return ks; - } - -} diff --git a/convex-core/src/main/java/convex/core/crypto/Providers.java b/convex-core/src/main/java/convex/core/crypto/Providers.java deleted file mode 100644 index 20bf259e6..000000000 --- a/convex-core/src/main/java/convex/core/crypto/Providers.java +++ /dev/null @@ -1,27 +0,0 @@ -package convex.core.crypto; - -import java.security.Security; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; - -import com.goterl.lazysodium.LazySodiumJava; -import com.goterl.lazysodium.SodiumJava; -import com.goterl.lazysodium.interfaces.Sign; - - - -public class Providers { - private static final SodiumJava NATIVE_SODIUM=new SodiumJava(); - - public static final LazySodiumJava SODIUM= new LazySodiumJava(NATIVE_SODIUM); - - public static final Sign.Native SODIUM_SIGN=(Sign.Native) SODIUM; - - static { - Security.addProvider(new BouncyCastleProvider()); - } - - public static void init() { - // static method to ensure static initialisation happens - } -} diff --git a/convex-core/src/main/java/convex/core/crypto/Symmetric.java b/convex-core/src/main/java/convex/core/crypto/Symmetric.java deleted file mode 100644 index 5202ae59f..000000000 --- a/convex-core/src/main/java/convex/core/crypto/Symmetric.java +++ /dev/null @@ -1,160 +0,0 @@ -package convex.core.crypto; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import javax.crypto.spec.IvParameterSpec; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; - -import convex.core.util.Utils; - -/** - * Class providing symmetric encryption functionality using AES - */ -public class Symmetric { - // Bouncy castle provider - static final BouncyCastleProvider PROVIDER = new org.bouncycastle.jce.provider.BouncyCastleProvider(); - - private static final String SYMMETRIC_ENCRYPTION_ALGO = "AES/CBC/PKCS5Padding"; - private static final int IV_LENGTH = 16; - private static final String SYMMETRIC_KEY_ALGORITHM = "AES"; - - private static final int KEY_LENGTH = 128; - - /** - * Encrypts a String with a given AES secret key, using standard UTF-8 encoding - * - * @param key AES secret key - * @param data String to encrypt - * @return Encrypted representation of the given string - */ - public static byte[] encrypt(SecretKey key, String data) { - return encrypt(key, data.getBytes(StandardCharsets.UTF_8)); - } - - /** - * Encrypt bytes with a given AES SecretKey Prepends the IV to the ciphertext. - * - * @param key Secret encryption key - * @param data Data to encrypt - * @return Encrypted representation of the given byte array data - */ - public static byte[] encrypt(SecretKey key, byte[] data) { - Cipher cipher = null; - byte[] iv = null; - try { - cipher = Cipher.getInstance(SYMMETRIC_ENCRYPTION_ALGO); - cipher.init(Cipher.ENCRYPT_MODE, key); - iv = cipher.getIV(); - } catch (Exception e) { - throw new Error("Failed to initialise encryption cipher", e); - } - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - try { - bos.write(iv); - } catch (IOException e) { - throw new Error("Problem writing IV with value " + Utils.toHexString(iv), e); - } - - CipherOutputStream cos = new CipherOutputStream(bos, cipher); - try { - cos.write(data); - cos.flush(); - cos.close(); - } catch (IOException e) { - throw new Error("Error encrypting data,e"); - } - return bos.toByteArray(); - } - - /** - * Decrypts a string from ciphertext, assuming UTF-8 format data - * - * @param key AES Secret Key - * @param encryptedData encrypted byte[] data to decrypt (ciphertext) - * @return The decrypted String - */ - public static String decryptString(SecretKey key, byte[] encryptedData) { - return new String(decrypt(key, encryptedData), StandardCharsets.UTF_8); - } - - /** - * Decrypts AES ciphertext with a given secret key. IV is assumed to be - * prepended to the cipherText - * - * @param key Secret encryption key - * @param encryptedData Encrypted data to decrypt - * @return A new byte array containing the decrypted data - */ - public static byte[] decrypt(SecretKey key, byte[] encryptedData) { - ByteArrayInputStream bis = new ByteArrayInputStream(encryptedData); - try { - return decrypt(key, bis); - } catch (IOException e) { - throw new Error("Unexpected IO exception", e); - } - } - - /** - * Decrypts AES ciphertext with a given secret key. IV is assumed to be - * prepended to the cipherText - * - * @param key Secret encryption key - * @param bis InputStream of data to decrypt - * @return A new byte array containing the decrypted data - * @throws IOException If an IO error occurs - */ - public static byte[] decrypt(SecretKey key, InputStream bis) throws IOException { - byte[] iv = new byte[IV_LENGTH]; - int x = bis.read(iv, 0, IV_LENGTH); - if (x != IV_LENGTH) throw new Error("IV not read correctly, " + x + " byes read"); - - // Create a Cipher using the provided key - Cipher cipher = null; - try { - cipher = Cipher.getInstance(SYMMETRIC_ENCRYPTION_ALGO); - IvParameterSpec ivParamSpec = new IvParameterSpec(iv); - cipher.init(Cipher.DECRYPT_MODE, key, ivParamSpec); - } catch (Exception e) { - throw new Error("Failed to initialise decryption cipher", e); - } - - // Read unencrypted bytes using CipherInputStream - CipherInputStream cis = new CipherInputStream(bis, cipher); - try { - return Utils.readBytes(cis); - } catch (IOException e) { - throw new Error("Failed to decrypt from input stream", e); - } - } - - /** - * Creates an AES secret key - * - * @return The generated SecretKey - */ - public static SecretKey createSecretKey() { - KeyGenerator kgen; - - try { - kgen = KeyGenerator.getInstance(SYMMETRIC_KEY_ALGORITHM); - kgen.init(KEY_LENGTH); - } catch (NoSuchAlgorithmException e) { - throw new Error("Key generator not initialised sucessfully", e); - } - SecretKey key = kgen.generateKey(); - return key; - } - -} diff --git a/convex-core/src/main/java/convex/core/crypto/Wallet.java b/convex-core/src/main/java/convex/core/crypto/Wallet.java deleted file mode 100644 index 042d51293..000000000 --- a/convex-core/src/main/java/convex/core/crypto/Wallet.java +++ /dev/null @@ -1,69 +0,0 @@ -package convex.core.crypto; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.security.KeyStore; -import java.util.Enumeration; -import java.util.HashMap; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -import convex.core.data.Address; - -public class Wallet { - public static final String KEYSTORE_TYPE="pkcs12"; - - private static final Logger log = LoggerFactory.getLogger(Wallet.class.getName()); - - private HashMap data; - - private Wallet(HashMap data) { - this.data = data; - } - - public static Wallet create() { - return new Wallet(new HashMap()); - } - - public WalletEntry get(Address a) { - return data.get(a); - } - - public static File createTempStore(String password) { - try { - KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE); - char[] pwdArray = "password".toCharArray(); - ks.load(null, pwdArray); - File file=File.createTempFile("temp-keystore", "p12"); - file.deleteOnExit(); - try (FileOutputStream fos = new FileOutputStream(file)) { - ks.store(fos, pwdArray); - } - return file; - } catch (Throwable t) { - throw new Error("Unable to create temp keystore",t); - } - } - - public static Wallet load(File file,String password) { - try { - KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE); - char[] pwdArray = password.toCharArray(); - ks.load(new FileInputStream(file), pwdArray); - Enumeration aliases=ks.aliases(); - Wallet wallet=Wallet.create(); - - while (aliases.hasMoreElements()) { - String alias=aliases.nextElement(); - ks.getKey(alias, pwdArray); - log.info("Loading private key with alias: "+alias); - } - return wallet; - } catch (Throwable t) { - throw new Error("Unable to load keystore with file: "+file,t); - } - } -} diff --git a/convex-core/src/main/java/convex/core/crypto/WalletEntry.java b/convex-core/src/main/java/convex/core/crypto/WalletEntry.java deleted file mode 100644 index 7527feb80..000000000 --- a/convex-core/src/main/java/convex/core/crypto/WalletEntry.java +++ /dev/null @@ -1,86 +0,0 @@ -package convex.core.crypto; - -import convex.core.data.ACell; -import convex.core.data.AMap; -import convex.core.data.AccountKey; -import convex.core.data.Address; -import convex.core.data.Keyword; -import convex.core.data.Maps; -import convex.core.data.SignedData; -import convex.core.exceptions.TODOException; - -/** - * Class implementing a Wallet Entry. - * - * May be in a locked locked or unlocked state. Unlocking requires passphrase. - */ -public class WalletEntry { - private final Address address; - private final AKeyPair keyPair; - private final AMap data; - - private WalletEntry(Address address, AMap data, AKeyPair kp) { - this.address=address; - this.data = data; - this.keyPair = kp; - } - - private WalletEntry(AMap data) { - this(null,data, null); - } - - public static WalletEntry create(Address address,AKeyPair kp) { - return new WalletEntry(address, Maps.empty(), kp); - } - - public AccountKey getAccountKey() { - return keyPair.getAccountKey(); - } - - public Address getAddress() { - return address; - } - - public AKeyPair getKeyPair() { - if (keyPair == null) throw new IllegalStateException("Wallet not unlocked!"); - return keyPair; - } - - public WalletEntry unlock(char[] password) { - if (keyPair != null) throw new IllegalStateException("Wallet already unlocked!"); - - // byte[] privateKey=PBE.deriveKey(password, data); - - // SignKeyPair kp=SignKeyPair.create(privateKey); - // return this.withKeyPair(kp); - - throw new TODOException(); - } - - public WalletEntry withKeyPair(AKeyPair kp) { - return new WalletEntry(address,data, kp); - } - - public WalletEntry withAddress(Address address) { - return new WalletEntry(null,data, keyPair); - } - - public WalletEntry lock() { - if (keyPair == null) throw new IllegalStateException("Wallet already locked!"); - // Clear keypair - return this.withKeyPair(null); - } - - public boolean isLocked() { - return (keyPair == null); - } - - @Override - public String toString() { - return getAddress() +" : " +getAccountKey().toChecksumHex(); - } - - public SignedData sign(R message) { - return keyPair.signData(message); - } -} diff --git a/convex-core/src/main/java/convex/core/data/AArrayBlob.java b/convex-core/src/main/java/convex/core/data/AArrayBlob.java deleted file mode 100644 index 0ef4dd828..000000000 --- a/convex-core/src/main/java/convex/core/data/AArrayBlob.java +++ /dev/null @@ -1,299 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.security.MessageDigest; -import java.util.Arrays; - -import convex.core.exceptions.InvalidDataException; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * Abstract base class for binary data stored in Java arrays. - * - */ -public abstract class AArrayBlob extends ABlob { - protected final byte[] store; - protected final int offset; - protected final int length; - - protected AArrayBlob(byte[] bytes, int offset, int length) { - this.store = bytes; - this.length = length; - this.offset = offset; - } - - @Override - public void updateDigest(MessageDigest digest) { - digest.update(store, offset, length); - } - - @Override - public Blob slice(long start, long length) { - if (length < 0) throw new IllegalArgumentException(Errors.negativeLength(length)); - if (start < 0) throw new IndexOutOfBoundsException("Start out of bounds: " + start); - if ((start + length) > this.length) - throw new IndexOutOfBoundsException("End out of bounds: " + (start + length)); - return Blob.wrap(store, Utils.checkedInt(start + offset), Utils.checkedInt(length)); - } - - @Override - public ABlob append(ABlob d) { - int dlength = Utils.checkedInt(d.count()); - if (dlength == 0) return this; - int length = this.length; - if (length == 0) return d; - byte[] newData = new byte[length + dlength]; - getBytes(newData, 0); - d.getBytes(newData, length); - return Blob.wrap(newData); - } - - @Override - public Blob slice(long start) { - return slice(start, count() - start); - } - - @Override - public Blob toBlob() { - return Blob.wrap(store, offset, length); - } - - @Override - public final int compareTo(ABlob b) { - if (b instanceof AArrayBlob) { - return compareTo((AArrayBlob) b); - } else { - return compareTo(b.toBlob()); - } - } - - public final int compareTo(AArrayBlob b) { - if (this == b) return 0; - int alength = this.length; - int blength = b.length; - int compareLength = Math.min(alength, blength); - int c = Utils.compareByteArrays(this.store, this.offset, b.store, b.offset, compareLength); - if (c != 0) return c; - if (alength > compareLength) return 1; // this is bigger - if (blength > compareLength) return -1; // b is bigger - return 0; - } - - @Override - public final void getBytes(byte[] dest, int destOffset) { - System.arraycopy(store, offset, dest, destOffset, length); - } - - @Override - public ByteBuffer writeToBuffer(ByteBuffer bb) { - return bb.put(store, offset, length); - } - - public int writeToBuffer(byte[] bs, int pos) { - System.arraycopy(store, offset, bs, pos, length); - return Utils.checkedInt(pos + length); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - System.arraycopy(store, offset, bs, pos, length); - return pos + length; - } - - @Override - public final String toHexString() { - return Utils.toHexString(store, offset, length); - } - - @Override - public void toHexString(StringBuilder sb) { - sb.append(toHexString()); - } - - @Override - public final long count() { - return length; - } - - @Override - public final byte byteAt(long i) { - int ix = (int) i; - if ((ix != i) || (ix < 0) || (ix >= length)) { - throw new IndexOutOfBoundsException("Index: " + i); - } - return store[offset + ix]; - } - - @Override - public final byte getUnchecked(long i) { - int ix = (int) i; - return store[offset + ix]; - } - - @Override - public int getHexDigit(long digitPos) { - byte b = store[offset+ (int)(digitPos >> 1)]; - // if ((digitPos & 1) == 0) { - // return (b >> 4) & 0x0F; // first hex digit - // } else { - // return b & 0x0F; // second hex digit - // } - int shift = 4 - (((int) digitPos & 1) << 2); - return (b >> shift) & 0x0F; - } - - /** - * Gets the internal array backing this Blob. Use with caution! - * @return Byte array backing this blob - */ - public byte[] getInternalArray() { - return store; - } - - /** - * Gets this offset into the internal array backing this Blob. - * @return Offset into backing array - */ - public int getInternalOffset() { - return offset; - } - - @Override - public ByteBuffer getByteBuffer() { - return ByteBuffer.wrap(store, offset, length).asReadOnlyBuffer(); - } - - @Override - public boolean equalsBytes(byte[] bytes, int byteOffset) { - return Utils.arrayEquals(store, offset, bytes, byteOffset, length); - } - - public boolean equalsBytes(ABlob k) { - if (k.count()!=this.count()) return false; - return k.equalsBytes(store,offset); - } - - /** - * Tests if a specific range of bytes are exactly equal. - * @param b Blob to compare with - * @param start Start index of range (inclusive) - * @param end End index of range (exclusive) - * @return true if digits are equal, false otherwise - */ - public boolean rangeMatches(ABlob b, int start, int end) { - if (b instanceof AArrayBlob) return rangeMatches((AArrayBlob)b,start,end); - for (int i = start; i < end; i++) { - // null entry if key does not match prefix - if (store[offset+i] != b.getUnchecked(i)) return false; - } - return true; - } - - /** - * Tests if a specific range of bytes are exactly equal from this Blob with another Blob - * @param b Blob with which to compare - * @param start Start index in both Blobs - * @param end End index in both Blobs - * @return true if digits are equal, false otherwise - */ - public boolean rangeMatches(AArrayBlob b, int start, int end) { - return Arrays.equals(store, offset+start, offset+end, b.store, b.offset+start,b.offset+end); - } - - @Override - public long hexMatchLength(ABlob b, long start, long length) { - if (b == this) return length; - long end = start + length; - for (long i = start; i < end; i++) { - if (!(getHexDigit(i) == b.getHexDigit(i))) return i - start; - } - return length; - } - - /** - * Tests if a specific range of hex digits are exactly equal. - * @param key Blob to compare with - * @param start Start hex digit index (inclusive) - * @param end End hex digit index (Exclusive) - * @return true if digits are equal, false otherwise - */ - public boolean hexMatches(ABlob key, int start, int end) { - if (key==this) return true; - if (start==end) return true; - if ((start&1)!=0) if (key.getHexDigit(start) != getHexDigit(start)) return false; - if ((end&1)!=0) if (key.getHexDigit(end-1) != getHexDigit(end-1)) return false; - return rangeMatches(key,(start+1)>>1,end>>1); - } - - @Override - public long commonHexPrefixLength(ABlob b) { - if (b == this) return count() * 2; - - long max = Math.min(count(), b.count()); - for (long i = 0; i < max; i++) { - byte ai = getUnchecked(i); - byte bi = b.getUnchecked(i); - if (ai != bi) return (i * 2) + (Utils.firstDigitMatch(ai, bi) ? 1 : 0); - } - return max * 2; - } - - - - @Override - public void validate() throws InvalidDataException { - super.validate(); - } - - @Override - public void validateCell() throws InvalidDataException { - if (length < 0) throw new InvalidDataException("Negative length: " + length, this); - if (offset < 0) throw new InvalidDataException("Negative data offset: " + offset, this); - if ((offset + length) > store.length) { - throw new InvalidDataException( - "End out of range: " + (offset + length) + " with array size=" + store.length, this); - } - } - - @Override - public long longValue() { - if (length != 8) throw new IllegalStateException(Errors.wrongLength(8, length)); - return Utils.readLong(store, offset); - } - - @Override - public long toLong() { - if (length >= 8) { - return Utils.readLong(store, offset + length - 8); - } else { - long result = 0l; - int ix = offset; - if ((length & 4) != 0) { - result += 0xffffffffL & Utils.readInt(store, ix); - ix += 4; - } - if ((length & 2) != 0) { - result = (result >> 16) + (0xFFFF & Utils.readShort(store, ix)); - ix += 2; - } - if ((length & 1) != 0) { - result = (result >> 8) + (0xFF & store[ix]); - ix += 1; - } - // TODO: do we want to sign extend? - // int shift=8*(8-length); - // correct sign - // result=(result<>shift; - return result; - } - } - - @Override - public int getRefCount() { - // Array-backed blobs have no child Refs by default - return 0; - } - - -} \ No newline at end of file diff --git a/convex-core/src/main/java/convex/core/data/ABlob.java b/convex-core/src/main/java/convex/core/data/ABlob.java deleted file mode 100644 index bfb2ddff1..000000000 --- a/convex-core/src/main/java/convex/core/data/ABlob.java +++ /dev/null @@ -1,393 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.security.MessageDigest; - -import convex.core.crypto.Hashing; -import convex.core.data.prim.CVMByte; -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.InvalidDataException; -import convex.core.util.Utils; - -/** - * Abstract base class for data objects containing immutable chunks of binary - * data. Representation is equivalent to a fixed size immutable byte sequence. - * - * Rationale: - Allow data to be encapsulated as an immutable object - Provide - * specialised methods for processing byte data - Provide a cached Hash value, - * lazily computed on demand - * - */ -public abstract class ABlob extends ACountable implements Comparable { - /** - * Cached hash of the Blob data. Might be null. - */ - protected Hash contentHash = null; - - @Override - public AType getType() { - return Types.BLOB; - } - - /** - * Copies the bytes from this blob to a given destination - * - * @param dest Destination array - * @param destOffset Offset into destination array - */ - public abstract void getBytes(byte[] dest, int destOffset); - - /** - * Gets the length of this Blob - * - * @return The length in bytes of this data object - */ - @Override - public abstract long count(); - - @Override - public CVMByte get(long ix) { - return CVMByte.create(byteAt(ix)); - } - - @Override - public Ref getElementRef(long index) { - return get(index).getRef(); - } - - public Blob empty() { - return Blob.EMPTY; - } - - /** - * Converts this data object to a lowercase hex string representation - * @return Hex String representation - */ - public abstract String toHexString(); - - /** - * Converts this blob to a readable byte buffer - * @return ByteBuffer with position zero (ready to read) - */ - public ByteBuffer toByteBuffer() { - return ByteBuffer.wrap(getBytes()); - } - - /** - * Converts this data object to a hex string representation of the given length. - * Equivalent to truncating the full String representation. - * @param length Length to truncate String to (in hex characters) - * @return String representation of hex values in Blob - */ - public String toHexString(int length) { - return toHexString().substring(0, length); - } - - /** - * Gets a contiguous slice of this blob, as a new Blob. - * - * Shares underlying backing data where possible - * - * @param start Start position for the created slice - * @param length Length of the slice - * @return A blob of the specified length, representing a slice of this blob. - */ - public abstract ABlob slice(long start, long length); - - /** - * Gets a slice of this blob, as a new blob, starting from the given offset and - * extending to the end of the blob. - * - * Shares underlying backing data where possible. Returned Blob may not be the - * same type as the original Blob - * @param start Start position to slice from - * @return Slice of Blob - */ - public ABlob slice(long start) { - return slice(start, count() - start); - } - - /** - * Converts this object to a Blob instance - * - * @return A Blob instance containing the same data as this Blob. - */ - public abstract Blob toBlob(); - - /** - * Computes the length of the longest common hex prefix between two blobs - * - * @param b Blob to compare with - * @return The length of the longest common prefix in hex digits - */ - public abstract long commonHexPrefixLength(ABlob b); - - /** - * Computes the hash of the byte data stored in this Blob, using the default MessageDigest. - * - * This is the correct hash ID for a data value if this blob contains the data value's encoding - * - * @return The Hash - */ - public final Hash getContentHash() { - if (contentHash == null) { - contentHash = computeHash(Hashing.getDigest()); - } - return contentHash; - } - - /** - * Computes the hash of the byte data stored in this Blob, using the given MessageDigest. - * - * @param digest MessageDigest instance - * @return The hash - */ - public final Hash computeHash(MessageDigest digest) { - updateDigest(digest); - return Hash.wrap(digest.digest()); - } - - protected abstract void updateDigest(MessageDigest digest); - - /** - * Gets the byte at the specified position in this blob - * - * @param i Index of the byte to get - * @return The byte at the specified position - */ - public byte byteAt(long i) { - if ((i < 0) || (i >= count())) { - throw new IndexOutOfBoundsException("Index: " + i); - } - return getUnchecked(i); - } - - /** - * Gets the byte at the specified position in this data object, without bounds checking. - * Only safe if index is known to be in bounds, otherwise result is undefined. - * - * @param i Index of the byte to get - * @return The byte at the specified position - */ - public abstract byte getUnchecked(long i); - - /** - * Gets the specified hex digit from this data object. - * - * Result is undefined if index is out of bounds. - * - * @param digitPos The position of the hex digit - * @return The value of the hex digit, in the range 0-15 inclusive - */ - public int getHexDigit(long digitPos) { - byte b = getUnchecked(digitPos >> 1); - //if ((digitPos & 1) == 0) { - // return (b >> 4) & 0x0F; // first hex digit - //} else { - // return b & 0x0F; // second hex digit - //} - int shift = 4-(((int)digitPos&1)<<2); - return (b>>shift)&0x0F; - } - - /** - * Gets a byte array containing a copy of this data object. - * - * @return A new byte array containing the contents of this blob. - */ - public byte[] getBytes() { - byte[] result = new byte[Utils.checkedInt(count())]; - getBytes(result, 0); - return result; - } - - /** - * Append an additional data object to this, creating a new data object. - * - * @param d Blob to append - * @return A new blob, containing the additional data appended to this blob. - */ - public abstract ABlob append(ABlob d); - - /** - * Determines if this Blob is equal to another Object. - * - * Blobs are defined to be equal if they have the same on-chain representation, - * i.e. if and only if all of the following are true: - * - * - Blob is of the same general type - Blobs are of the same length - All byte - * values are equal - */ - @Override - public final boolean equals(ACell o) { - if (!(o instanceof ABlob)) return false; - return equals((ABlob)o); - } - - /** - * Determines if this Blob is equal to another Blob. - * - * Blobs are defined to be equal if they have the same on-chain representation, - * i.e. if and only if all of the following are true: - * - * - Blob is of the same general type - Blobs are of the same length - All byte - * values are equal - * - * @param o Blob to compare with - * @return true if Blobs are equal, false otherwise - */ - public abstract boolean equals(ABlob o); - - @Override - public abstract ABlob toCanonical(); - - /** - * Tests if this Blob is equal to a subset of a byte array - * @param bytes Byte array to compare with - * @param byteOffset Offset into byte array - * @return true if exactly equal, false otherwise - */ - public abstract boolean equalsBytes(byte[] bytes, int byteOffset); - - /** - * Compares this blob to another blob, in lexographic order sorting by first - * bytes. - * - * Note: This means that compareTo does not precisely match equality, because - * different blob types may be lexicographically equal but represent different values. - */ - @Override - public int compareTo(ABlob b) { - if (this == b) return 0; - long alength = this.count(); - long blength = b.count(); - long compareLength = Math.min(alength, blength); - for (long i = 0; i < compareLength; i++) { - int c = (0xFF & getUnchecked(i)) - (0xFF & b.getUnchecked(i)); - if (c > 0) return 1; - if (c < 0) return -1; - } - if (alength > compareLength) return 1; // this is bigger - if (blength > compareLength) return -1; // b is bigger - return 0; - } - - /** - * Writes the raw byte contents of this blob to a ByteBuffer. - * - * @param bb ByteBuffer to write to - * @return The passed ByteBuffer, after writing byte content - */ - public abstract ByteBuffer writeToBuffer(ByteBuffer bb); - - /** - * Writes the raw byte contents of this blob to a byte array - * - * @param bs Byte array to write to - * @param pos Starting position in byte array to write to - * @return The position in the array after writing - */ - public abstract int writeToBuffer(byte[] bs, int pos); - - /** - * Gets a chunk of this Blob, as a canonical Blob up to the maximum chunk size - * - * @param i Index of chunk - * @return A Blob containing the specified chunk data. - */ - public abstract Blob getChunk(long i); - - @Override - public void print(StringBuilder sb) { - sb.append("0x"); - toHexString(sb); - } - - /** - * Gets a byte buffer containing this Blob's data. Will have remaining bytes - * equal to this Blob's size. - * - * @return A ByteBuffer containing the Blob's data. - */ - public abstract ByteBuffer getByteBuffer(); - - public abstract void toHexString(StringBuilder sb); - - @Override - public void validate() throws InvalidDataException { - super.validate(); - } - - @Override - public void validateCell() throws InvalidDataException { - if (count() < 0) throw new InvalidDataException("Negative blob length", this); - } - - /** - * Returns the number of matching hex digits in the given hex range of another blob. Assumes - * range is valid for both blobs. - * - * Returns length if this Blob is exactly equal to the specified hex range. - * - * @param start Start position (in hex digits) - * @param length Length to compare (in hex digits) - * @param b Blob to compare with - * @return The number of matching hex characters - */ - public abstract long hexMatchLength(ABlob b, long start, long length); - - public boolean hexEquals(ABlob b) { - long c = count(); - if (b.count() != c) return false; - return hexMatchLength(b, 0L, c) == c; - } - - public boolean hexEquals(ABlob b, long start, long length) { - return hexMatchLength(b, start, length) == length; - } - - public long hexLength() { - return count() << 1; - } - - /** - * Converts this Blob to the corresponding long value. - * - * Assumes big-endian format, as if the entire blob is interpreted as a signed big integer. Higher bytes - * outside the Long range will be ignored. - * - * @return long value of this blob - */ - public abstract long toLong(); - - @Override - public int hashCode() { - // note: We use the Java hashcode of the last bytes for blobs - return Long.hashCode(toLong()); - } - - /** - * Gets the long value of this Blob if the length is exactly 8 bytes, otherwise - * throws an Exception - * - * @return The long value represented by the Blob - */ - public abstract long longValue(); - - /** - * Returns true if this object is a regular blob (i.e. not a special blob type like Hash or Address) - * @return True if a regular blob - */ - public boolean isRegularBlob() { - return getTag()==Tag.BLOB; - } - - /** - * Tests if this Blob has exactly the same bytes as another Blob - * @param b Blob to compare with - * @return True if byte content is exactly equal, false otherwise - */ - public abstract boolean equalsBytes(ABlob b); - -} diff --git a/convex-core/src/main/java/convex/core/data/ABlobMap.java b/convex-core/src/main/java/convex/core/data/ABlobMap.java deleted file mode 100644 index 9c8d9747d..000000000 --- a/convex-core/src/main/java/convex/core/data/ABlobMap.java +++ /dev/null @@ -1,115 +0,0 @@ -package convex.core.data; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.TODOException; - -/** - * Abstract base class for a sorted radix-tree map of Blobs to values. - * - * Primary benefits: - Provide sorted orderings for indexes - Support Schedule - * data structure - * - * @param Type of BlobMap keys - * @param Type of BlobMap values - */ -public abstract class ABlobMap extends AMap { - protected ABlobMap(long count) { - super(count); - } - - @Override - public final V get(ACell key) { - if (!(key instanceof ABlob)) return null; - return get((ABlob) key); - } - - @Override - public boolean containsKey(ACell key) { - if (!(key instanceof ABlob)) return false; - return (getEntry((ABlob) key) != null); - } - - /** - * Gets the map entry for a given blob - * - * @param key Key to lookup up - * @return The value specified by the given blob key or null if not present. - */ - public abstract V get(ABlob key); - - @Override - public boolean equalsKeys(AMap map) { - // TODO: probably not needed? Only need this for set implementations - throw new TODOException(); - } - - @Override - public Set> entrySet() { - HashSet> hs=new HashSet<>(size()); - long n=count(); - for (long i=0; i me=entryAt(i); - hs.add(me); - } - return Collections.unmodifiableSet(hs); - } - - @Override - public abstract int getRefCount(); - - @Override - public abstract Ref getRef(int i); - - @Override - public boolean isCanonical() { - return true; - } - - @Override - public AType getType() { - return Types.BLOBMAP; - } - - /** - * Associates a blob key with a value in this data structure. - * - * Returns null if the key is not a valid BlobMap key - */ - @Override - public abstract ABlobMap assoc(ACell key, ACell value); - - @SuppressWarnings("unchecked") - @Override - public final ABlobMap dissoc(ACell key) { - if (key instanceof ABlob) { - return dissoc((K)key); - } - return this; - } - - public abstract ABlobMap dissoc(K key); - - @Override - public MapEntry getKeyRefEntry(Ref ref) { - return getEntry(ref.getValue()); - } - - @Override - public abstract MapEntry entryAt(long i); - - public MapEntry getEntry(ACell key) { - if (key instanceof ABlob) return getEntry((ABlob)key); - return null; - } - - public abstract MapEntry getEntry(ABlob key); - - @Override - public abstract int estimatedEncodingSize(); - -} diff --git a/convex-core/src/main/java/convex/core/data/ACell.java b/convex-core/src/main/java/convex/core/data/ACell.java deleted file mode 100644 index a5d3eaa6f..000000000 --- a/convex-core/src/main/java/convex/core/data/ACell.java +++ /dev/null @@ -1,495 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.util.function.Consumer; - -import convex.core.Constants; -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.InvalidDataException; -import convex.core.exceptions.TODOException; -import convex.core.store.AStore; -import convex.core.store.Stores; -import convex.core.util.Utils; - -/** - * Abstract base class for Cells. - * - * Cells may contain Refs to other Cells, which can be tested with getRefCount() - * - * All data objects intended for on-chain usage serialisation should extend this. The only - * exceptions are data objects which are Embedded (inc. certain JVM types like Long etc.) - * - * "It is better to have 100 functions operate on one data structure than - * to have 10 functions operate on 10 data structures." - Alan Perlis - */ -public abstract class ACell extends AObject implements IWriteable, IValidated { - - - /** - * An empty Java array of cells - */ - public static final ACell[] EMPTY_ARRAY = new ACell[0]; - - /** - * We cache the computed memorySize. May be 0 for embedded objects - * -1 is initial value for when size is not calculated - */ - private long memorySize=-1; - - /** - * Cached Ref. This is useful to manage persistence - */ - protected Ref cachedRef=null; - - @Override - public void validate() throws InvalidDataException { - validateCell(); - } - - /** - * Validates the local structure and invariants of this cell. Called by validate() super implementation. - * - * Should validate directly contained data, but should not validate all other structure of this cell. - * - * In particular, should not traverse potentially missing child Refs. - * - * @throws InvalidDataException If the Cell is invalid - */ - public abstract void validateCell() throws InvalidDataException; - - /** - * Hash of data Encoding of this cell, equivalent to the Value ID. Calling this method - * may force hash computation if needed. - * - * @return The Hash of this cell's encoding. - */ - public final Hash getHash() { - // final method to avoid any mistakes. - return getEncoding().getContentHash(); - } - - /** - * Gets the tag byte for this cell. The tag byte will be the first byte of the encoding - * @return Tag byte for this Cell - */ - public abstract byte getTag(); - - /** - * Gets the Hash if already computed, or null if not yet available - * @return Cached Hash value, or null if not available - */ - protected final Hash cachedHash() { - if (cachedRef!=null) { - Hash h=cachedRef.cachedHash(); - if (h!=null) return h; - } - if (encoding==null) return null; - return encoding.contentHash; - } - - /** - * Gets the Java hashCode for this cell. Must be consistent with equals. - * - * Default is the first bytes (big-endian) of the Cell Encoding's hash, since this is consistent with - * encoding-based equality. However, different Types may provide more efficient hashcodes provided that - * the usual invariants are preserved - * - * @return integer hash code. - */ - @Override - public int hashCode() { - return getHash().firstInt(); - } - - @Override - public final boolean equals(Object a) { - if (!(a instanceof ACell)) return false; - return equals((ACell)a); - } - - /** - * Gets the encoded byte representation of this cell. - * - * @return A blob representing this cell in encoded form - */ - public final Blob getEncoding() { - if (encoding==null) encoding=createEncoding(); - return encoding; - } - - /** - * Checks for equality with another object. In general, data objects should be considered equal - * if they have the same canonical representation, i.e. an identical encoding with the same hash value. - * - * Subclasses should override this if they have a more efficient equals implementation. - * - * @param a Cell to compare with. May be null?? - * @return True if this cell is equal to the other object - */ - public boolean equals(ACell a) { - if (this==a) return true; // important optimisation for e.g. hashmap equality - if (a==null) return false; - if (!(a.getTag()==this.getTag())) return false; - return getEncoding().equals(a.getEncoding()); - } - - /** - * Writes this Cell's encoding to a ByteBuffer, including a tag byte which will be written first - * - * @param bb A ByteBuffer to which to write the encoding - * @return The passed ByteBuffer, after the representation of this object has been written. - */ - @Override - public final ByteBuffer write(ByteBuffer bb) { - return getEncoding().writeToBuffer(bb); - } - - /** - * Writes this Cell's encoding to a byte array, including a tag byte which will be written first - * - * @param bs A byte array to which to write the encoding - * @param pos The offset into the byte array - * - * @return New position after writing - */ - public abstract int encode(byte[] bs, int pos); - - /** - * Writes this Cell's encoding to a byte array, excluding the tag byte - * - * @param bs A byte array to which to write the encoding - * @param pos The offset into the byte array - * @return New position after writing - */ - public abstract int encodeRaw(byte[] bs, int pos); - - @Override - public final Blob createEncoding() { - int capacity=estimatedEncodingSize(); - byte[] bs=new byte[capacity]; - int pos=0; - boolean done=false; - while (!done) { - try { - pos=encode(bs,pos); - done=true; - } catch (IndexOutOfBoundsException be) { - // We really want to eliminate these, because exception handling is expensive - // System.out.println("Insufficient encoding size: "+capacity+ " for "+this.getClass()); - capacity=capacity*2+10; - bs=new byte[capacity]; - } - } - return Blob.wrap(bs,0,pos); - } - - /** - * Returns the String representation of this Cell. - * - * The String representation is intended to be a easy-to-read textual representation of the Cell's data content. - * - */ - @Override - public String toString() { - StringBuilder sb=new StringBuilder(); - print(sb); - return sb.toString(); - } - - /** - * Gets the cached blob representing this Cell's Encoding in binary format, if it exists. - * - * @return The cached blob for this cell, or null if not available. - */ - public ABlob cachedEncoding() { - return encoding; - } - - /** - * Calculates the Memory Size for this Cell. - * - * Requires any child Refs to be either Direct or of persisted status at minimum, - * or you might get a MissingDataException - * - * @return Memory Size of this Cell - */ - protected long calcMemorySize() { - // add size for each child Ref (might be zero if embedded) - long result=0; - int n=getRefCount(); - for (int i=0; i childRef=getRef(i); - long childSize=childRef.getMemorySize(); - result+=childSize; - } - - if (!isEmbedded()) { - // We need to count this cell's own encoding length - result+=getEncodingLength(); - - // Add overhead for storage of non-embedded cell - result+=Constants.MEMORY_OVERHEAD; - } - return result; - } - - /** - * Method to calculate the encoding length of a Cell. May be overridden to avoid - * creating encodings during memory size calculations. This reduces hashing! - * - * @return Exact encoding length of this Cell - */ - public long getEncodingLength() { - return getEncoding().count(); - } - - /** - * Gets the Memory Size of this Cell, computing it if required. - * - * The memory size is the total storage requirement for this cell. Embedded cells do not require storage for - * their own encoding, but may require storage for nested non-embedded Refs. - * - * @return Memory Size of this Cell - */ - public final long getMemorySize() { - long ms=memorySize; - if (ms>=0) return ms; - ms=calcMemorySize(); - this.memorySize=ms; - return ms; - } - - /** - * Determines if this Cell Represents an embedded object. Embedded objects are encoded directly into - * the encoding of the containing Cell (avoiding the need for a hashed reference). - * - * Subclasses should override this if they have a cheap O(1) - * way to determine if they are embedded or otherwise. - * - * @return true if Cell is embedded, false otherwise - */ - public boolean isEmbedded() { - if (cachedRef!=null) { - int flags=cachedRef.flags; - if ((flags&Ref.KNOWN_EMBEDDED_MASK)!=0) return true; - if ((flags&Ref.NON_EMBEDDED_MASK)!=0) return false; - } - boolean embedded= getEncodingLength()<=Format.MAX_EMBEDDED_LENGTH; - if (cachedRef!=null) { - cachedRef.flags|=(embedded)?Ref.KNOWN_EMBEDDED_MASK:Ref.NON_EMBEDDED_MASK; - } - return embedded; - } - - /** - * Returns true if this Cell is in a canonical format for message writing. - * Reading or writing a non-canonical value should be considered illegal, but - * non-canonical objects may be used on a temporary internal basis. - * - * @return true if the object is in canonical format, false otherwise - */ - public abstract boolean isCanonical(); - - /** - * Converts this Cell to its canonical version. Returns this if already canonical - * - * @return Canonical version of Cell - */ - public abstract ACell toCanonical(); - - /** - * Returns true if this object represents a first class CVM Value. Sub-structural cells that are not themselves first class values - * should return false. - * - * CVM values might not be in a canonical format, e.g. temporary data structures - * - * @return true if the object is a CVM Value, false otherwise - */ - public abstract boolean isCVMValue(); - - /** - * Gets the number of Refs contained within this Cell. This number is - * final / immutable for any given instance. - * - * Contained Refs may be either external or embedded. - * - * @return The number of Refs in this Cell - */ - public abstract int getRefCount(); - - /** - * Gets the Ref for this Cell, creating a new direct reference if necessary - * - * @param Type of Cell - * @return Ref for this Cell - */ - @SuppressWarnings("unchecked") - public final Ref getRef() { - if (cachedRef!=null) return (Ref) cachedRef; - return createRef(); - } - - /** - * Creates a new Ref for this Cell - * @param Type of Cell - * @return New Ref instance - */ - @SuppressWarnings("unchecked") - protected Ref createRef() { - Ref newRef= RefDirect.create(this,cachedHash()); - cachedRef=newRef; - return (Ref) newRef; - } - - /** - * Gets a numbered child Ref from within this Cell. - * - * @param Type of referenced Cell - * @param i Index of ref to get - * @return The Ref at the specified index - */ - public Ref getRef(int i) { - // This will always throw an error if not overridden. Provided for warning purposes if accidentally used. - if (getRefCount()==0) { - throw new IndexOutOfBoundsException("No Refs to get in "+Utils.getClassName(this)); - } else { - throw new TODOException(Utils.getClassName(this) +" does not yet implement getRef(i) for i = "+i); - } - } - - /** - * Updates all Refs in this object using the given function. - * - * The function *must not* change the hash value of Refs, in order to ensure - * structural integrity of modified data structures. - * - * This is a building block for a very sneaky trick that enables use to do a lot - * of efficient operations on large trees of smart references. - * - * Must return the same object if no Refs are altered. - * @param func Ref update function - * @return Cell with updated Refs - */ - public ACell updateRefs(IRefFunction func) { - if (getRefCount()==0) return this; - throw new TODOException(Utils.getClassName(this) +" does not yet implement updateRefs(...)"); - } - - /** - * Gets an array of child Refs for this Cell, in the same order as order accessible by - * getRef. - * - * Concrete implementations may override this to optimise performance. - * - * @param Type of referenced Cell - * @return Array of Refs - */ - @SuppressWarnings("unchecked") - public Ref[] getChildRefs() { - int n = getRefCount(); - Ref[] refs = new Ref[n]; - for (int i = 0; i < n; i++) { - refs[i] = getRef(i); - } - return refs; - } - - /** - * Gets the most specific known runtime Type for this Cell. - * @return The Type of this Call - */ - public AType getType() { - return Types.ANY; - } - - /** - * Updates the memorySize of this Cell - * - * Not valid for embedded Cells, may throw IllegalOperationException() - * - * @param memorySize Memory size to assign - */ - public void attachMemorySize(long memorySize) { - if (this.memorySize<0) { - this.memorySize=memorySize; - assert (this.memorySize>0) : "Attempting to attach memory size "+memorySize+" to object of class "+Utils.getClassName(this); - } else { - assert (this.memorySize==memorySize) : "Attempting to attach memory size "+memorySize+" to object of class "+Utils.getClassName(this)+" which already has memorySize "+this.memorySize; - } - } - - /** - * Updates the cached ref of this Cell - * - * @param ref Ref to assign - */ - @SuppressWarnings("unchecked") - public void attachRef(Ref ref) { - this.cachedRef=(Ref) ref; - } - - /** - * Creates an ANNOUNCED Ref with the given value in the current store. - * - * Novelty handler is called for all new Refs that are persisted (recursively), - * starting from lowest levels. - * - * @param Type of Value - * @param value Value to announce - * @param noveltyHandler Novelty handler to call for any Novelty (may be null) - * @return Persisted Ref - */ - public static T createAnnounced(T value, Consumer> noveltyHandler) { - if (value==null) return null; - return value.announce(noveltyHandler); - } - - public T announce() { - return announce(null); - } - - @SuppressWarnings("unchecked") - public T announce(Consumer> noveltyHandler) { - Ref ref = getRef(); - AStore store=Stores.current(); - ref= store.storeTopRef(ref, Ref.ANNOUNCED,noveltyHandler); - cachedRef=ref; - return (T) this; - } - - - /** - * Creates a persisted Ref with the given value in the current store. - * - * Novelty handler is called for all new Refs that are persisted (recursively), - * starting from lowest levels (depth first order) - * - * @param Type of Value - * @param value Any CVM value to persist - * @param noveltyHandler Novelty handler to call for any Novelty (may be null) - * @return Persisted Ref - */ - @SuppressWarnings("unchecked") - public static Ref createPersisted(T value, Consumer> noveltyHandler) { - Ref ref = Ref.get(value); - if (ref.isPersisted()) return ref; - AStore store=Stores.current(); - ref = (Ref) store.storeTopRef(ref, Ref.PERSISTED,noveltyHandler); - value.cachedRef=(Ref)ref; - return ref; - } - - /** - * Creates a persisted Ref with the given value in the current store. Returns - * the current Ref if already persisted - * - * @param Type of Value - * @param value Any CVM value to persist - * @return Ref to the given value - */ - public static Ref createPersisted(T value) { - return createPersisted(value, null); - } - -} diff --git a/convex-core/src/main/java/convex/core/data/ACollection.java b/convex-core/src/main/java/convex/core/data/ACollection.java deleted file mode 100644 index 5827ae1fd..000000000 --- a/convex-core/src/main/java/convex/core/data/ACollection.java +++ /dev/null @@ -1,211 +0,0 @@ -package convex.core.data; - -import java.lang.reflect.Array; -import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; -import java.util.ListIterator; -import java.util.NoSuchElementException; -import java.util.function.Function; - -import convex.core.data.type.AType; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * Abstract base class for Persistent Merkle Collections - * - *

- * A Collection is a data structure that contains zero or more elements. Possible collection subtypes include: - *

- *
    - *
  • Sequential collections (Lists, Vectors)
  • - *
  • Sets (with unique elements)
  • - *
- * - * @param Type of elements in this collection - */ -public abstract class ACollection extends ADataStructure implements Collection { - - protected ACollection(long count) { - super(count); - } - - @Override - public abstract AType getType(); - - @Override - public abstract int encode(byte[] bs, int pos); - - @Override - public abstract boolean contains(Object o); - - @Override - public Iterator iterator() { - return new BasicIterator(0); - } - - /** - * Custom ListIterator for ListVector - */ - private class BasicIterator implements ListIterator { - long pos; - - public BasicIterator(long index) { - if (index < 0L) throw new IndexOutOfBoundsException((int)index); - - long c = count(); - if (index > c) throw new IndexOutOfBoundsException((int)index); - pos = index; - } - - @Override - public boolean hasNext() { - return pos < count(); - } - - @Override - public T next() { - return get(pos++); - } - - @Override - public boolean hasPrevious() { - if (pos > 0) return true; - return false; - } - - @Override - public T previous() { - if (pos > 0) return get(--pos); - throw new NoSuchElementException(); - } - - @Override - public int nextIndex() { - return Utils.checkedInt(pos); - } - - @Override - public int previousIndex() { - return Utils.checkedInt(pos - 1); - } - - @Override - public void remove() { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public void set(T e) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public void add(T e) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - } - - @Override - public final boolean add(T e) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public final boolean remove(Object o) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public boolean containsAll(Collection c) { - HashSet h=new HashSet(this.size()); - h.addAll(this); - for (Object o: c) { - if (!h.contains(o)) return false; - } - return true; - } - - @Override - public final boolean addAll(Collection c) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public final boolean removeAll(Collection c) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public final boolean retainAll(Collection c) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public final void clear() { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - /** - * Converts this collection to a canonical vector of elements - * @return This collection coerced to a vector - */ - public abstract AVector toVector(); - - /** - * Copies the elements of this collection in order to an array at the specified offset - * - * @param Type of array elements required - * @param arr - * @param offset - */ - protected abstract void copyToArray(R[] arr, int offset); - - /** - * Converts this collection to a new Cell array - * @return A new cell array containing the elements of this sequence - */ - public ACell[] toCellArray() { - int n=Utils.checkedInt(count()); - ACell[] cells=new ACell[n]; - int i=0; - for (ACell cell: this) { - cells[i++]=cell; - } - return cells; - } - - @SuppressWarnings("unchecked") - @Override - public V[] toArray(V[] a) { - int s = size(); - if (s > a.length) { - Class c = (Class) a.getClass().getComponentType(); - a = (V[]) Array.newInstance(c, s); - } - copyToArray(a, 0); - if (s < a.length) a[s] = null; - return a; - } - - /** - * Adds an element to this collection, according to the natural semantics of the collection - * @param x Value to add - * @return The updated collection - */ - @Override - public abstract ACollection conj(R x); - - /** - * Maps a function over a collection, applying it to each element in turn. - * - * @param Type of element in resulting collection - * @param mapper Function to map over collection - * @return Collection after function applied to each element - */ - public abstract ACollection map(Function mapper); - - -} diff --git a/convex-core/src/main/java/convex/core/data/ACountable.java b/convex-core/src/main/java/convex/core/data/ACountable.java deleted file mode 100644 index ef31c7c32..000000000 --- a/convex-core/src/main/java/convex/core/data/ACountable.java +++ /dev/null @@ -1,56 +0,0 @@ -package convex.core.data; - -/** - * Abstract base class for Countable objects. - * - * Countable values support a count of sub-elements and the ability to get by index. - * - * @param Type of element that is counted - */ -public abstract class ACountable extends ACell { - - /** - * Returns the number of elements in this data structure - * - * @return Number of elements in this collection. - */ - public abstract long count(); - - /** - * Gets the element at the specified index in this collection - * - * @param index Index of element to get - * @return Element at the specified index - */ - public abstract E get(long index); - - - /** - * Gets a Ref to the element at the specified index in this collection - * - * @param index Index of element to get - * @return Element at the specified index - */ - public abstract Ref getElementRef(long index); - - /** - * Checks if this data structure is empty, i.e. has a count of zero elements. - * - * @return true if this data structure is empty, false otherwise - */ - public boolean isEmpty() { - return count() == 0L; - } - - /** - * Gets the size of this data structure as an int. - * - * Returns Integer.MAX_SIZE if the count is larger than can fit in an int. If - * this might be a problem, use count() instead. - * - * @return Number of elements in this collection. - */ - public int size() { - return (int) (Math.min(count(), Integer.MAX_VALUE)); - } -} diff --git a/convex-core/src/main/java/convex/core/data/ADataStructure.java b/convex-core/src/main/java/convex/core/data/ADataStructure.java deleted file mode 100644 index 5382a9179..000000000 --- a/convex-core/src/main/java/convex/core/data/ADataStructure.java +++ /dev/null @@ -1,121 +0,0 @@ -package convex.core.data; - -/** - * Abstract base class for Persistent data structures. Each can be regarded as a - * countable, immutable collection of elements. - * - * Data structures in general support: - *
    - *
  • Immutability
  • - *
  • Addition of an element(s) of appropriate type
  • - *
  • Construction of an empty (zero) element
  • - *
- * - *

- * "When you know your data can never change out from underneath you, everything - * is different." - Rich Hickey - *

- * - * @param Type of Data Structure elements - */ -public abstract class ADataStructure extends ACountable { - - protected final long count; - - protected ADataStructure(long count) { - this.count=count; - } - - /** - * Gets the count of elements in this data structure - */ - @Override - public final long count() { - return count; - } - - @Override - public final int size() { - return (int) (Math.min(count, Integer.MAX_VALUE)); - } - - /** - * Returns an empty instance of the same general type as this data structure. - * - * @return An empty data structure - */ - public abstract ADataStructure empty(); - - @Override - public final boolean isEmpty() { - return count==0L; - } - - /** - * Adds an element to this data structure, in the natural manner defined by the - * general data structure type. e.g. append at the end of a vector. - * - * @param Type of Value added - * @param x New element to add - * @return The updated data structure, or null if a failure occurred due to invalid element type - */ - public abstract ADataStructure conj(R x); - - /** - * Adds multiple elements to this data structure, in the natural manner defined by the - * general data structure type. e.g. append at the end of a vector. - * - * This may be more efficient than using 'conj' for individual items. - * - * @param Type of Value added - * @param xs New elements to add - * @return The updated data structure, or null if a failure occurred due to invalid elementtypes - */ - @SuppressWarnings("unchecked") - public ADataStructure conjAll(ACollection xs) { - ADataStructure result=(ADataStructure) this; - for (R x: xs) { - result=result.conj(x); - if (result==null) return null; - } - return result; - } - - /** - * Associates a key with a value in this associative data structure. - * - * May return null if the Key or Value is incompatible with the data structure. - * - * @param key Associative key - * @param value Value to associate with key - * @return Updates data structure, or null if data types are invalid - */ - public abstract ADataStructure assoc(ACell key,ACell value); - - /** - * Get the value associated with a given key. - * - * @param key Associative key to look up - * @return Value from collection, or a falsey value (null or false) if not found - */ - public abstract ACell get(ACell key); - - /** - * Get the value associated with a given key. - * - * @param key Key to look up in data structure - * @param notFound Value to return if key is not found - * @return Value from collection, or notFound value if not found - */ - public abstract ACell get(ACell key, ACell notFound); - - - /** - * Checks if the data structure contains the specified key - * - * @param key Associative key to look up - * @return true if the data structure contains the key, false otherwise - */ - public abstract boolean containsKey(ACell key); - -} diff --git a/convex-core/src/main/java/convex/core/data/AHashMap.java b/convex-core/src/main/java/convex/core/data/AHashMap.java deleted file mode 100644 index 7727f1190..000000000 --- a/convex-core/src/main/java/convex/core/data/AHashMap.java +++ /dev/null @@ -1,171 +0,0 @@ -package convex.core.data; - -import java.util.function.Function; -import java.util.function.Predicate; - -import convex.core.data.type.Types; -import convex.core.exceptions.InvalidDataException; -import convex.core.util.MergeFunction; -import convex.core.util.Utils; - -public abstract class AHashMap extends AMap { - - protected AHashMap(long count) { - super(count); - } - - @Override - public AHashMap empty() { - return Maps.empty(); - } - - /** - * Dissoc given a Ref to the key value. - * @param key Ref of key to remove - * @return Map with specified key removed. - */ - public abstract AHashMap dissocRef(Ref key); - - public abstract AHashMap assocRef(Ref keyRef, V value); - - @Override - public abstract AHashMap assoc(ACell key, ACell value); - - @Override - public abstract AHashMap dissoc(ACell key); - - protected abstract AHashMap assocRef(Ref keyRef, V value, int shift); - - public abstract AHashMap assocEntry(MapEntry e); - - protected abstract AHashMap assocEntry(MapEntry e, int shift); - - /** - * Merge another map into this map. Replaces existing entries if they are - * different - * - * O(n) in size of map to merge. - * - * @param m HashMap to merge into this HashMap - * @return Merged HashMap - */ - public AHashMap merge(AHashMap m) { - AHashMap result = this; - long n = m.count(); - for (int i = 0; i < n; i++) { - result = result.assocEntry(m.entryAt(i)); - } - return result; - } - - /** - * Merge this map with another map, using the given function for each key that - * is present in either map and has a different value - * - * The function is passed null for missing values in either map, and must return - * type V. - * - * If the function returns null, the entry is removed. - * - * Returns the same map if no changes occurred. - * - * @param b Other map to merge with - * @param func Merge function, returning a new value for each key - * @return A merged map, or this map if no changes occurred - */ - public abstract AHashMap mergeDifferences(AHashMap b, MergeFunction func); - - protected abstract AHashMap mergeDifferences(AHashMap b, MergeFunction func, int shift); - - /** - * Merge this map with another map, using the given function for each key that - * is present in either map. The function is applied to the corresponding values - * with the same key. - * - * The function is passed null for missing values in either map, and must return - * type V. - * - * If the function returns null, the entry is removed. - * - * Returns the same map if no changes occurred. - * - * PERF WARNING: This method's contract requires calling the function on all - * values in both sets, which will cause a full data structure traversal. If the - * function will only return one or other of the compared values consider using - * mergeDifferences instead. - * - * @param b Other map to merge with - * @param func Merge function, returning a new value for each key - * @return A merged map, or this map if no changes occurred - */ - public abstract AHashMap mergeWith(AHashMap b, MergeFunction func); - - protected abstract AHashMap mergeWith(AHashMap b, MergeFunction func, int shift); - - @Override - public AHashMap filterValues(Predicate pred) { - // TODO make more efficient? - return mergeWith(this, (a, b) -> pred.test(a) ? a : null); - } - - @Override - public boolean equals(AMap a) { - if (this == a) return true; // important optimisation for e.g. hashmap equality - if (a == null) return false; - if (a.getType()!=Types.MAP) return false; - long n=this.count(); - if (n != a.count()) return false; - return getHash().equals(a.getHash()); - } - - /** - * Maps a function over all entries in this Map to produce updated entries. - * - * May not change keys, but may return null to remove an entry. - * - * @param func A function that maps old map entries to updated map entries. - * @return The updated Map, or this Map if no changes - */ - public abstract AHashMap mapEntries(Function, MapEntry> func); - - /** - * Validates the map with a given hex prefix. This is necessary to ensure that - * child maps are valid, in particular have the correct shift level and that all - * key hashes start with the correct prefix of hex characters. - * - * TODO: consider faster way of passing prefix than hex string, probably a - * byte[] stack. - * - * @param string - * @throws InvalidDataException - */ - protected abstract void validateWithPrefix(String string) throws InvalidDataException; - - @Override - public abstract AHashMap updateRefs(IRefFunction func); - - /** - * Returns true if this map contains all the same keys as another map - * @param map Map to compare with - * @return True if this map contains all the keys of the other - */ - public abstract boolean containsAllKeys(AHashMap map); - - /** - * Writes this HashMap to a byte array. Will include values by default. - * @param bs Byte array to encode into - * @param pos Start position to encode at - * @return Updated position - */ - public abstract int encode(byte[] bs, int pos); - - public AVector getKeys() { - int n=Utils.checkedInt(count); - ACell[] keys=new ACell[n]; - for (int i=0; i extends ASet { - - protected static final int OP_UNION=1; - protected static final int OP_INTERSECTION=2; - protected static final int OP_DIFF_LEFT=3; - protected static final int OP_DIFF_RIGHT=4; - - protected static final int MAX_SHIFT = Hash.LENGTH*2-1; - - - protected AHashSet(long count) { - super(count); - } - - protected abstract AHashSet mergeWith(AHashSet b, int setOp); - - protected abstract AHashSet mergeWith(AHashSet b, int setOp, int shift); - - @SuppressWarnings("unchecked") - public ASet includeAll(ASet elements) { - return (ASet) mergeWith((AHashSet) elements,OP_UNION); - }; - - protected final int reverseOp(int setOp) { - if (setOp>=OP_DIFF_LEFT) { - setOp=OP_DIFF_LEFT+OP_DIFF_RIGHT-setOp; - } - return setOp; - } - - protected final Ref applyOp(int setOp, Ref a, Ref b) { - switch (setOp) { - case OP_UNION: return (a==null)?b:a; - case OP_INTERSECTION: return (a==null)?null:((b==null)?null:a); - case OP_DIFF_LEFT: return (a==null)?null:((b==null)?a:null); - case OP_DIFF_RIGHT: return (b==null)?null:((a==null)?b:null); - default: throw new Error("Invalid setOp: "+setOp); - } - } - - protected final AHashSet applySelf(int setOp) { - switch (setOp) { - case OP_UNION: return this; - case OP_INTERSECTION: return this; - case OP_DIFF_LEFT: return Sets.empty(); - case OP_DIFF_RIGHT: return Sets.empty(); - default: throw new Error("Invalid setOp: "+setOp); - } - } - - public ASet intersectAll(ASet elements) { - return mergeWith((AHashSet) elements,OP_INTERSECTION); - }; - - public ASet excludeAll(ASet elements) { - return mergeWith((AHashSet) elements,OP_DIFF_LEFT); - }; - - public abstract AHashSet toCanonical(); - - public ASet conjAll(ACollection elements) { - if (elements instanceof AHashSet) return includeAll((AHashSet) elements); - @SuppressWarnings("unchecked") - AHashSet result=(AHashSet) this; - long n=elements.count(); - for (long i=0; i disjAll(ACollection b) { - if (b instanceof AHashSet) return excludeAll((AHashSet) b); - AHashSet result=this; - long n=b.count(); - for (long i=0; i excludeRef(Ref valueRef); - - public abstract AHashSet includeRef(Ref ref) ; - - @SuppressWarnings("unchecked") - @Override - public AHashSet conj(R a) { - return (AHashSet) includeRef((Ref) Ref.get(a)); - } - - @Override - public ASet exclude(T a) { - return excludeRef((Ref) Ref.get(a)); - } - - @SuppressWarnings("unchecked") - @Override - public AHashSet include(R a) { - return (AHashSet) includeRef((Ref) Ref.get(a)); - } - - /** - * Validates the set with a given hex prefix. This is necessary to ensure that - * child maps are valid, in particular have the correct shift level and that all - * hashes start with the correct prefix of hex characters. - * - * @param prefix Hash for earlier prefix values - * @param digit Hex digit expected at position [shift] - * @throws InvalidDataException - */ - protected abstract void validateWithPrefix(Hash prefix, int digit, int shift) throws InvalidDataException; - - @Override - public Object[] toArray() { - int s = size(); - Object[] result = new Object[s]; - copyToArray(result, 0); - return result; - } - - @Override - public final CVMBool get(ACell key) { - Ref me = getValueRef(key); - if (me == null) return CVMBool.FALSE; - return CVMBool.TRUE; - } - - /** - * Gets the Value in the set for the given hash, or null if not found - * @param hash Hash of value to check in set - * @return The Value for the given Hash if found, null otherwise. - */ - public T getByHash(Hash hash) { - Ref ref=getRefByHash(hash); - if (ref==null) return null; - return ref.getValue(); - } - - protected abstract AHashSet includeRef(Ref e, int i); - - /** - * Tests if this Set contains a given hash - * @param hash Hash to test for set membership - * @return True if set contains value for given hash, false otherwise - */ - public abstract boolean containsHash(Hash hash); - - @Override - public boolean contains(ACell key) { - return getValueRef(key) != null; - } -} diff --git a/convex-core/src/main/java/convex/core/data/AList.java b/convex-core/src/main/java/convex/core/data/AList.java deleted file mode 100644 index 471073d40..000000000 --- a/convex-core/src/main/java/convex/core/data/AList.java +++ /dev/null @@ -1,74 +0,0 @@ -package convex.core.data; - -import java.util.function.Function; - -import convex.core.data.type.AType; -import convex.core.data.type.Types; - -/** - * Abstract base class for lists. - * - * Lists are immutable sequences of values, with efficient access and change to - * the head of the list. Lists are most importantly used for representing code - * as data, in the fine tradition of Lisp. - * - * For general manipulation of sequential data, vectors are recommended. - * - * There are multiple possible implementations for different list types, but all - * should conform to the general AList interface. We use an abstract base class - * in preference to an interface because we control the hierarchy and it offers - * some mild performance advantages. - * - * General design goals: - Immutability - Optimised performance for front of - * list (cons, first etc.) - Able to share vector implementations where - * appropriate - * - * @param Type of list - */ -public abstract class AList extends ASequence { - - public AList(long count) { - super(count); - } - - @Override - public final AType getType() { - return Types.LIST; - } - - @Override - public abstract AList cons(T x); - - /** - * Adds an element to this list, in first position. - * - * Returns a new list. - */ - @Override - public abstract AList conj(R x); - - @Override - public AList empty() { - return Lists.empty(); - } - - @Override - public abstract AList map(Function mapper); - - @Override - public abstract AList concat(ASequence vals); - - @Override - public abstract AList assoc(long i, R value); - - // TODO: make sure this is O(1)? - /** - * Drops elements from the front of the list. - * @param n Number of elements to drop - * @return List with n elements removed, or null if not possible - */ - public abstract AList drop(long n); - - - -} diff --git a/convex-core/src/main/java/convex/core/data/ALongBlob.java b/convex-core/src/main/java/convex/core/data/ALongBlob.java deleted file mode 100644 index f02c15cb9..000000000 --- a/convex-core/src/main/java/convex/core/data/ALongBlob.java +++ /dev/null @@ -1,150 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; - -import convex.core.util.Errors; -import convex.core.util.Utils; - -public abstract class ALongBlob extends ABlob { - - protected static final long LENGTH = 8; - - protected final long value; - - protected ALongBlob(long value) { - this.value=value; - } - - @Override - public final long count() { - return 8; - } - - @Override - @SuppressWarnings("unchecked") - protected Ref createRef() { - // Create Ref at maximum status to reflect internal embedded nature - Ref newRef= RefDirect.create(this,cachedHash(),Ref.INTERNAL|Ref.KNOWN_EMBEDDED_MASK); - cachedRef=newRef; - return (Ref) newRef; - } - - @Override - public final String toHexString() { - return Utils.toHexString(value); - } - - @Override - public abstract ABlob slice(long start, long length); - - @Override - public abstract Blob toBlob(); - - @Override - public long commonHexPrefixLength(ABlob b) { - return toBlob().commonHexPrefixLength(b); - } - - private void checkIndex(long i) { - if ((i < 0) || (i >= LENGTH)) throw new IndexOutOfBoundsException(Errors.badIndex(i)); - } - - @Override - public final byte byteAt(long i) { - checkIndex(i); - return (byte) (value >> ((LENGTH - i - 1) * 8)); - } - - @Override - public final byte getUnchecked(long i) { - return (byte) (value >> ((LENGTH - i - 1) * 8)); - } - - @Override - public final ABlob append(ABlob d) { - return toBlob().append(d); - } - - @Override - public abstract boolean equals(ABlob o); - - @Override - public final ByteBuffer writeToBuffer(ByteBuffer bb) { - return bb.putLong(value); - } - - @Override - public final int writeToBuffer(byte[] bs, int pos) { - Utils.writeLong(bs, pos, value); - return pos+8; - } - - @Override - public final Blob getChunk(long i) { - if (i == 0L) return toBlob(); - throw new IndexOutOfBoundsException(Errors.badIndex(i)); - } - - @Override - public final ByteBuffer getByteBuffer() { - return toBlob().getByteBuffer(); - } - - @Override - protected final long calcMemorySize() { - // always embedded and no child Refs, so memory size == 0 - return 0; - } - - @Override - public final void toHexString(StringBuilder sb) { - String s= Utils.toHexString(value); - sb.append(s); - } - - @Override - public long hexMatchLength(ABlob b, long start, long length) { - return toBlob().hexMatchLength(b,start,length); - } - - @Override - public final long toLong() { - return value; - } - - @Override - public long longValue() { - return value; - } - - @Override - public final boolean equalsBytes(ABlob b) { - if (b.count()!=LENGTH) return false; - return value==b.longValue(); - } - - @Override - public abstract byte getTag(); - - @Override - public boolean isCanonical() { - return true; - } - - @Override - public boolean isCVMValue() { - return true; - } - - @Override - public final int getRefCount() { - return 0; - } - - @Override - public final boolean isEmbedded() { - // Always embedded - return true; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/AMap.java b/convex-core/src/main/java/convex/core/data/AMap.java deleted file mode 100644 index 317a0420e..000000000 --- a/convex-core/src/main/java/convex/core/data/AMap.java +++ /dev/null @@ -1,323 +0,0 @@ -package convex.core.data; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Predicate; - -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.TODOException; -import convex.core.lang.RT; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * Abstract base class for maps. - * - * Maps are Smart Data Structures that represent an immutable mapping of keys to - * values. The can also be seen as a data structure where the elements are map entries - * (equivalent to length 2 vectors) - * - * Ordering of map entries (as seen through iterators etc.) depends on map type. - * - * @param Type of keys - * @param Type of values - */ -public abstract class AMap extends ADataStructure> - implements Map { - - protected AMap(long count) { - super(count); - } - - @Override - public AType getType() { - return Types.MAP; - } - - /** - * Gets the values from this map, in map-determined order - */ - @Override - public AVector values() { - int len = size(); - ArrayList al = new ArrayList(len); - accumulateValues(al); - return Vectors.create(al); - } - - // TODO: Review plausible alternative implementation for values() - // - // @Override - // public AVector values() { - // return reduceValues((v,e)->((AVector)v).append(e), Vectors.empty()); - // } - - /** - * Associates the given key with the specified value. - * - * @param key Map key to associate - * @param value Map value - * @return An updated map with the new association, or null if the association fails - */ - public abstract AMap assoc(ACell key, ACell value); - - - /** - * Dissociates a key from this map, returning an updated map if the key was - * removed, or the same unchanged map if the key is not present. - * - * @param key Key to remove. - * @return Updated map - */ - public abstract AMap dissoc(ACell key); - - public final boolean containsKeyRef(Ref ref) { - return getKeyRefEntry(ref) != null; - } - - @SuppressWarnings("unchecked") - public boolean containsKey(ACell key) { - return getEntry((K)key)!=null; - } - - @Override - public final boolean containsKey(Object key) { - if ((key==null)||(key instanceof ACell)) { - return containsKey((ACell)key); - } - return false; - } - - /** - * Get an entry given a Ref to the key value. This is more efficient than - * directly looking up using the key for some map types, and should be preferred - * if the caller already has a Ref available. - * - * @param ref Ref to Map key - * @return MapEntry for the given key ref - */ - public abstract MapEntry getKeyRefEntry(Ref ref); - - /** - * Accumulate all entries from this map in the given HashSet. - * - * @param h HashSet in which to accumulate entries - */ - protected abstract void accumulateEntrySet(HashSet> h); - - /** - * Accumulate all keys from this map in the given HashSet. - * - * @param h HashSet in which to accumulate keys - */ - protected abstract void accumulateKeySet(HashSet h); - - /** - * Accumulate all values from this map in the given ArrayList. - * - * @param al ArrayList in which to accumulate values - */ - protected abstract void accumulateValues(ArrayList al); - - @Override - public final V put(K key, V value) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public final V remove(Object key) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public final void putAll(Map m) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public final void clear() { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public abstract void forEach(BiConsumer action); - - @Override - public void print(StringBuilder sb) { - sb.append('{'); - this.forEach((k, v) -> { - Utils.print(sb,k); - sb.append(' '); - Utils.print(sb,v); - sb.append(','); - }); - if (count() > 0) sb.setLength(sb.length() - 1); // delete trailing comma - sb.append('}'); - } - - /** - * Associate the given map entry into the map. May return null if the map entry is not valid for this map type. - * - * @param e A map entry - * @return The updated map - */ - public abstract AMap assocEntry(MapEntry e); - - /** - * Gets the entry in this map at a specified index, according to the - * map-specific order. - * - * @param i Index of entry - * @return MapEntry at the specified index. - * @throws IndexOutOfBoundsException If this index is not valid - */ - public abstract MapEntry entryAt(long i); - - @Override - public Ref> getElementRef(long index) { - return entryAt(index).getRef(); - } - - @Override - public final MapEntry get(long i) { - return entryAt(i); - } - - /** - * Gets the MapEntry for the given key - * - * @param k Key to lookup in Map - * @return The map entry, or null if the key is not found - */ - public abstract MapEntry getEntry(ACell k); - - @Override - public V get(Object key) { - if (key instanceof ACell) return (V) get((ACell)key); - return null; - } - - public abstract V get(ACell key); - - /** - * Gets the value at a specified key, or returns the fallback value if not found - * - * @param key Key to lookup in Map - * @param notFound Fallback value to return if key is not present - * @return Value for the specified key, or the notFound value. - */ - @SuppressWarnings("unchecked") - public final V get(ACell key, ACell notFound) { - MapEntry me = getEntry((K) key); - if (me == null) { - return (V) notFound; - } else { - return me.getValue(); - } - } - - /** - * Reduce over all values in this map - * - * @param Type of reduction return value - * @param func A function taking the reduction value and a map value - * @param initial Initial reduction value - * @return The final reduction value - */ - public abstract R reduceValues(BiFunction func, R initial); - - - /** - * Filters all values in this map with the given predicate. - * - * @param pred A predicate specifying which elements to retain. - * @return The updated map containing those entries where the predicate returned - * true. - */ - public AMap filterValues(Predicate pred) { - throw new TODOException(); - } - - /** - * Reduce over all map entries in this map - * - * @param Type of reduction return value - * @param func A function taking the reduction value and a map entry - * @param initial Initial reduction value - * @return The final reduction value - */ - public abstract R reduceEntries(BiFunction, ? extends R> func, R initial); - - @SuppressWarnings("unchecked") - @Override - public Set keySet() { - ASet ks=reduceEntries((s,me)->s.conj(me.getKey()), (ASet)(Sets.empty())); - return ks; - } - - - /** - * Returns true if this map has exactly the same keys as the other map - * - * @param map Map to compare with - * @return true if maps have the same keys, false otherwise - */ - public abstract boolean equalsKeys(AMap map); - - @SuppressWarnings("unchecked") - @Override - public final boolean equals(ACell a) { - if (!(a instanceof AMap)) return false; - return equals((AMap) a); - } - - /** - * Checks this map for equality with another map. In general, maps should be - * considered equal if they have the same canonical representation, i.e. the - * same hash value. - * - * Subclasses may override this this they have a more efficient equals - * implementation or a more specific definition of equality. - * - * @param a Map to compare with - * @return true if maps are equal, false otherwise. - */ - public abstract boolean equals(AMap a); - - /** - * Gets the map entry with the specified hash - * - * @param hash Hash of key to lookup - * @return The specified MapEntry, or null if not found. - */ - protected abstract MapEntry getEntryByHash(Hash hash); - - /** - * Adds a new map entry to this map. The argument must be a valid map entry or - * length 2 vector. - * - * @param x An object that can be cast to a MapEntry - * @return Updated map with the specified entry added, or null if the argument - * is not a valid map entry - */ - @SuppressWarnings("unchecked") - public ADataStructure conj(R x) { - MapEntry me = RT.ensureMapEntry(x); - if (me == null) return null; - return (ADataStructure) assocEntry(me); - } - - /** - * Gets a vector of all map entries. - * - * @return Vector map entries, in map-defined order. - */ - public AVector> entryVector() { - return reduceEntries((acc, e) -> acc.conj(e), Vectors.empty()); - } -} diff --git a/convex-core/src/main/java/convex/core/data/AMapEntry.java b/convex-core/src/main/java/convex/core/data/AMapEntry.java deleted file mode 100644 index 0b8db0e79..000000000 --- a/convex-core/src/main/java/convex/core/data/AMapEntry.java +++ /dev/null @@ -1,156 +0,0 @@ -package convex.core.data; - -import java.util.Iterator; -import java.util.ListIterator; -import java.util.Map; -import java.util.Spliterator; -import java.util.function.Consumer; -import java.util.function.Predicate; - -import convex.core.exceptions.TODOException; -import convex.core.util.Errors; -import convex.core.util.Utils; - -public abstract class AMapEntry extends AVector implements Map.Entry { - - public AMapEntry(long count) { - super(2); - } - - @Override - public abstract ACell get(long i); - - @Override - public final AVector appendChunk(VectorLeaf listVector) { - throw new IllegalArgumentException("Can't append chunk to a MapEntry of size: 2"); - } - - @Override - public final VectorLeaf getChunk(long offset) { - return toVector().getChunk(offset); - } - - @Override - public final boolean isPacked() { - return false; - } - - @Override - public abstract int getRefCount(); - - @Override - public abstract Ref getRef(int i); - - @Override - public abstract K getKey(); - - @Override - public abstract V getValue(); - - @Override - public final V setValue(V value) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public abstract boolean isCanonical(); - - @Override - public AVector append(ACell value) { - return toVector().append(value); - } - - @Override - public Spliterator spliterator(long position) { - return toVector().spliterator(position); - } - - @Override - public ListIterator listIterator(long index) { - return toVector().listIterator(index); - } - - @Override - public ListIterator listIterator() { - return toVector().listIterator(); - } - - @Override - public Iterator iterator() { - return toVector().iterator(); - } - - @Override - public long longIndexOf(Object o) { - return toVector().longIndexOf(o); - } - - @Override - public long longLastIndexOf(Object o) { - return toVector().longLastIndexOf(o); - } - - @Override - public long commonPrefixLength(AVector b) { - if (b == this) return 2; - long bc = b.count(); - if (bc == 0) return 0; - if (!Utils.equals(getKey(), b.get(0))) return 0; - if (bc == 1) return 1; - if (!Utils.equals(getValue(), b.get(1))) return 1; - return 2; - } - - /** - * Create a new MapEntry with an updated key. Shares old value. Returns the same - * MapEntry if unchanged - * - * @param key Key to update - * @return - */ - protected abstract AMapEntry withKey(K key); - - /** - * Create a new MapEntry with an updated value. Shares old key. Returns the same - * MapEntry if unchanged - * - * @param value Value to update - * @return - */ - protected abstract AMapEntry withValue(V value); - - @Override - public AVector next() { - return Vectors.of(getValue()); - } - - @Override - public abstract int encode(byte[] bs, int pos); - - @Override - public final ACell set(int index, ACell element) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean anyMatch(Predicate pred) { - return toVector().anyMatch(pred); - } - - @Override - public boolean allMatch(Predicate pred) { - return toVector().allMatch(pred); - } - - @Override - public void forEach(Consumer action) { - action.accept(getKey()); - action.accept(getValue()); - } - - @Override - public T[] toArray(T[] a) { - throw new TODOException(); - } - -} diff --git a/convex-core/src/main/java/convex/core/data/ANumericBlob.java b/convex-core/src/main/java/convex/core/data/ANumericBlob.java deleted file mode 100644 index f432b6e49..000000000 --- a/convex-core/src/main/java/convex/core/data/ANumericBlob.java +++ /dev/null @@ -1,65 +0,0 @@ -package convex.core.data; - -import java.util.Arrays; - -import convex.core.exceptions.TODOException; - -/** - * Base class for Blobs which represent an integral numeric value - */ -public abstract class ANumericBlob extends AArrayBlob { - - protected ANumericBlob(byte[] bytes, int offset, int length) { - super(bytes, offset, length); - } - - @Override - public int estimatedEncodingSize() { - // Tag+reasonable length+raw bytes - return 10+length; - } - - @Override - public boolean equals(ABlob a) { - if (a instanceof ANumericBlob) { - return equals((ANumericBlob)a); - } - return false; - } - - public boolean equals(ANumericBlob a) { - // TODO: should be overridden to handle specific types - return Arrays.equals(store, offset, offset+length, a.store, a.offset, a.offset+a.length); - } - - @Override - public Blob getChunk(long i) { - return toBlob().getChunk(i); - } - - @Override - public boolean isRegularBlob() { - return false; - } - - - // TODO: these should be abstract - @Override - public int encode(byte[] bs, int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isCanonical() { - return false; - } - - @Override public final boolean isCVMValue() { - return true; - } - - @Override - public byte getTag() { - throw new TODOException(); - } -} diff --git a/convex-core/src/main/java/convex/core/data/AObject.java b/convex-core/src/main/java/convex/core/data/AObject.java deleted file mode 100644 index 970e4d6f5..000000000 --- a/convex-core/src/main/java/convex/core/data/AObject.java +++ /dev/null @@ -1,56 +0,0 @@ -package convex.core.data; - -public abstract class AObject { - /** - * We cache the Blob for the binary encoding of this Cell - */ - protected Blob encoding; - - /** - * Prints this Object to a readable String Representation - * - * @param sb StringBuilder to append to - */ - public abstract void print(StringBuilder sb); - - /** - * Renders this object as a String value - * @return String representation - */ - public final String print() { - StringBuilder sb = new StringBuilder(); - print(sb); - return sb.toString(); - } - - /** - * Gets the encoded byte representation of this cell. - * - * @return A blob representing this cell in encoded form - */ - public Blob getEncoding() { - if (encoding==null) encoding=createEncoding(); - return encoding; - } - - /** - * Creates a Blob object representing this object. Should be called only after - * the cached encoding has been checked. - * - * @return Blob Encoding of Object - */ - protected abstract Blob createEncoding(); - - /** - * Attach the given encoding Blob to this object, if no encoding is currently cached - * - * Warning: Blob must be the correct canonical representation of this Cell, - * otherwise bad things may happen (incorrect hashcode, etc.) - * - * @param data Encoding of Value. Must be a correct canonical encoding. - */ - public final void attachEncoding(Blob data) { - this.encoding=data; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/ARecord.java b/convex-core/src/main/java/convex/core/data/ARecord.java deleted file mode 100644 index 51eae1698..000000000 --- a/convex-core/src/main/java/convex/core/data/ARecord.java +++ /dev/null @@ -1,340 +0,0 @@ -package convex.core.data; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; - -import convex.core.Block; -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.lang.impl.RecordFormat; -import convex.core.util.Utils; - -/** - * Base class for record data types. - * - * Records are map-like data structures with fixed sets of keys, and optional custom behaviour. - * - * Ordering of fields is defined by the Record's RecordFormat - * - */ -public abstract class ARecord extends AMap { - - protected final RecordFormat format; - - // TODO: need a better default value? - public static final ARecord DEFAULT_VALUE=Block.create(0, AccountKey.ZERO, Vectors.empty()); - - protected ARecord(RecordFormat format) { - super(format.count()); - this.format=format; - } - - public AType getType() { - return Types.RECORD; - } - - @Override - public int estimatedEncodingSize() { - return (int) (Format.MAX_EMBEDDED_LENGTH*format.count()); - } - - @Override - public boolean isCanonical() { - // Records should always be canonical - return true; - } - - @Override public final boolean isCVMValue() { - return true; - } - - /** - * Writes the raw fields of this record in declared order - * @param bs Array to write to - */ - @Override - public int encodeRaw(byte[] bs, int pos) { - List keys=getKeys(); - for (Keyword key: keys) { - pos=Format.write(bs,pos, get(key)); - } - return pos; - } - - @Override - public void print(StringBuilder sb) { - sb.append("{"); - long n=format.count(); - for (int i=0; i me=entryAt(i); - Keyword k=me.getKey(); - k.print(sb); - sb.append(' '); - Object v=me.getValue(); - Utils.print(sb, v); - if (i<(n-1)) sb.append(','); - } - sb.append("}"); - } - - /** - * Gets a vector of keys for this record - * - * @return Vector of Keywords - */ - public final AVector getKeys() { - return format.getKeys(); - } - - /** - * Gets a vector of values for this record, in format-determined order - * - * @return Vector of Values - */ - @Override - public AVector values() { - int n=size(); - ACell[] os=new ACell[n]; - for (int i=0; i Ref getRef(int index) { - long n=size(); - int si=index; - if (index<0) throw new IndexOutOfBoundsException("Negative ref index: "+index); - for (int i=0; i keys=getKeys(); - for (int i=0; i keys=format.getKeys(); - for (int i=0; i keySet() { - return format.keySet(); - } - - @Override - public Set> entrySet() { - return toHashMap().entrySet(); - } - - @Override - public AMap assoc(ACell key, ACell value) { - // TODO: OK to convert records to hashmaps? - return toHashMap().assoc(key, value); - } - - public AMap dissoc(Keyword key) { - if (!containsKey(key)) return this; - return toHashMap().dissoc(key); - } - - @Override - public AMap dissoc(ACell key) { - if (!containsKey(key)) return this; - return toHashMap().dissoc(key); - } - - @Override - public MapEntry getKeyRefEntry(Ref ref) { - // TODO: could maybe be more efficient? - return getEntry(ref.getValue()); - } - - @Override - protected void accumulateEntrySet(HashSet> h) { - for (long i=0; i h) { - AVector keys=format.getKeys(); - for (long i=0; i al) { - toHashMap().accumulateValues(al); - } - - @Override - public void forEach(BiConsumer action) { - throw new UnsupportedOperationException(); - } - - @Override - public AMap assocEntry(MapEntry e) { - return assoc(e.getKey(),e.getValue()); - } - - @Override - public MapEntry entryAt(long i) { - if ((i<0)||(i>=count)) throw new IndexOutOfBoundsException("Index:"+i); - Keyword k=format.getKeys().get(i); - return getEntry(k); - } - - @Override - public MapEntry getEntry(ACell k) { - if (!containsKey(k)) return null; - return MapEntry.create((Keyword)k,get(k)); - } - - @Override - public R reduceValues(BiFunction func, R initial) { - for (int i=0; i R reduceEntries(BiFunction, ? extends R> func, R initial) { - for (int i=0; i map) { - return toHashMap().equalsKeys(map); - } - - /** - * Converts this record to a hashmap - * @return HashMap instance - */ - protected AHashMap toHashMap() { - AHashMap m=Maps.empty(); - for (int i=0; i getEntryByHash(Hash hash) { - return toHashMap().getEntryByHash(hash); - } - - @Override - public AHashMap empty() { - // coerce to AHashMap since we are removing all keys - return Maps.empty(); - } - - /** - * Gets the RecordFormat instance that describes this Record's layout - * @return RecordFormat instance - */ - public RecordFormat getFormat() { - return format; - } - - @Override - public ARecord toCanonical() { - // Should already be canonical - return this; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/ARecordGeneric.java b/convex-core/src/main/java/convex/core/data/ARecordGeneric.java deleted file mode 100644 index e0e15066f..000000000 --- a/convex-core/src/main/java/convex/core/data/ARecordGeneric.java +++ /dev/null @@ -1,102 +0,0 @@ -package convex.core.data; - -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.impl.RecordFormat; -import convex.core.util.Utils; - -/** - * Abstract base class for generic records. - * - * Generic records are backed by a vector - */ -public abstract class ARecordGeneric extends ARecord { - - protected AVector values; - - protected ARecordGeneric(RecordFormat format, AVector values) { - super(format); - if (values.count()!=format.count()) throw new IllegalArgumentException("Wrong number of field values for record: "+values.count()); - this.values=values; - } - - @Override - public MapEntry entryAt(long i) { - return MapEntry.create(format.getKey(Utils.checkedInt(i)), values.get(i)); - } - - @Override - public ACell get(ACell key) { - Long ix=format.indexFor(key); - if (ix==null) return null; - return values.get((long)ix); - } - - @Override - public abstract byte getTag(); - - @Override - public int getRefCount() { - return values.getRefCount(); - } - - @Override - public boolean equals(AMap a) { - if (this == a) return true; // important optimisation for e.g. hashmap equality - if (a == null) return false; - if (a.getTag()!=getTag()) return false; - Hash h=this.cachedHash(); - if (h!=null) { - Hash ha=a.cachedHash(); - if (ha!=null) return Utils.equals(h, ha); - } - return values.equals(((ARecordGeneric)a).values); - } - - @Override - public Ref getRef(int index) { - return values.getRef(index); - } - - @Override - public ARecord updateRefs(IRefFunction func) { - AVector newValues=values.updateRefs(func); - return withValues(newValues); - } - - @Override - protected ARecord updateAll(ACell[] newVals) { - int n=size(); - if (newVals.length!=n) throw new IllegalArgumentException("Wrong number of values: "+newVals.length); - boolean changed = false; - for (int i=0; i newVector=Vectors.create(newVals); - return withValues(newVector); - } - - @Override - public AVector values() { - return values; - } - - /** - * Updates the record with a new set of values. - * - * Returns this if and only if values vector is identical. - * - * @param newValues New values to use - * @return Updated Record - */ - protected abstract ARecord withValues(AVector newValues); - - @Override - public void validateCell() throws InvalidDataException { - values.validateCell(); - } - -} diff --git a/convex-core/src/main/java/convex/core/data/ASequence.java b/convex-core/src/main/java/convex/core/data/ASequence.java deleted file mode 100644 index f30ee34a7..000000000 --- a/convex-core/src/main/java/convex/core/data/ASequence.java +++ /dev/null @@ -1,262 +0,0 @@ -package convex.core.data; - -import java.util.Collection; -import java.util.List; -import java.util.ListIterator; -import java.util.function.Consumer; -import java.util.function.Function; - -import convex.core.data.prim.CVMLong; -import convex.core.lang.RT; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * Abstract base class for persistent lists and vectors - * - * @param Type of list elements - */ -public abstract class ASequence extends ACollection implements List, IAssociative { - - public ASequence(long count) { - super(count); - } - - @Override - public boolean contains(Object o) { - return longIndexOf(o) >= 0; - } - - /** - * Gets the first long index at which the specified value appears in the the sequence. - * @param value Any value which could appear as an element of the sequence. - * @return Index of the value, or -1 if not found. - */ - public abstract long longIndexOf(Object value); - - /** - * Gets the last long index at which the specified value appears in the the sequence. - * @param value Any value which could appear as an element of the sequence. - * @return Index of the value, or -1 if not found. - */ - public abstract long longLastIndexOf(Object value); - - public abstract ASequence map(Function mapper); - - @Override - public abstract void forEach(Consumer action); - - /** - * Visits all elements in this sequence, calling the specified consumer for each. - * - * @param f Function to call for each element - */ - public abstract void visitElementRefs(Consumer> f); - - @SuppressWarnings("unchecked") - public ASequence flatMap(Function> mapper) { - ASequence> vals = this.map(mapper); - ASequence result = (ASequence) this.empty(); - for (ASequence seq : vals) { - result = result.concat(seq); - } - return result; - } - - /** - * Concatenates the elements from another sequence to the end of this sequence. - * Potentially O(n) in size of resulting sequence - * - * @param vals A sequence of values to concatenate. - * @return The concatenated sequence, of the same type as this sequence. - */ - public abstract ASequence concat(ASequence vals); - - @Override - public final boolean addAll(int index, Collection c) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - /** - * Gets the sequence of all elements after the first, or null if no elements - * remain - * - * @return Sequence following the first element - */ - public abstract ASequence next(); - - /** - * Gets the element at the specified index in this sequence. - * - * Behaves as if the index was considered as a long - * - * @param index Index of element to get - * @return Element at the specified index - */ - @Override - public T get(int index) { - return get((long) index); - } - - @Override - public abstract T get(long index); - - /** - * Gets the element at the specified key - * - * @param key Key of element to get - * @return The value at the specified index, or null if not valid - */ - @Override - public T get(ACell key) { - if (key instanceof CVMLong) { - long ix = ((CVMLong) key).longValue(); - if ((ix >= 0) && (ix < count())) return get(ix); - } - return null; - } - - @SuppressWarnings("unchecked") - @Override - public ACell get(ACell key, ACell notFound) { - if (key instanceof CVMLong) { - long ix = ((CVMLong) key).longValue(); - if ((ix >= 0) && (ix < count())) return get(ix); - } - return (T) notFound; - } - - @Override - public boolean containsKey(ACell key) { - if (key instanceof CVMLong) { - long ix = ((CVMLong) key).longValue(); - if ((ix >= 0) && (ix < count())) return true; - } - return false; - } - - - - /** - * Gets the element Ref at the specified index - * - * @param index Index of element to get - * @return Ref to element at specified index - */ - public abstract Ref getElementRef(long index); - - @Override - public T set(int index, T element) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @SuppressWarnings("unchecked") - @Override - public ASequence assoc(ACell key, ACell value) { - CVMLong ix=RT.ensureLong(key); - if (ix==null) return null; - return assoc(ix.longValue(),(T)value); - } - - /** - * Updates a value at the given position in the sequence. - * - * @param i Index of element to update - * @param value New element value - * @return Updated sequence, or null if index is out of range - */ - public abstract ASequence assoc(long i, R value); - - /** - * Checks if an index range is valid for this sequence - * - * @param start - * @param length - */ - protected void checkRange(long start, long length) { - if (start < 0) throw Utils.sneakyThrow(new IndexOutOfBoundsException("Negative start: " + start)); - if (length < 0L) throw Utils.sneakyThrow(new IndexOutOfBoundsException("Negative length: " + length)); - if ((start + length) > count()) - throw Utils.sneakyThrow(new IndexOutOfBoundsException("End out of bounds: " + start + length)); - } - - @Override - public final void add(int index, T element) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public final T remove(int index) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - /** - * Converts this sequence to a new Cell array - * @return A new cell array containing the elements of this sequence - */ - public ACell[] toCellArray() { - int n=Utils.checkedInt(count()); - ACell[] cells=new ACell[n]; - for (int i=0; i ASequence conj(R value); - - /** - * Produces a slice of this sequence, beginning with the specified start index and of the given length. - * The start and length must be contained within this sequence. Will return the same sequence if the - * start is zero and the length matches this sequence. - * - * @param start Index of the start element - * @param length Length of slice to create. - * @return A sequence representing the requested slice. - */ - public abstract ASequence slice(long start, long length); - - /** - * Prepends an element to this sequence, returning a list. - * @param x Any new element value - * @return A list starting with the new element. - */ - public abstract AList cons(T x); - - /** - * Gets a vector containing the specified subset of this sequence. - * - * @param start Start index of sub vector - * @param length Length of sub vector to produce - * @return Sub-vector of this sequence - */ - public abstract AVector subVector(long start, long length); - - @Override - public final java.util.List subList(int fromIndex, int toIndex) { - long start = fromIndex; - long length = toIndex - fromIndex; - return subVector(start, length); - } - - /** - * Gets the ListIterator for a long position - * - * @param l - * @return ListIterator instance. - */ - protected abstract ListIterator listIterator(long l); - - /** - * Reverses a sequence, converting Lists to Vectors and vice versa - * @return Reversed sequence - */ - public abstract ASequence reverse(); -} diff --git a/convex-core/src/main/java/convex/core/data/ASet.java b/convex-core/src/main/java/convex/core/data/ASet.java deleted file mode 100644 index 27ca9c326..000000000 --- a/convex-core/src/main/java/convex/core/data/ASet.java +++ /dev/null @@ -1,216 +0,0 @@ -package convex.core.data; - -import java.util.function.Function; - -import convex.core.Constants; -import convex.core.data.prim.CVMBool; -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.util.Utils; - -/** - * Abstract based class for sets. - * - * Sets are immutable Smart Data Structures representing an unordered - * collection of distinct values. - * - * Iteration order is dependent on the Set implementation. In general, it - * is bad practice to depend on any specific ordering for sets. - * - * @param Type of set elements - */ -public abstract class ASet extends ACollection implements java.util.Set, IAssociative { - - protected ASet(long count) { - super(count); - } - - @Override - public final AType getType() { - return Types.SET; - } - - @Override - public final byte getTag() { - return Tag.SET; - } - - /** - * Updates the set to include the given element - * @param a Value to include - * @return Updated set - */ - public abstract ASet include(R a); - - /** - * Updates the set to exclude the given element - * @param a Value to exclude - * @return Updated set - */ - public abstract ASet exclude(T a) ; - - /** - * Updates the set to include all the given elements. - * Can be used to implement union of sets - * - * @param elements Elements to include - * @return Updated set - */ - public abstract ASet includeAll(ASet elements) ; - - /** - * Updates the set to exclude all the given elements. - * - * @param elements Elements to exclude - * @return Updated set - */ - public abstract ASet excludeAll(ASet elements) ; - - @Override - public abstract ASet conjAll(ACollection xs); - - /** - * Removes all elements from this set, returning a new set. - * @param xs Collection of elements to remove - * @return Set with specified element(s) removed - */ - public abstract ASet disjAll(ACollection xs); - - @Override - public AVector toVector() { - int n=Utils.checkedInt(count); - ACell[] elements=new ACell[n]; - copyToArray(elements,0); - return Vectors.create(elements); - } - - @Override - public ASet map(Function mapper) { - ASet result=Sets.empty(); - for (long i=0; i intersectAll(ASet xs); - - @Override - public CVMBool get(ACell key) { - return contains(key)?Constants.SET_INCLUDED:Constants.SET_EXCLUDED; - } - - @Override - public ACell get(ACell key, ACell notFound) { - if (contains(key)) return Constants.SET_INCLUDED; - return notFound; - } - - @Override - public T get(long index) { - return getElementRef(index).getValue(); - } - - /** - * Tests if this Set contains a given value - * @param o Value to test for set membership - * @return True if set contains value, false otherwise - */ - public abstract boolean contains(ACell o); - - @Override - public final boolean contains(Object o) { - if ((o==null)||(o instanceof ACell)) { - return contains((ACell)o); - } - return false; - } - - @SuppressWarnings("unchecked") - @Override - public final boolean equals(ACell o) { - if (o instanceof ASet) return equals((ASet)o); - return false; - } - - /** - * Checks if another set is exactly equal to this set - * - * @param other Set to compare with this set - * @return true if sets are equal, false otherwise - */ - public abstract boolean equals(ASet other); - - /** - * Adds a value to this set using a Ref to the value - * @param ref Ref to value to include - * @return Updated set - */ - public abstract ASet includeRef(Ref ref) ; - - @Override - public abstract ASet conj(R a); - - @SuppressWarnings("unchecked") - @Override - public ASet assoc(ACell key, ACell value) { - if (value==CVMBool.TRUE) return (ASet) include(key); - if (value==CVMBool.FALSE) return exclude((T) key); - return null; - } - - @Override - public boolean containsKey(ACell key) { - return contains(key); - } - - @Override - public ASet empty() { - return Sets.empty(); - } - - /** - * Gets the Ref in the Set for a given value, or null if not found - * @param k Value to check for set membership - * @return Ref to value, or null - */ - public abstract Ref getValueRef(ACell k); - - /** - * Gets the Ref in the Set for a given hash, or null if not found - * @param hash Hash to check for set membership - * @return Ref to value with given Hash, or null - */ - protected abstract Ref getRefByHash(Hash hash); - - /** - * Tests if this set contains all the elements of another set - * @param b Set to compare with - * @return True if other set is completely contained within this set, false otherwise - */ - public abstract boolean containsAll(ASet b); - - /** - * Tests if this set is a (non-strict) subset of another Set - * @param b Set to test against - * @return True if this is a subset of the other set, false otherwise. - */ - public boolean isSubset(ASet b) { - return b.containsAll(this); - } - - @Override - public void print(StringBuilder sb) { - sb.append("#{"); - for (long i=0; i0) sb.append(','); - Utils.print(sb,get(i)); - } - sb.append('}'); - } -} diff --git a/convex-core/src/main/java/convex/core/data/AString.java b/convex-core/src/main/java/convex/core/data/AString.java deleted file mode 100644 index d485ef849..000000000 --- a/convex-core/src/main/java/convex/core/data/AString.java +++ /dev/null @@ -1,84 +0,0 @@ -package convex.core.data; - -import convex.core.data.prim.CVMChar; -import convex.core.data.type.AType; -import convex.core.data.type.Types; - -/** - * Class representing a CVM String - */ -public abstract class AString extends ACountable implements CharSequence, Comparable { - - protected int length; - - protected AString(int length) { - this.length=length; - } - - @Override - public AType getType() { - return Types.STRING; - } - - @Override - public void print(StringBuilder sb) { - sb.append('"'); - // TODO. Fix escaping. - sb.append(this); - sb.append('"'); - } - - @Override - public int length() { - return length; - } - - @Override - public long count() { - return length; - } - - public StringShort empty() { - return Strings.EMPTY; - } - - protected abstract AString append(char charValue); - - @Override - public CVMChar get(long i) { - return CVMChar.create(charAt((int)i)); - } - - @Override - public Ref getElementRef(long i) { - return get(i).getRef(); - } - - @Override - public int compareTo(AString o) { - return CharSequence.compare(this,o); - } - - @Override - public String toString() { - StringBuilder sb=new StringBuilder(length); - appendToStringBuffer(sb,0,length()); - return sb.toString(); - } - - protected abstract void appendToStringBuffer(StringBuilder sb, int start, int length); - - @Override - public abstract AString subSequence(int start, int end); - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.STRING; - return encodeRaw(bs,pos); - } - - @Override - public final byte getTag() { - return Tag.STRING; - } -} diff --git a/convex-core/src/main/java/convex/core/data/ASymbolic.java b/convex-core/src/main/java/convex/core/data/ASymbolic.java deleted file mode 100644 index 49cd549af..000000000 --- a/convex-core/src/main/java/convex/core/data/ASymbolic.java +++ /dev/null @@ -1,70 +0,0 @@ -package convex.core.data; - -import convex.core.Constants; -import convex.core.exceptions.InvalidDataException; - -/** - * Abstract based class for symbolic objects (Keywords, Symbols) - */ -public abstract class ASymbolic extends ACell { - - protected final String name; - - protected ASymbolic(String name) { - this.name = name; - } - - @Override - @SuppressWarnings("unchecked") - protected Ref createRef() { - // Create Ref at maximum status to reflect internal embedded status - Ref newRef= RefDirect.create(this,cachedHash(),Ref.INTERNAL_FLAGS); - cachedRef=newRef; - return (Ref) newRef; - } - - @Override public final boolean isCVMValue() { - return true; - } - - @Override - protected long calcMemorySize() { - // always embedded and no child Refs, so memory size == 0 - return 0; - } - - public String getName() { - return name; - } - - protected static boolean validateName(String name2) { - if (name2 == null) return false; - int n = name2.length(); - if ((n < 1) || (n > (Constants.MAX_NAME_LENGTH))) { - return false; - } - - // We have a valid name - return true; - } - - @Override - public boolean isEmbedded() { - // Symbolic values are always embedded - return true; - } - - @Override - public final int hashCode() { - return name.hashCode(); - } - - /** - * Validates the name of this Symbolic value - */ - @Override - public void validateCell() throws InvalidDataException { - if (!validateName(name)) throw new InvalidDataException("Invalid name: " + name, this); - } - -} diff --git a/convex-core/src/main/java/convex/core/data/AVector.java b/convex-core/src/main/java/convex/core/data/AVector.java deleted file mode 100644 index 5d28d880e..000000000 --- a/convex-core/src/main/java/convex/core/data/AVector.java +++ /dev/null @@ -1,268 +0,0 @@ -package convex.core.data; - -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; -import java.util.Spliterator; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Predicate; - -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.util.MergeFunction; -import convex.core.util.Utils; - -/** - * Abstract base class for vectors. - * - * Vectors are immutable sequences of values, with efficient appends to the tail - * of the list. - * - * This is a hierarchy with multiple implementations for different vector types, - * but all should conform to the general AVector interface. We use an abstract - * base class in preference to an interface because we control the hierarchy and - * it offers some mild performance advantages. - * - * General design goals: - Immutability - Cell structure breakdown for larger - * vectors, while keeping a shallow tree - Optimised performance for end of - * vector (conj, pop, last etc.) - Fast prefix comparisons to support consensus - * algorithm - * - * "If I had any recommendation to you at all, it's just if you're thinking - * about designing a system and you're not sure, whether you can answer all that - * questions in the forward direction, choose immutability. You can almost back - * into a little more than 50% of this design just by haven taken immutability - * as a constraint, saying 'oh my god now what am I gonna do? I cannot change - * this. I better do this!' And keep forcing you into good answers. So if I had - * any architectural guidance from this: Just do it. Choose immutability and see - * where it takes you." - Rich Hickey - * - * @param Type of element in Vector - */ -public abstract class AVector extends ASequence { - - - public AVector(long count) { - super(count); - } - - @Override - public AType getType() { - return Types.VECTOR; - } - - /** - * Gets the element at the specified index in this vector - * - * @param i The index of the element to get - * @return The element value at the specified index - */ - @Override - public abstract T get(long i); - - /** - * Appends a ListVector chunk to this vector. This vector must contain a whole - * number of chunks - * - * @param listVector A chunk to append. Must be a ListVector of maximum size - * @return The updated vector, of the same type as this vector @ - */ - public abstract AVector appendChunk(VectorLeaf listVector); - - /** - * Gets the VectorLeaf chunk at a given offset - * - * @param offset Offset into this vector. Must be a valid chunk start position - * @return The chunk referenced - */ - public abstract VectorLeaf getChunk(long offset); - - /** - * Appends a single element to this vector - * - * @param value Value to append - * @return Updated vector - */ - public abstract AVector append(T value); - - /** - * Returns true if this Vector is a single fully packed tree. i.e. a full - * ListVector or TreeVector. - * - * @return true is fully packed, flase otherwise - */ - public abstract boolean isPacked(); - - @Override - public void print(StringBuilder sb) { - sb.append('['); - int size = size(); - for (int i = 0; i < size; i++) { - if (i > 0) sb.append(' '); - Utils.print(sb,get(i)); - } - sb.append(']'); - } - - - - @Override - public T get(int index) { - return get((long) index); - } - - public abstract boolean anyMatch(Predicate pred); - - public abstract boolean allMatch(Predicate pred); - - @Override - public abstract AVector map(Function mapper); - - @Override - @SuppressWarnings("unchecked") - public AVector flatMap(Function> mapper) { - ASequence> vals = this.map(mapper); - AVector result = (AVector) this.empty(); - for (ASequence seq : vals) { - result = result.concat(seq); - } - return result; - } - - @Override - public abstract AVector concat(ASequence b); - - public abstract R reduce(BiFunction func, R value); - - @Override - public Spliterator spliterator() { - return spliterator(0); - } - - public abstract Spliterator spliterator(long position); - - @Override - public Iterator iterator() { - return listIterator(); - } - - @Override - public Object[] toArray() { - int s = size(); - Object[] result = new Object[s]; - copyToArray(result, 0); - return result; - } - - @Override - public final int indexOf(Object o) { - return Utils.checkedInt(longIndexOf(o)); - } - - @Override - public final int lastIndexOf(Object o) { - return Utils.checkedInt(longLastIndexOf(o)); - } - - @Override - public final ListIterator listIterator(int index) { - return listIterator((long) index); - } - - @Override - public abstract ListIterator listIterator(long index); - - /** - * Returns true if this vector is in canonical format, i.e. suitable as - * top-level serialised representation of a vector. - * - * @return true if the vector is in canonical format, false otherwise - */ - @Override - public abstract boolean isCanonical(); - - @Override - public abstract AVector updateRefs(IRefFunction func); - - /** - * Computes the length of the longest common prefix of this vector and another - * vector. - * - * @param b Any vector - * @return Length of the longest common prefix - */ - public abstract long commonPrefixLength(AVector b); - - public AVector appendAll(List list) { - // TODO Optimise with chunks - AVector result = this; - for (T value : list) { - result = result.append(value); - } - return result; - } - - @SuppressWarnings("unchecked") - @Override - public final AVector conj(R value) { - return (AVector) append((T) value); - } - - public AVector conjAll(ACollection xs) { - if (xs instanceof ASequence) { - return concat((ASequence)xs); - } - return concat(Vectors.create(xs)); - } - - @Override - public AList cons(T x) { - return Lists.create(this).cons(x); - } - - @Override - public abstract AVector next(); - - @Override - public final AVector slice(long start, long length) { - return subVector(start, length); - } - - @Override - public abstract AVector assoc(long i, R value); - - @Override - public AVector empty() { - return Vectors.empty(); - } - - @Override - public AList reverse() { - return convex.core.data.List.reverse(this); - } - - /** - * Merges this vector with another vector, using the provided merge function. - * - * Returns the same vector if the result is equal to this vector, or the other - * vector if the result is exactly equal to the other vector. - * - * The merge function is passed null for elements where one vector is shorter - * than the other. - * - * @param b Another vector - * @param func A merge function to apply to all elements of this and the other - * vector - * @return A new vector, equal in length to the largest of the two vectors - * passed @ - */ - public AVector mergeWith(AVector b, MergeFunction func) { - throw new UnsupportedOperationException(); - } - - @Override - public byte getTag() { - return Tag.VECTOR; - } -} diff --git a/convex-core/src/main/java/convex/core/data/AccountKey.java b/convex-core/src/main/java/convex/core/data/AccountKey.java deleted file mode 100644 index f869fb7a2..000000000 --- a/convex-core/src/main/java/convex/core/data/AccountKey.java +++ /dev/null @@ -1,281 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; - -import convex.core.Constants; -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.InvalidDataException; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * Immutable class representing an Ed25519 Public Key for an Account - * - *

- * Using Ed25519: - *

- *
    - *
  • AccountKey is the Public Key (32 bytes)
  • - *
- * - */ -public class AccountKey extends AArrayBlob { - public static final int LENGTH = Constants.KEY_LENGTH; - - public static final AType TYPE = Types.BLOB; - - - public static final int LENGTH_BITS = LENGTH * 8; - - public static final AccountKey ZERO = AccountKey.dummy("0"); - - private AccountKey(byte[] data, int offset, int length) { - super(data, offset, length); - if (length != LENGTH) throw new IllegalArgumentException("AccountKey length must be " + LENGTH + " bytes"); - } - - @Override - public AType getType() { - return TYPE; - } - - @Override - @SuppressWarnings("unchecked") - protected Ref createRef() { - // Create Ref at maximum status to reflect internal embedded status - Ref newRef= RefDirect.create(this,cachedHash(),Ref.INTERNAL_FLAGS); - cachedRef=newRef; - return (Ref) newRef; - } - - /** - * Wraps the specified bytes as an AccountKey object. Warning: underlying bytes are - * used directly. Use only if no external references to the byte array will be - * retained. - * - * @param data Byte array to wrap as Account Key - * @return An Address wrapping the given bytes - */ - public static AccountKey wrap(byte[] data) { - return new AccountKey(data, 0, data.length); - } - - /** - * Wraps the specified bytes as an AccountKey object. Warning: underlying bytes are - * used directly. Use only if no external references to the byte array will be - * retained. - * - * @param data Data array containing address bytes. - * @param offset Offset into byte array - * @return An Address wrapping the given bytes - */ - public static AccountKey wrap(byte[] data, int offset) { - return new AccountKey(data, offset, LENGTH); - } - - /** - * Creates an AccountKey from a blob. Must have correct length. - * @param b Blob to wrap as Account Key - * @return AccountKey instance, or null if not valid - */ - public static AccountKey create(ABlob b) { - if (b.count()!=LENGTH) return null; - if (b instanceof AccountKey) return (AccountKey) b; - if (b instanceof AArrayBlob) { - AArrayBlob ab=(AArrayBlob)b; - return new AccountKey(ab.getInternalArray(),ab.getInternalOffset(),LENGTH); - } - return wrap(b.getBytes()); - } - - /** - * Creates a "Dummy" Address that is not a valid public key, and therefore - * cannot have valid signed transactions. - * - * To do this, a short hex nonce is repeated to fill the entire address length. This - * construction makes it possible to examine an Address and assess whether it is (plausibly) - * a dummy address. - * - * @param nonce Hex string to repeat to produce a visible dummy address - * @return An Address that cannot be used to sign transactions. - */ - public static AccountKey dummy(String nonce) { - int n = nonce.length(); - if (n == 0) throw new Error("Empty nonce"); - if (n >= LENGTH / 2) throw new Error("Nonce too long for dummy address"); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < LENGTH * 2; i += n) { - sb.append(nonce); - } - return AccountKey.fromHex(sb.substring(0, LENGTH * 2)); - } - - @Override - public boolean equals(ABlob o) { - if (o==null) return false; - if (o instanceof AccountKey) return equals((AccountKey)o); - if (o.getType()!=TYPE) return false; - if (o.count()!=LENGTH) return false; - return o.equalsBytes(this.store, this.offset); - } - - public boolean equals(AccountKey o) { - if (o == this) return true; - return Utils.arrayEquals(o.store, o.offset, this.store, this.offset, LENGTH); - } - - /** - * Constructs an AccountKey object from a hex string. - * Throws an exception if string is not valid - * - * @param hexString Hex String - * @return An AccountKey constructed from the hex string - */ - public static AccountKey fromHex(String hexString) { - AccountKey result = fromHexOrNull(hexString); - if (result == null) throw new IllegalArgumentException("Invalid Address hex String [" + hexString + "]"); - return result; - } - - /** - * Constructs an AccountKey object from a hex string - * - * @param hexString Hex String - * @return An Address constructed from the hex string, or null if not a valid - * hex string - */ - public static AccountKey fromHexOrNull(String hexString) { - byte[] bs = Utils.hexToBytes(hexString, LENGTH * 2); - if (bs == null) return null; // invalid string - if (bs.length != LENGTH) return null; // wrong length - return wrap(bs); - } - - public static AccountKey fromHexOrNull(AString a) { - if (a.length()!=LENGTH*2) return null; - return fromHexOrNull(a.toString()); - } - - - /** - * Constructs an AccountKey object from a checksummed hex string. - * - * Throws an exception if checksum is not valid - * - * @param hexString Hex String - * @return An Address constructed from the hex string - */ - public static AccountKey fromChecksumHex(String hexString) { - byte[] bs = Utils.hexToBytes(hexString, LENGTH * 2); - AccountKey a = AccountKey.wrap(bs); - Hash h = a.getContentHash(); - for (int i = 0; i < LENGTH * 2; i++) { - int dh = h.getHexDigit(i); - char c = hexString.charAt(i); - if (Character.isDigit(c)) continue; - boolean check = (c >= 'a') ^ (dh >= 8); // note 'a' is higher than 'A' - if (!check) - throw new IllegalArgumentException("Bad checksum at position " + i + " in address " + hexString); - } - return a; - } - - /** - * Converts this AccountKey to a checksummed hex string. - * - * @return A String containing the checksummed hex representation of this - * Address - */ - public String toChecksumHex() { - StringBuilder sb = new StringBuilder(64); - Hash h = this.getContentHash(); - for (int i = 0; i < LENGTH * 2; i++) { - int dh = h.getHexDigit(i); - int da = this.getHexDigit(i); - if (da < 10) { - sb.append((char) ('0' + da)); - } else { - boolean up = (dh >= 8); - sb.append((char) ((up ? 'A' : 'a') + da - 10)); - } - } - return sb.toString(); - } - - public static AccountKey readRaw(ByteBuffer data) { - byte[] buff = new byte[LENGTH]; - data.get(buff); - return AccountKey.wrap(buff); - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.BLOB; - bs[pos++]=Constants.KEY_LENGTH; - return encodeRaw(bs,pos); - } - - @Override public final boolean isCVMValue() { - return true; - } - - @Override - public int estimatedEncodingSize() { - // tag plus LENGTH bytes - return 3 + LENGTH; - } - - @Override - public long getEncodingLength() { - // Always a fixed encoding length, tag plus count plus length - return 2 + LENGTH; - } - - @Override - public Blob getChunk(long i) { - if (i != 0) throw new IndexOutOfBoundsException(Errors.badIndex(i)); - return toBlob(); - } - - @Override - public void validateCell() throws InvalidDataException { - if (length != LENGTH) - throw new InvalidDataException("Address length must be " + LENGTH + " bytes = " + LENGTH_BITS + " bits", - this); - } - - @Override - public boolean isEmbedded() { - return true; - } - - @Override - protected long calcMemorySize() { - // always embedded and no child Refs, so memory size == 0 - return 0; - } - - @Override - public boolean isRegularBlob() { - return true; - } - - @Override - public byte getTag() { - return Tag.BLOB; - } - - @Override - public boolean isCanonical() { - return false; - } - - @Override - public Blob toCanonical() { - return toBlob(); - } - - - -} diff --git a/convex-core/src/main/java/convex/core/data/AccountStatus.java b/convex-core/src/main/java/convex/core/data/AccountStatus.java deleted file mode 100644 index 1dc23b07d..000000000 --- a/convex-core/src/main/java/convex/core/data/AccountStatus.java +++ /dev/null @@ -1,500 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; - -import convex.core.Constants; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.AFn; -import convex.core.lang.RT; -import convex.core.lang.impl.RecordFormat; -import convex.core.util.Utils; - -/** - * Class representing the current on-chain status of an account. - * - * Accounts may be User accounts or Actor accounts. - * - * "People said I should accept the world. Bullshit! I don't accept the world." - * - Richard Stallman - */ -public class AccountStatus extends ARecord { - private final long sequence; - private final long balance; - private final long memory; - private final AHashMap environment; - private final AHashMap> metadata; - private final ABlobMap holdings; - private final Address controller; - private final AccountKey publicKey; - - private static final Keyword[] ACCOUNT_KEYS = new Keyword[] { Keywords.SEQUENCE, Keywords.BALANCE,Keywords.ALLOWANCE,Keywords.ENVIRONMENT,Keywords.METADATA, - Keywords.HOLDINGS, Keywords.CONTROLLER, Keywords.KEY}; - - private static final RecordFormat FORMAT = RecordFormat.of(ACCOUNT_KEYS); - - private static final int HAS_SEQUENCE=1< environment, - AHashMap> metadata, - ABlobMap holdings, - Address controller, - AccountKey publicKey) { - super(FORMAT); - this.sequence = sequence; - this.balance = balance; - this.memory = memory; - this.environment = environment; - this.metadata=metadata; - this.holdings=holdings; - this.controller=controller; - this.publicKey=publicKey; - } - - /** - * Create a regular account, with the specified balance and zero allowance - * - * @param sequence Sequence number - * @param balance Convex Coin balance of Account - * @param key Public Key of new Account - * @return New AccountStatus - */ - public static AccountStatus create(long sequence, long balance, AccountKey key) { - return new AccountStatus(sequence, balance, 0L, null,null,null,null,key); - } - - /** - * Create a governance account. - * - * @param balance Balance for governance account - * @return New governance AccountStatus - */ - public static AccountStatus createGovernance(long balance) { - return new AccountStatus(Constants.INITIAL_SEQUENCE, balance, 0L, null,null,null,null,null); - } - - public static AccountStatus createActor() { - return new AccountStatus(Constants.INITIAL_SEQUENCE, 0L, 0L,null,null,null,null,null); - } - - public static AccountStatus create(long balance, AccountKey key) { - return create(0, balance,key); - } - - /** - * Create a completely empty Account record, with no balance or public key - * @return Empty Account record - */ - public static AccountStatus create() { - return create(0, 0L,null); - } - - /** - * Gets the sequence number for this Account. The sequence number is the number - * of transactions executed by this account to date. It will be zero for new - * Accounts. - * - * The next transaction executed must have a nonce equal to this value plus one. - * - * @return The sequence number for this Account. - */ - public long getSequence() { - return sequence; - } - - public long getBalance() { - return balance; - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.ACCOUNT_STATUS; - return encodeRaw(bs,pos); - } - - private int getInclusion() { - int included=0; - if (sequence!=0L) included|=HAS_SEQUENCE; - if (balance!=0L) included|=HAS_BALANCE; - if (memory!=0L) included|=HAS_ALLOWANCE; - if (environment!=null) included|=HAS_ENVIRONMENT; - if (metadata!=null) included|=HAS_METADATA; - if (holdings!=null) included|=HAS_HOLDINGS; - if (controller!=null) included|=HAS_CONTROLLER; - if (publicKey!=null) included|=HAS_KEY; - return included; - - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - int included=getInclusion(); - bs[pos++]=(byte)included; - if ((included&HAS_SEQUENCE)!=0) pos = Format.writeVLCLong(bs, pos,sequence); - if ((included&HAS_BALANCE)!=0) pos = Format.writeVLCLong(bs,pos, balance); - if ((included&HAS_ALLOWANCE)!=0) pos = Format.writeVLCLong(bs,pos, memory); - if ((included&HAS_ENVIRONMENT)!=0) pos = Format.write(bs,pos, environment); - if ((included&HAS_METADATA)!=0) pos = Format.write(bs,pos, metadata); - if ((included&HAS_HOLDINGS)!=0) pos = Format.write(bs,pos, holdings); - if ((included&HAS_CONTROLLER)!=0) pos = Format.write(bs,pos, controller); - if ((included&HAS_KEY)!=0) pos = publicKey.writeToBuffer(bs, pos); - return pos; - } - - public static AccountStatus read(ByteBuffer bb) throws BadFormatException { - int included=bb.get(); - long sequence = ((included&HAS_SEQUENCE)!=0) ? Format.readVLCLong(bb) : 0L; - long balance = ((included&HAS_BALANCE)!=0) ? Format.readVLCLong(bb) : 0L; - long allowance = ((included&HAS_ALLOWANCE)!=0) ? Format.readVLCLong(bb) : 0L; - AHashMap environment = ((included&HAS_ENVIRONMENT)!=0) ? Format.read(bb):null; - AHashMap> metadata = ((included&HAS_METADATA)!=0) ? Format.read(bb) : null; - ABlobMap holdings = ((included&HAS_HOLDINGS)!=0) ? Format.read(bb) : null; - Address controller = ((included&HAS_CONTROLLER)!=0) ? Format.read(bb) : null; - AccountKey publicKey = ((included&HAS_KEY)!=0) ? AccountKey.readRaw(bb) : null; - return new AccountStatus(sequence, balance, allowance, environment,metadata,holdings,controller,publicKey); - } - - @Override - public int estimatedEncodingSize() { - return 30+Format.estimateSize(environment)+Format.estimateSize(holdings)+Format.estimateSize(controller)+33; - } - - @Override - public boolean isCanonical() { - return true; - } - - public boolean isActor() { - return publicKey==null; - } - - - - /** - * Get the controller for this Account - * @return Controller Address, or null if there is no controller - */ - public Address getController() { - return controller; - } - - /** - * Checks if this account has enough balance for a transaction consuming the - * specified amount. - * - * @param amt minimum amount that must be present in the specified balance - * @return true if Account has at least the balance specified, false otherwise - */ - public boolean hasBalance(long amt) { - if (amt < 0) return false; - if (amt > balance) return false; - return true; - } - - public AccountStatus withBalance(long newBalance) { - if (balance==newBalance) return this; - return new AccountStatus(sequence, newBalance, memory, environment,metadata,holdings,controller,publicKey); - } - - - public AccountStatus withAccountKey(AccountKey newKey) { - if (newKey==publicKey) return this; - return new AccountStatus(sequence, balance, memory, environment,metadata,holdings,controller,newKey); - } - - public AccountStatus withMemory(long newMemory) { - if (memory==newMemory) return this; - return new AccountStatus(sequence, balance, newMemory, environment,metadata,holdings,controller,publicKey); - } - - public AccountStatus withBalances(long newBalance, long newAllowance) { - if ((balance==newBalance)&&(memory==newAllowance)) return this; - return new AccountStatus(sequence, newBalance, newAllowance, environment,metadata,holdings,controller,publicKey); - } - - public AccountStatus withEnvironment(AHashMap newEnvironment) { - if ((newEnvironment!=null)&&newEnvironment.isEmpty()) newEnvironment=null; - if (environment==newEnvironment) return this; - return new AccountStatus(sequence, balance, memory,newEnvironment,metadata,holdings,controller,publicKey); - } - - public AccountStatus withMetadata(AHashMap> newMeta) { - if ((newMeta!=null)&&newMeta.isEmpty()) newMeta=null; - if (metadata==newMeta) return this; - return new AccountStatus(sequence, balance, memory,environment,newMeta,holdings,controller,publicKey); - } - - @Override - public boolean equals(AMap a) { - if (this == a) return true; // important optimisation for e.g. hashmap equality - if (a == null) return false; - if (a.getTag()!=getTag()) return false; - AccountStatus as=(AccountStatus)a; - return equals(as); - } - - /** - * Tests if this account is equal to another Account - * @param a AccountStatus to compare with - * @return true if equal, false otherwise - */ - public boolean equals(AccountStatus a) { - if (a == null) return false; - Hash h=this.cachedHash(); - if (h!=null) { - Hash ha=a.cachedHash(); - if (ha!=null) return Utils.equals(h, ha); - } - - if (balance!=a.balance) return false; - if (sequence!=a.sequence) return false; - if (memory!=a.memory) return false; - if (!(Utils.equals(controller, a.controller))) return false; - if (!(Utils.equals(publicKey, a.publicKey))) return false; - if (!(Utils.equals(holdings, a.holdings))) return false; - if (!(Utils.equals(metadata, a.metadata))) return false; - if (!(Utils.equals(environment, a.environment))) return false; - return true; - } - - /** - * Updates this account with a new sequence number. - * - * @param newSequence New sequence number - * @return Updated account, or null if the sequence number was wrong - */ - public AccountStatus updateSequence(long newSequence) { - // SECURITY: shouldn't ever be trying to call updateSequence on a Actor address! - if (isActor()) throw new Error("Trying to update Actor sequence number!"); - - long expected = sequence + 1; - if (expected != newSequence) { - return null; - } - - return new AccountStatus(newSequence, balance, memory, environment,metadata,holdings,controller,publicKey); - } - - @Override - public void validateCell() throws InvalidDataException { - if (environment != null) { - if (environment.isEmpty()) throw new InvalidDataException("Account should not have empty map as environment",this); - environment.validateCell(); - } - if (holdings != null) { - if (environment.isEmpty()) throw new InvalidDataException("Account should not have empty map as metadata",this); - holdings.validateCell(); - } - } - - /** - * Gets the value in the Account's environment for the given symbol. - * - * @param Result type - * @param symbol Symbol to get in Environment - * @return The value from the environment, or null if not found - */ - @SuppressWarnings("unchecked") - public R getEnvironmentValue(Symbol symbol) { - if (environment == null) return null; - ACell value = environment.get(symbol); - return (R) value; - } - - /** - * Gets the holdings for this account. Will always be a non-null map. - * @return Holdings map for this account - */ - public ABlobMap getHoldings() { - ABlobMap result=holdings; - if (result==null) return BlobMaps.empty(); - return result; - } - - public ACell getHolding(Address addr) { - if (holdings==null) return null; - return holdings.get(addr); - } - - public AccountStatus withHolding(Address addr,ACell value) { - ABlobMap newHoldings=getHoldings(); - if (value==null) { - newHoldings=newHoldings.dissoc(addr); - } else if (newHoldings==null) { - newHoldings=BlobMaps.of(addr,value); - } else { - newHoldings=newHoldings.assoc(addr, value); - } - return withHoldings(newHoldings); - } - - private AccountStatus withHoldings(ABlobMap newHoldings) { - if (newHoldings.isEmpty()) newHoldings=null; - if (holdings==newHoldings) return this; - return new AccountStatus(sequence, balance, memory, environment,metadata,newHoldings,controller,publicKey); - } - - public AccountStatus withController(Address newController) { - if (controller==newController) return this; - return new AccountStatus(sequence, balance, memory, environment,metadata,holdings,newController,publicKey); - } - - @Override - public int getRefCount() { - int rc=(environment==null)?0:environment.getRefCount(); - rc+=(metadata==null)?0:metadata.getRefCount(); - rc+=(holdings==null)?0:holdings.getRefCount(); - return rc; - } - - public Ref getRef(int i) { - if (i<0) throw new IndexOutOfBoundsException(i); - - int ec=(environment==null)?0:environment.getRefCount(); - if (i newEnv=(AHashMap) newVals[3]; - AHashMap> newMeta=(AHashMap>) newVals[4]; - ABlobMap newHoldings=(ABlobMap) newVals[5]; - if ((newHoldings!=null)&&newHoldings.isEmpty()) newHoldings=null; // switch empty maps to null - Address newController = (Address)newVals[6]; - AccountKey newKey=(AccountKey)newVals[7]; - - if ((balance==newBal)&&(sequence==newSeq)&&(newEnv==environment)&&(newMeta==metadata)&&(newHoldings==holdings)&&(newController==controller)&&(newKey==publicKey)) { - return this; - } - - return new AccountStatus(newSeq,newBal,newAllowance,newEnv,newMeta,newHoldings,newController,newKey); - } - - /** - * Gets the memory allowance for this account - * @return Memory allowance in bytes - */ - public long getMemory() { - return memory; - } - - /** - * Gets the memory usage for this Account. Memory usage is defined as the size of the AccountStatus Cell - * @return Memory usage of this Account in bytes. - */ - public long getMemoryUsage() { - return this.getMemorySize(); - } - - /** - * Adds a change in balance to this account. Must not cause an illegal balance. Returns this instance unchanged - * if the delta is zero - * @param delta Amount of Convex copper to add - * @return Updates account record - */ - public AccountStatus addBalance(long delta) { - if (delta==0) return this; - return withBalance(balance+delta); - } - - /** - * Gets the public key for this Account. May bu null (e.g. for Actors) - * @return Account public key - */ - public AccountKey getAccountKey() { - return publicKey; - } - - public AHashMap> getMetadata() { - if (metadata==null) return Maps.empty(); - return metadata; - } - - /** - * Gets the Environment for this account. Defaults to the an empty map if no Environment has been created. - * @return Environment map for this Account - */ - public AHashMap getEnvironment() { - if (environment==null) return Maps.empty(); - return environment; - } - - /** - * Gets the callable functions from this Account. - * @return Set of callable Symbols - */ - public ASet getCallableFunctions() { - ASet results=Sets.empty(); - if (metadata==null) return results; - for (Entry> me:metadata.entrySet()) { - ACell callVal=me.getValue().get(Keywords.CALLABLE_Q); - if (RT.bool(callVal)) { - Symbol sym=me.getKey(); - if (RT.ensureFunction(getEnvironmentValue(sym))==null) continue; - results=results.conj(sym); - } - } - return results; - } - - /** - * Gets a callable function from the environment, or null if not callable - * @param sym Symbol to look up - * @return Callable function if found, null otherwise - */ - public AFn getCallableFunction(Symbol sym) { - ACell exported=getEnvironmentValue(sym); - if (exported==null) return null; - AFn fn=RT.ensureFunction(exported); - if (fn==null) return null; - AHashMap md=getMetadata().get(sym); - if (RT.bool(md.get(Keywords.CALLABLE_Q))) { - // We have both a function and required metadata tag - return fn; - } - return null; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/Address.java b/convex-core/src/main/java/convex/core/data/Address.java deleted file mode 100644 index 951c048e8..000000000 --- a/convex-core/src/main/java/convex/core/data/Address.java +++ /dev/null @@ -1,227 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.security.MessageDigest; - -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.util.Utils; - -/** - * Immutable class representing an Address. - * - * An Address is a specialised 8-byte long blob instance that wraps a non-negative long account number. This number - * serves as an index into the vector of accounts for the current state. - * - */ -public final class Address extends ALongBlob { - - - public static final Address ZERO = Address.create(0); - - private Address(long value) { - super(value); - } - - /** - * Creates an Address from a blob. Number be a valid non-negative long value. - * - * @param number Account number - * @return Address instance, or null if not valid - */ - public static Address create(long number) { - if (number<0) return null; - return new Address(number); - } - - /** - * Creates an Address from a blob. Must be a valid long value - * @param b Blob to convert to an Address - * @return Address instance, or null if not valid - */ - public static Address create(ABlob b) { - if (b.count()!=8) return null; - return create(b.longValue()); - } - - @Override - public AType getType() { - return Types.ADDRESS; - } - - - @Override - public int hashCode() { - // note: We use the Java hashcode of a long - return Long.hashCode(value); - } - - @Override - public boolean equals(ABlob o) { - if (!(o instanceof Address)) return false; - return value==((Address) o).value; - } - - public boolean equals(Address o) { - return value==o.value; - } - - /** - * Constructs an Address object from a hex string - * - * @param hexString String to read Address from - * @return An Address constructed from the hex string, or null if not a valid - * hex string - */ - public static Address fromHex(String hexString) { - // catch nulls just in case - if (hexString==null) return null; - - // catch odd length - if ((hexString.length()&1)!=0) return null; - - if (hexString.length()>16) return null; - Blob b=Blob.fromHex(hexString); - if (b==null) return null; - if (b.length!=8) return null; - return create(b.longValue()); - } - - /** - * Constructs an Address from an arbitrary String, attempting to parse different possible formats - * @param s String to parse - * @return Address parsed, or null if not valid - */ - public static Address parse(String s) { - s=s.trim(); - if (s.startsWith("#")) { - s=s.substring(1); - } - - if (s.startsWith("0x")) { - s=s.substring(2); - return fromHex(s); - } - - try { - long l=Long.parseLong(s); - return Address.create(l); - } catch (NumberFormatException e) { - // fall through - } - - return null; - } - - public static Address readRaw(ByteBuffer bb) throws BadFormatException { - long value=Format.readVLCLong(bb); - Address a= Address.create(value); - if (a==null) throw new BadFormatException("Invalid VLC encoding for Address"); - return a; - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.ADDRESS; - return encodeRaw(bs,pos); - } - - @Override - public void print(StringBuilder sb) { - sb.append("#"); - sb.append(value); - } - - @Override - public boolean isCanonical() { - // always canonical, since class invariants are maintained - return true; - } - - @Override - public int estimatedEncodingSize() { - // tag plus LENGTH bytes - return 1 + Format.MAX_VLC_LONG_LENGTH; - } - - @Override - public void validateCell() throws InvalidDataException { - if (value<0) - throw new InvalidDataException("Address must be positive",this); - - } - - @Override public final boolean isCVMValue() { - return true; - } - - - @Override - public boolean isRegularBlob() { - return false; - } - - @Override - public void getBytes(byte[] dest, int destOffset) { - Utils.writeLong(dest, destOffset, value); - } - - @Override - public Blob slice(long start, long length) { - return toBlob().slice(start,length); - } - - @Override - public Blob toBlob() { - byte[] bs=new byte[8]; - Utils.writeLong(bs, 0, value); - return Blob.wrap(bs); - } - - @Override - protected void updateDigest(MessageDigest digest) { - toBlob().updateDigest(digest); - } - - @Override - public boolean equalsBytes(byte[] bytes, int byteOffset) { - return value==Utils.readLong(bytes, byteOffset); - } - - @Override - public long longValue() { - return value; - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - return Format.writeVLCLong(bs, pos, value); - } - - public static final int MAX_ENCODING_LENGTH = 1+Format.MAX_VLC_LONG_LENGTH; - - @Override - public byte getTag() { - return Tag.ADDRESS; - } - - @Override - public Address toCanonical() { - return this; - } - - /** - * Creates a new Address at an offset to this Address - * @param offset Offset to add to this Address (may be negative) - * @return New Address - */ - public Address offset(long offset) { - return create(value+offset); - } - - - - -} diff --git a/convex-core/src/main/java/convex/core/data/Blob.java b/convex-core/src/main/java/convex/core/data/Blob.java deleted file mode 100644 index 9567f1647..000000000 --- a/convex-core/src/main/java/convex/core/data/Blob.java +++ /dev/null @@ -1,269 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Random; - -import convex.core.exceptions.BadFormatException; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * General purpose immutable wrapper for byte array data. - * - * Can be serialised directly if 4096 bytes or less, otherwise needs to be - * structures as a BlobTree. - * - * Encoding format is: - * - Tag.BLOB tag byte - * - VLC encoded Blob length in bytes (one or two bytes describing a length in range 0..4096) - * - Byte data of the given length - */ -public class Blob extends AArrayBlob { - public static final Blob EMPTY = wrap(Utils.EMPTY_BYTES); - public static final Blob NULL_ENCODING = Blob.wrap(new byte[] {Tag.NULL}); - - public static final int CHUNK_LENGTH = 4096; - - private Blob(byte[] bytes, int offset, int length) { - super(bytes, offset, length); - } - - /** - * Creates a new data object using a copy of the specified byte range - * - * @param data Byte array - * @param offset Start offset in the byte array - * @param length Number of bytes to take from data array - * @return The new Data object - */ - public static Blob create(byte[] data, int offset, int length) { - if (length <= 0) { - if (length == 0) return EMPTY; - throw new IllegalArgumentException(Errors.negativeLength(length)); - } - byte[] store = Arrays.copyOfRange(data, offset, offset + length); - return wrap(store); - } - - /** - * Creates a new data object using a copy of the specified byte array. - * - * @param data Byte array - * @return Blob with the same byte contents as the given array - */ - public static Blob create(byte[] data) { - return create(data, 0, data.length); - } - - /** - * Wraps the specified bytes as a Data object Warning: underlying bytes are used - * directly. Use only if no other references to the byte array are kept which - * might be mutated. - * - * @param data Byte array - * @return Blob wrapping the given data - */ - public static Blob wrap(byte[] data) { - return new Blob(data, 0, data.length); - } - - /** - * Wraps the specified bytes as a Data object Warning: underlying bytes are used - * directly. Use only if no other references to the byte array are kept which - * might be mutated. - * - * @param data Byte array - * @param offset Offset into byte array - * @param length Length of byte array to wrap - * @return Blob wrapping the given byte array segment - */ - public static Blob wrap(byte[] data, int offset, int length) { - if (length < 0) throw new IllegalArgumentException(Errors.negativeLength(length)); - if ((offset < 0) || (offset + length > data.length)) - throw new IndexOutOfBoundsException(Errors.badRange(offset, length)); - return new Blob(data, offset, length); - } - - @Override - public Blob toBlob() { - return this; - } - - @Override - public Blob slice(long start, long length) { - if (start < 0) throw new IllegalArgumentException("Start out of bounds: " + start); - if ((start + length) > this.length) - throw new IllegalArgumentException("End out of bounds: " + (start + length)); - if (length < 0) throw new IllegalArgumentException("Negative length of slice: " + length); - if (length == 0) return EMPTY; - return Blob.wrap(store, Utils.checkedInt(start + offset), Utils.checkedInt(length)); - } - - @Override - public boolean equals(ABlob a) { - if (a==null) return false; - if (a instanceof Blob) return equals((Blob) a); - if (a.getType()!=getType()) return false; - if (a.count()!=count()) return false; - return a.equalsBytes(this.store, this.offset); - } - - public boolean equals(Blob b) { - if (length!=b.length) return false; - return Arrays.equals(store, offset, offset+length, b.store, b.offset, b.offset+length); - } - - /** - * Equality for array Blob objects - * - * Implemented by testing equality of byte data - * - * @param other Blob to comapre with - * @return true if blobs are equal, false otherwise. - */ - public boolean equals(AArrayBlob other) { - if (other == this) return true; - if (this.length != other.length) return false; - - // avoid false positives with other Blob types, especially Hash and Address - if (this.getType() != other.getType()) return false; - - if ((contentHash != null) && (other.contentHash != null) && contentHash.equals(other.contentHash)) return true; - return Utils.arrayEquals(other.store, other.offset, this.store, this.offset, this.length); - } - - /** - * Constructs a Blob object from a hex string - * - * @param hexString Hex String to read - * @return Blob with the provided hex value, or null if not a valid blob - */ - public static Blob fromHex(String hexString) { - byte[] bs=Utils.hexToBytes(hexString); - if (bs==null) return null; - return wrap(bs); - } - - /** - * Constructs a Blob object from all remaining bytes in a ByteBuffer - * - * @param bb ByteBuffer - * @return Blob containing the contents read from the ByteBuffer - */ - public static Blob fromByteBuffer(ByteBuffer bb) { - int count = bb.remaining(); - byte[] bs = new byte[count]; - bb.get(bs); - return Blob.wrap(bs); - } - - @Override - public ByteBuffer getByteBuffer() { - if (offset == 0) { - return ByteBuffer.wrap(store, offset, length).asReadOnlyBuffer(); - } else { - return ByteBuffer.wrap(this.getBytes()).asReadOnlyBuffer(); - } - } - - /** - * Fast read of a Blob from its representation insider another Blob object, - * - * Main benefit is to avoid reconstructing via ByteBuffer allocation, enabling - * retention of source Blob object as encoded data. - * - * @param source Source Blob object. - * @param len Length in bytes to take from the source Blob - * @return Blob read from the source - * @throws BadFormatException If encoding is invalid - */ - public static AArrayBlob read(Blob source, long len) throws BadFormatException { - // compute data length, excluding tag and encoded length - int headerLength = (1 + Format.getVLCLength(len)); - long rLen = source.count() - headerLength; - if (len != rLen) { - throw new BadFormatException("Invalid length for Blob, length field " + len + " but actual length " + rLen); - } - - return source.slice(headerLength, len); - } - - @Override - public int encode(byte[] bs, int pos) { - if (length > CHUNK_LENGTH) { - return BlobTree.create(this).encode(bs,pos); - } else { - // we have a Blob of canonical size - bs[pos++]=Tag.BLOB; - pos=Format.writeVLCLong(bs, pos, length); - pos=encodeRaw(bs,pos); - return pos; - } - } - - @Override - public int estimatedEncodingSize() { - // space for tag, generous VLC length, plus raw data - return 1 + Format.MAX_VLC_LONG_LENGTH + length; - } - - /** - * Maximum encoding size for a regular Blob - */ - public static int MAX_ENCODING_LENGTH=1+Format.getVLCLength(CHUNK_LENGTH)+CHUNK_LENGTH; - - @Override - public boolean isCanonical() { - return length <= Blob.CHUNK_LENGTH; - } - - @Override public final boolean isCVMValue() { - return true; - } - - /** - * Creates a Blob of random bytes of the given length - * - * @param random Any Random generator instance - * @param length Length of blob to generate in bytes - * @return Blob with the specified number of random bytes - */ - public static Blob createRandom(Random random, long length) { - byte[] randBytes = new byte[Utils.checkedInt(length)]; - random.nextBytes(randBytes); - return wrap(randBytes); - } - - @Override - public Blob getChunk(long i) { - if ((i == 0) && (length <= CHUNK_LENGTH)) return this; - long chunkStart = i * CHUNK_LENGTH; - return slice(chunkStart, Math.min(CHUNK_LENGTH, length - chunkStart)); - } - - public void attachContentHash(Hash hash) { - if (contentHash == null) contentHash = hash; - } - - @Override - public boolean isRegularBlob() { - return true; - } - - @Override - public byte getTag() { - return Tag.BLOB; - } - - @Override - public ABlob toCanonical() { - if (isCanonical()) return this; - return Blobs.toCanonical(this); - } - - - - - -} \ No newline at end of file diff --git a/convex-core/src/main/java/convex/core/data/BlobMap.java b/convex-core/src/main/java/convex/core/data/BlobMap.java deleted file mode 100644 index c4c020930..000000000 --- a/convex-core/src/main/java/convex/core/data/BlobMap.java +++ /dev/null @@ -1,744 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Predicate; - -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.RT; -import convex.core.util.Bits; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * BlobMap node implementation supporting: - * - *
    - *
  • An optional prefix string
  • - *
  • An optional entry with this prefix
  • - *
  • Up to 16 child entries at the next level of depth
  • - *
- * @param Type of Keys - * @param Type of values - */ -public class BlobMap extends ABlobMap { - @SuppressWarnings({ "unchecked", "rawtypes" }) - private static final Ref[] EMPTY_CHILDREN = new Ref[0]; - - /** - * Empty BlobMap singleton - */ - public static final BlobMap EMPTY = new BlobMap(0, 0, null, EMPTY_CHILDREN, - (short) 0, 0L); - - static { - // Set empty Ref flags as internal embedded constant - EMPTY.getRef().setFlags(Ref.INTERNAL_FLAGS); - } - - /** - * Child entries, i.e. nodes with keys where this node is a common prefix. Only contains children where mask is set. - * Child entries must have at least one entry. - */ - private final Ref>[] children; - - /** - * Entry for this node of the radix tree. Invariant assumption that the prefix - * is correct. May be null if there is no entry at this node. - */ - private final MapEntry entry; - - /** - * Mask of child entries, 16 bits for each hex digit that may be present. - */ - private final short mask; - - /** - * Depth of radix tree in number of hex digits. Top level is 0. - * Children should have depth = parent depth + parent prefixLength + 1 - */ - private final long depth; - - /** - * Length of prefix, where the tree branches beyond depth. 0 = no prefix. - */ - private final long prefixLength; - - @SuppressWarnings({ "rawtypes", "unchecked" }) - protected BlobMap(long depth, long prefixLength, MapEntry entry, Ref[] entries, - short mask, long count) { - super(count); - this.depth = depth; - this.prefixLength = prefixLength; - this.entry = entry; - int cn = Utils.bitCount(mask); - if (cn != entries.length) throw new IllegalArgumentException( - "Illegal mask: " + Utils.toHexString(mask) + " for given number of children: " + cn); - this.children = (Ref[]) entries; - this.mask = mask; - } - - public static BlobMap create(MapEntry me) { - ACell k=me.getKey(); - if (!(k instanceof ABlob)) return null; - long hexLength = ((ABlob)k).hexLength(); - return new BlobMap(0, hexLength, me, EMPTY_CHILDREN, (short) 0, 1L); - } - - private static BlobMap createAtDepth(MapEntry me, long depth) { - Blob prefix = me.getKey().toBlob(); - long hexLength = prefix.hexLength(); - if (depth > hexLength) - throw new IllegalArgumentException("Depth " + depth + " too deep for key with hexLength: " + hexLength); - return new BlobMap(depth, hexLength - depth, me, EMPTY_CHILDREN, (short) 0, 1L); - } - - public static BlobMap create(K k, V v) { - MapEntry me = MapEntry.create(k, v); - long hexLength = k.hexLength(); - return new BlobMap(0, hexLength, me, EMPTY_CHILDREN, (short) 0, 1L); - } - - public static BlobMap of(Object k, Object v) { - return create(RT.cvm(k),RT.cvm(v)); - } - - @Override - public boolean isCanonical() { - return true; - } - - @Override public final boolean isCVMValue() { - return (depth==0); - } - - @SuppressWarnings("unchecked") - @Override - public BlobMap updateRefs(IRefFunction func) { - MapEntry newEntry = (entry == null) ? null : entry.updateRefs(func); - Ref>[] newChildren = Ref.updateRefs(children, func); - if ((entry == newEntry) && (children == newChildren)) return this; - return new BlobMap(depth, prefixLength, newEntry, (Ref[])newChildren, mask, count); - } - - @Override - public boolean containsValue(Object value) { - if ((entry != null) && Utils.equals(entry.getValue(), value)) return true; - for (int i = 0; i < count; i++) { - if (children[i].getValue().containsValue(value)) return true; - } - return false; - } - - @Override - public V get(ABlob key) { - MapEntry me = getEntry(key); - if (me == null) return null; - return me.getValue(); - } - - @Override - public MapEntry getEntry(ABlob key) { - long kl = key.hexLength(); - long pl = depth + prefixLength; - if (kl < pl) return null; // key is too short to start with current prefix - - if (kl == pl) { - if ((entry!=null)&&(key.equalsBytes(entry.getKey()))) return entry; // we matched this key exactly! - return null; // entry does not exist - } - - int digit = key.getHexDigit(pl); - BlobMap cc = getChild(digit); - - if (cc == null) return null; - return cc.getEntry(key); - } - - /** - * Gets the child for a specific digit, or null if not found - * - * @param digit - * @return - */ - private BlobMap getChild(int digit) { - int i = Bits.indexForDigit(digit, mask); - if (i < 0) return null; - return (BlobMap) children[i].getValue(); - } - - @Override - public int getRefCount() { - return ((entry == null) ? 0 : entry.getRefCount()) + children.length; - } - - @SuppressWarnings("unchecked") - @Override - public Ref getRef(int i) { - if (entry != null) { - int erc = entry.getRefCount(); - if (i < erc) return entry.getRef(i); - i -= erc; - } - int cl = children.length; - if (i < cl) return (Ref) children[i]; - throw new IndexOutOfBoundsException("No ref for index:" + i); - } - - @SuppressWarnings("unchecked") - public BlobMap assoc(ACell key, ACell value) { - if (!(key instanceof ABlob)) return null; - return assocEntry(MapEntry.create((K)key, (V)value)); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public BlobMap dissoc(ABlob k) { - if (count <= 1) { - if (count == 0) return this; // Must already be empty singleton - if (entry.getKey().equalsBytes(k)) { - return (depth==0)?empty():null; - } - return this; // leave existing entry in place - } - long pDepth = depth + prefixLength; // hex depth of this node including prefix - long kl = k.hexLength(); // hex length of key to dissoc - if (kl < pDepth) { - // no match for sure, so no change - return this; - } - if (kl == pDepth) { - // need to check for match with current entry - if (entry == null) return this; - if (!k.equalsBytes(entry.getKey())) return this; - // at this point have matched entry exactly. So need to remove it safely while - // preserving invariants - if (children.length == 1) { - // need to promote child to the current depth - BlobMap c = (BlobMap) children[0].getValue(); - return new BlobMap(depth, (c.depth + c.prefixLength) - depth, c.entry, c.children, c.mask, - count - 1); - } else { - // Clearing current entry, keeping existing children (must be 2+) - return new BlobMap(depth, prefixLength, null, children, mask, count - 1); - } - } - // dissoc beyond current prefix length, so need to check children - int digit = k.getHexDigit(pDepth); - int childIndex = Bits.indexForDigit(digit, mask); - if (childIndex < 0) return this; // key miss - // we know we need to replace a child - BlobMap oldChild = (BlobMap) children[childIndex].getValue(); - BlobMap newChild = oldChild.dissoc(k); - BlobMap r=this.withChild(digit, oldChild, newChild); - - // check if whole blobmap was emptied - if ((r==null)&&(depth==0)) r= empty(); - return r; - } - - /** - * Prefix blob, must contain hex digits in the range [depth,depth+prefixLength). - * - * May contain more hex digits in memory, this is irrelevant from the - * perspective of serialisation. - * - * Typically we populate with the key of the first entry added to avoid - * unnecessary blob instances being created. - */ - private ABlob getPrefix() { - if (entry!=null) return entry.getKey(); - int n=children.length; - if (n==0) return Blob.EMPTY; - return children[0].getValue().getPrefix(); - } - - @Override - protected void accumulateEntrySet(HashSet> h) { - for (int i = 0; i < children.length; i++) { - children[i].getValue().accumulateEntrySet(h); - } - if (entry != null) h.add(entry); - } - - @Override - protected void accumulateKeySet(HashSet h) { - for (int i = 0; i < children.length; i++) { - children[i].getValue().accumulateKeySet(h); - } - if (entry != null) h.add(entry.getKey()); - } - - @Override - protected void accumulateValues(ArrayList al) { - // add this entry first, since we want lexicographic order - if (entry != null) al.add(entry.getValue()); - for (int i = 0; i < children.length; i++) { - children[i].getValue().accumulateValues(al); - } - } - - @Override - public void forEach(BiConsumer action) { - if (entry != null) action.accept(entry.getKey(), entry.getValue()); - for (int i = 0; i < children.length; i++) { - children[i].getValue().forEach(action); - } - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public BlobMap assocEntry(MapEntry e) { - if (count == 0L) return create(e); - if (count == 1L) { - assert (mask == (short) 0); // should be no children - if (entry.keyEquals(e)) { - if (entry == e) return this; - // recreate, preserving current depth - return createAtDepth(e, depth); - } - } - ABlob k = e.getKey(); - long pDepth = this.prefixDepth(); // hex depth of this node including prefix - long newKeyLength = k.hexLength(); // hex length of new key - long mkl; // matched key length - ABlob prefix=getPrefix(); - if (newKeyLength >= pDepth) { - // constrain relevant key length by match with current prefix - mkl = depth + k.hexMatchLength(prefix, depth, prefixLength); - } else { - mkl = depth + k.hexMatchLength(prefix, depth, newKeyLength - depth); - } - if (mkl < pDepth) { - // we collide at a point shorter than the current prefix length - if (mkl == newKeyLength) { - // new key is subset of the current prefix, so split prefix at key position mkl - // doesn't need to adjust child depths, since they are splitting at the same - // point - long newDepth=mkl+1; // depth for new child - BlobMap split = new BlobMap(newDepth, pDepth - newDepth, entry, (Ref[]) children, mask, - count); - int splitDigit = prefix.getHexDigit(mkl); - short splitMask = (short) (1 << splitDigit); - BlobMap result = new BlobMap(depth, mkl - depth, e, new Ref[] { split.getRef() }, - splitMask, count + 1); - return result; - } else { - // we need to fork the current prefix in two at position mkl - long newDepth=mkl+1; // depth for new children - - BlobMap branch1 = new BlobMap(newDepth, pDepth - newDepth, entry, (Ref[]) children, mask, - count); - BlobMap branch2 = new BlobMap(newDepth, newKeyLength - newDepth, e, (Ref[]) EMPTY_CHILDREN, - (short) 0, 1L); - int d1 = prefix.getHexDigit(mkl); - int d2 = k.getHexDigit(mkl); - if (d1 > d2) { - // swap to get in right order - BlobMap temp = branch1; - branch1 = branch2; - branch2 = temp; - } - Ref[] newChildren = new Ref[] { branch1.getRef(), branch2.getRef() }; - short newMask = (short) ((1 << d1) | (1 << d2)); - BlobMap fork = new BlobMap(depth, mkl - depth, null, newChildren, newMask, count + 1L); - return fork; - } - } - assert (newKeyLength >= pDepth); - if (newKeyLength == pDepth) { - // we must have matched the current entry exactly - if (entry == null) { - // just add entry at this position - return new BlobMap(depth, prefixLength, e, (Ref[]) children, mask, count + 1); - } - if (entry == e) return this; - - // swap entry, no need to change count - return new BlobMap(depth, prefixLength, e, (Ref[]) children, mask, count); - } - // at this point we have matched full prefix, but new key length is longer. - // so we need to update (or add) exactly one child - int childDigit = k.getHexDigit(pDepth); - BlobMap oldChild = getChild(childDigit); - BlobMap newChild; - if (oldChild == null) { - newChild = createAtDepth(e, pDepth+1); // Myst be at least 1 beyond current prefix - } else { - newChild = oldChild.assocEntry(e); - } - return withChild(childDigit, oldChild, newChild); // can't be null since associng - } - - /** - * Updates this BlobMap with a new child. - * - * Either oldChild or newChild may be null. Empty maps are treated as null. - * - * @param childDigit Digit for new child - * @param newChild - * @return BlobMap with child removed, or null if BlobMap was deleted entirely - */ - @SuppressWarnings({ "rawtypes", "unchecked"}) - private BlobMap withChild(int childDigit, BlobMap oldChild, BlobMap newChild) { - // consider empty children as null - //if (oldChild == EMPTY) oldChild = null; - //if (newChild == EMPTY) newChild = null; - if (oldChild == newChild) return this; - - int n = children.length; - // we need a new child array - Ref[] newChildren = children; - if (oldChild == null) { - // definitely need a new entry - newChildren = new Ref[n + 1]; - int newPos = Bits.positionForDigit(childDigit, mask); - short newMask = (short) (mask | (1 << childDigit)); - - System.arraycopy(children, 0, newChildren, 0, newPos); // earlier entries - newChildren[newPos] = newChild.getRef(); - System.arraycopy(children, newPos, newChildren, newPos + 1, n - newPos); // later entries - return new BlobMap(depth, prefixLength, entry, newChildren, newMask, - count + newChild.count()); - } else { - // dealing with an existing child - if (newChild == null) { - // need to delete an existing child - int delPos = Bits.positionForDigit(childDigit, mask); - - // handle special case where we need to promote the remaining child - if (entry == null) { - if (n == 2) { - BlobMap rm = (BlobMap) children[1 - delPos].getValue(); - long newPLength = prefixLength + rm.prefixLength+1; - return new BlobMap(depth, newPLength, rm.entry, (Ref[]) rm.children, rm.mask, - rm.count()); - } else if (n == 1) { - // deleting entire BlobMap! - return null; - } - } - if (n==0) { - System.out.print("BlobMap Bad!"); - } - newChildren = new Ref[n - 1]; - short newMask = (short) (mask & ~(1 << childDigit)); - System.arraycopy(children, 0, newChildren, 0, delPos); // earlier entries - System.arraycopy(children, delPos + 1, newChildren, delPos, n - delPos - 1); // later entries - return new BlobMap(depth, prefixLength, entry, newChildren, newMask, - count - oldChild.count()); - } else { - // need to replace a child - int childPos = Bits.positionForDigit(childDigit, mask); - newChildren = children.clone(); - newChildren[childPos] = newChild.getRef(); - long newCount = count + newChild.count() - oldChild.count(); - return new BlobMap(depth, prefixLength, entry, newChildren, mask, newCount); - } - } - } - - @Override - public R reduceValues(BiFunction func, R initial) { - if (entry != null) initial = func.apply(initial, entry.getValue()); - int n = children.length; - for (int i = 0; i < n; i++) { - initial = children[i].getValue().reduceValues(func, initial); - } - return initial; - } - - @Override - public R reduceEntries(BiFunction, ? extends R> func, R initial) { - if (entry != null) initial = func.apply(initial, entry); - int n = children.length; - for (int i = 0; i < n; i++) { - initial = children[i].getValue().reduceEntries(func, initial); - } - return initial; - } - - @Override - public BlobMap filterValues(Predicate pred) { - BlobMap r=this; - for (int i=0; i<16; i++) { - if (r==null) break; // might be null from dissoc - BlobMap oldChild=r.getChild(i); - if (oldChild==null) continue; - BlobMap newChild=oldChild.filterValues(pred); - r=r.withChild(i, oldChild, newChild); - } - - // check entry at this level. A child might have moved here during the above loop! - if (r!=null) { - if ((r.entry!=null)&&!pred.test(r.entry.getValue())) r=r.dissoc(r.entry.getKey()); - } - - // check if whole blobmap was emptied - if (r==null) { - // everything deleted, but need - if (depth==0) r=empty(); - } - return r; - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.BLOBMAP; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = Format.writeVLCLong(bs,pos, count); - if (count == 0) return pos; // nothing more to know... this must be the empty singleton - - pos = Format.writeVLCLong(bs,pos, depth); - pos = Format.writeVLCLong(bs,pos, prefixLength); - - pos = MapEntry.encodeCompressed(entry,bs,pos); // entry may be null - if (count == 1) return pos; // must be a single entry - - // finally write children - pos = Utils.writeShort(bs,pos,mask); - int n = children.length; - for (int i = 0; i < n; i++) { - pos = encodeChild(bs,pos,i); - } - return pos; - } - - private int encodeChild(byte[] bs, int pos, int i) { - Ref> cref = children[i]; - return cref.encode(bs, pos); - - // TODO: maybe compress single entries? -// ABlobMap c=cref.getValue(); -// if (c.count==1) { -// MapEntry me=c.entryAt(0); -// pos = me.getRef().encode(bs, pos); -// } else { -// pos = cref.encode(bs,pos); -// } -// return pos; - } - - @Override - public int estimatedEncodingSize() { - return 100 + (children.length*2+1) * Format.MAX_EMBEDDED_LENGTH; - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static BlobMap read(ByteBuffer bb) throws BadFormatException { - long count = Format.readVLCLong(bb); - if (count < 0) throw new BadFormatException("Negative count!"); - if (count == 0) return (BlobMap) EMPTY; - - long depth = Format.readVLCLong(bb); - if (depth < 0) throw new BadFormatException("Negative depth!"); - long prefixLength = Format.readVLCLong(bb); - if (prefixLength < 0) throw new BadFormatException("Negative prefix length!"); - - // Get entry at this node, might be null - MapEntry me = MapEntry.readCompressed(bb); - - // single entry map - if (count == 1) return new BlobMap(depth, prefixLength, me, EMPTY_CHILDREN, (short) 0, 1L); - - short mask = bb.getShort(); - int n = Utils.bitCount(mask); - Ref[] children = new Ref[n]; - long childDepth=depth+prefixLength+1; // depth for children = this prefixDepth plus one extra hex digit - for (int i = 0; i < n; i++) { - children[i] = readChild(bb,childDepth); - } - return new BlobMap(depth, prefixLength, me, children, mask, count); - } - - @SuppressWarnings({ "rawtypes" }) - private static Ref readChild(ByteBuffer bb, long childDepth) throws BadFormatException { - Ref ref = Format.readRef(bb); - return ref; - // TODO: compression of single entries? -// ACell c=ref.getValue(); -// if (c instanceof BlobMap) { -// return ref; -// } else if (c instanceof AVector) { -// AVector v=(AVector)c; -// MapEntry me=MapEntry.convertOrNull(v); -// if (me==null) throw new BadFormatException("Invalid MApEntry vector as BlobMap child"); -// -// return createAtDepth(me,childDepth).getRef(); -// } else { -// throw new BadFormatException("Bad BlobMap child Type: "+RT.getType(c)); -// } - } - - @Override - protected MapEntry getEntryByHash(Hash hash) { - throw new UnsupportedOperationException(); - } - - @SuppressWarnings("unchecked") - @Override - public void validate() throws InvalidDataException { - super.validate(); - - long ecount = (entry == null) ? 0 : 1; - int n = children.length; - long pDepth = prefixDepth(); - for (int i = 0; i < n; i++) { - ACell o = children[i].getValue(); - if (!(o instanceof BlobMap)) - throw new InvalidDataException("Illegal BlobMap child type: " + Utils.getClass(o), this); - BlobMap c = (BlobMap) o; - - long ccount=c.count(); - if (ccount==0) { - throw new InvalidDataException("Child "+i+" should not be empty! At depth "+depth,this); - } - - if (c.depth != (pDepth+1)) { - throw new InvalidDataException("Child must have depth: " + (pDepth+1) + " but was: " + c.depth, - this); - } - - if (c.prefixDepth() <= prefixDepth()) { - throw new InvalidDataException("Child must have greater total prefix depth than " + prefixDepth() - + " but was: " + c.prefixDepth(), this); - } - c.validate(); - - ecount += ccount; - } - - if (count != ecount) throw new InvalidDataException("Bad entry count: " + ecount + " expected: " + count, this); - } - - /** - * Gets the total depth of this node including prefix - * - * @return - */ - private long prefixDepth() { - return depth + prefixLength; - } - - @Override - public void validateCell() throws InvalidDataException { - if (prefixLength < 0) throw new InvalidDataException("Negative prefix length!" + prefixLength, this); - if (count == 0) { - if (this != EMPTY) throw new InvalidDataException("Non-singleton empty BlobMap", this); - return; - } else if (count == 1) { - if (entry == null) throw new InvalidDataException("Single entry BlobMap with null entry?", this); - if (mask != 0) throw new InvalidDataException("Single entry BlobMap with child mask?", this); - return; - } - - // at least count 2 from this point - int cn = Utils.bitCount(mask); - if (cn != children.length) throw new InvalidDataException( - "Illegal mask: " + Utils.toHexString(mask) + " for given number of children: " + children.length, this); - - if (entry != null) { - entry.validateCell(); - if (cn == 0) - throw new InvalidDataException("BlobMap with entry and count=" + count + " must have children", this); - } else { - if (cn <= 1) throw new InvalidDataException( - "BlobMap with no entry and count=" + count + " must have two or more children", this); - } - } - - @SuppressWarnings("unchecked") - @Override - public BlobMap empty() { - return (BlobMap) EMPTY; - } - - @Override - public MapEntry entryAt(long ix) { - if (entry != null) { - if (ix == 0L) return entry; - ix -= 1; - } - int n = children.length; - for (int i = 0; i < n; i++) { - ABlobMap c = children[i].getValue(); - long cc = c.count(); - if (ix < cc) return c.entryAt(ix); - ix -= cc; - } - throw new IndexOutOfBoundsException(Errors.badIndex(ix)); - } - - /** - * Removes n leading entries from this BlobMap, in key order. - * - * @param n Number of entries to remove - * @return Updated BlobMap with leading entries removed. - * @throws IndexOutOfBoundsException If there are insufficient entries in the - * BlobMap - */ - public BlobMap removeLeadingEntries(long n) { - // TODO: optimise this - BlobMap bm = this; - for (long i = 0; i < n; i++) { - MapEntry me = bm.entryAt(0); - bm = bm.dissoc(me.getKey()); - } - return bm; - } - - /** - * Checks this BlobMap for equality with another map. - * - * @param a Map to compare with - * @return true if maps are equal, false otherwise. - */ - public boolean equals(AMap a) { - if (this == a) return true; // important optimisation for e.g. hashmap equality - if (a == null) return false; - if (this.getType()!=a.getType()) return false; - // Must be a BlobMap - return equals((BlobMap)a); - } - - /** - * Checks this BlobMap for equality with another BlobMap - * - * @param a BlobMap to compare with - * @return true if maps are equal, false otherwise. - */ - public boolean equals(BlobMap a) { - if (a==null) return false; - long n=this.count(); - if (n != a.count()) return false; - if (this.mask!=a.mask) return false; - - if (!Utils.equals(this.entry, a.entry)) return false; - - Hash h=this.cachedHash(); - if (h!=null) { - Hash ha=a.cachedHash(); - if (ha!=null) return h.equals(ha); - } - return getHash().equals(a.getHash()); - } - - @Override - public byte getTag() { - return Tag.BLOBMAP; - } - - @Override - public ACell toCanonical() { - return this; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/BlobMaps.java b/convex-core/src/main/java/convex/core/data/BlobMaps.java deleted file mode 100644 index 0be7cb7f2..000000000 --- a/convex-core/src/main/java/convex/core/data/BlobMaps.java +++ /dev/null @@ -1,38 +0,0 @@ -package convex.core.data; - -import convex.core.lang.RT; -import convex.core.util.Utils; - -public class BlobMaps { - - /** - * Returns the empty BlobMap. Guaranteed singleton. - * - * @param Type of Blob Map - * @param Key type - * @param Value type - * @return The empty BlobMap - */ - @SuppressWarnings("unchecked") - public static , K extends ABlob, V extends ACell> R empty() { - return (R) BlobMap.EMPTY; - } - - @SuppressWarnings("unchecked") - public static , K extends ABlob, V extends ACell> R create(K k, V v) { - return (R) BlobMap.create(k, v); - } - - @SuppressWarnings("unchecked") - public static , K extends ABlob, V extends ACell> R of(Object... kvs) { - int n = kvs.length; - if (Utils.isOdd(n)) throw new IllegalArgumentException("Even number of key + values required"); - BlobMap result = empty(); - for (int i = 0; i < n; i += 2) { - V value=RT.cvm(kvs[i + 1]); - result = result.assoc((K) kvs[i], value); - } - - return (R) result; - } -} diff --git a/convex-core/src/main/java/convex/core/data/BlobTree.java b/convex-core/src/main/java/convex/core/data/BlobTree.java deleted file mode 100644 index 44d1616c8..000000000 --- a/convex-core/src/main/java/convex/core/data/BlobTree.java +++ /dev/null @@ -1,503 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.security.MessageDigest; - -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * Implementation of a large Blob data structure consisting of 2 or more chunks. - * - * Intention is to enable relatively large binary content to be handled without - * too many tree levels, and without too many references in a single tree node - * We choose a branching factor of 16 as a reasonable tradeoff. - * - * Level 1 can hold up to 64k Level 2 can hold up to 1mb Level 3 can hold up to - * 16mb Level 4 can hold up to 256mb ... Level 15 (max) should be big enough for - * the moment - * - * One smart reference is maintained for each child node at each level - * - */ -public class BlobTree extends ABlob { - - public static final int BIT_SHIFT_PER_LEVEL = 4; - public static final int FANOUT = 1 << BIT_SHIFT_PER_LEVEL; - - private final Ref[] children; - private final int shift; - private final long count; - - private BlobTree(Ref[] children, int shift, long count) { - this.children = children; - this.shift = shift; - this.count = count; - } - - /** - * Create a BlobTree from an arbitrary Blob. - * - * Must be of sufficient size to convert to BlobTree - * @param blob Source of BlobTree data - * @return New BolobTree instance - */ - public static BlobTree create(ABlob blob) { - if (blob instanceof BlobTree) return (BlobTree) blob; - - long length = blob.count(); - int chunks = Utils.checkedInt(calcChunks(length)); - Blob[] blobs = new Blob[chunks]; - for (int i = 0; i < chunks; i++) { - int offset = i * Blob.CHUNK_LENGTH; - blobs[i] = blob.slice(offset, Math.min(Blob.CHUNK_LENGTH, length - offset)).toBlob(); - } - return create(blobs); - } - - /** - * Create a BlobTree from an array of children. Each child must be a valid - * chunk. All except the last child must be of the correct chunk size. - * - * @param blobs Blobs to include - * @return New BlobTree - */ - static BlobTree create(Blob... blobs) { - return create(blobs, 0, blobs.length); - } - - /** - * Computes the shift level for a BlobTree with the specified number of chunks - * - * @param chunkCount - * @return Shift value for a BlobTree with the specified number of chunks - */ - static int calcShift(long chunkCount) { - int shift = 0; - while (chunkCount > FANOUT) { - shift += BIT_SHIFT_PER_LEVEL; - chunkCount >>= BIT_SHIFT_PER_LEVEL; - } - return shift; - } - - /** - * Computes the number of chunks for a BlobTree of the given length - * - * @param length The length of the Blob in bytes - * @return Number of chunks needed for a given byte length. - */ - public static long calcChunks(long length) { - return ((length - 1) >> Blobs.CHUNK_SHIFT) + 1; - } - - private static BlobTree createSmall(Blob[] blobs, int offset, int chunkCount) { - long length = 0; - if (chunkCount < 2) throw new IllegalArgumentException("Cannot create BlobTree without at least 2 Blobs"); - @SuppressWarnings("unchecked") - Ref[] children = new Ref[chunkCount]; - for (int i = 0; i < chunkCount; i++) { - Blob blob = blobs[offset + i]; - long childLength = blob.count(); - - if (childLength > Blob.CHUNK_LENGTH) - throw new IllegalArgumentException("BlobTree chunk too large: " + childLength); - if ((i < chunkCount - 1) && (childLength != Blob.CHUNK_LENGTH)) - throw new IllegalArgumentException("Illegal internediate chunk size: " + childLength); - - Ref child = blob.getRef(); - children[i] = child; - length += childLength; - } - return new BlobTree(children, 0, length); - } - - private static BlobTree create(Blob[] blobs, int offset, int chunkCount) { - int shift = calcShift(chunkCount); - if (shift == 0) { - return createSmall(blobs, offset, chunkCount); - } else { - int childSize = 1 << shift; // number of chunks in children - int numChildren = ((chunkCount - 1) >> shift) + 1; - @SuppressWarnings("unchecked") - Ref[] children = new Ref[numChildren]; - - long length = 0; - for (int i = 0; i < numChildren; i++) { - int childOffset = i * childSize; - BlobTree bt = create(blobs, offset + childOffset, Math.min(childSize, chunkCount - childOffset)); - children[i] = bt.getRef(); - length += bt.count; - } - return new BlobTree(children, shift, length); - } - } - -// TODO: better implementation of this -// @Override -// public int compareTo(ABlob o) { -// if (this==o) return 0; -// throw new UnsupportedOperationException(); -// } - - @Override - public boolean isCanonical() { - return count>Blob.CHUNK_LENGTH; - } - - @Override - public final boolean isCVMValue() { - return true; - } - - @Override - public void getBytes(byte[] dest, int destOffset) { - long clen = childLength(); - int n = children.length; - for (int i = 0; i < n; i++) { - getChild(i).getBytes(dest, Utils.checkedInt(destOffset + i * clen)); - } - } - - @Override - public long count() { - return count; - } - - @Override - public ABlob slice(long start, long length) { - if ((start == 0L) && (length == this.count)) return this; - if (start < 0L) throw new IndexOutOfBoundsException(Errors.badIndex(start)); - - long csize = childLength(); - int ci = (int) (start / csize); - if (ci == (start + length - 1) / csize) { - return getChild(ci).slice(start - ci * csize, length); - } - - // FIXME: This looks broken - // TODO: handle big slices more effectively - int alen = Utils.checkedInt(this.count); - byte[] bs = new byte[alen]; - return Blob.wrap(bs, Utils.checkedInt(start), Utils.checkedInt(length)); - } - - private ABlob getChild(int childIndex) { - return children[childIndex].getValue(); - } - - @Override - public Blob toBlob() { - int len = Utils.checkedInt(count()); - byte[] data = new byte[len]; - getBytes(data, 0); - return Blob.wrap(data); - } - - @Override - protected void updateDigest(MessageDigest digest) { - int n = children.length; - for (int i = 0; i < n; i++) { - getChild(i).updateDigest(digest); - } - } - - @Override - public byte getUnchecked(long i) { - int childLength = childLength(); - int ci = (int) (i >> (shift + Blobs.CHUNK_SHIFT)); - return getChild(ci).getUnchecked(i - ci * childLength); - } - - /** - * Gets the length in bytes of each full child of this BlobTree - * @return - */ - private int childLength() { - return 1 << (shift + Blobs.CHUNK_SHIFT); - } - - @Override - public boolean equals(ABlob a) { - if (!(a instanceof BlobTree)) return false; - return equals((BlobTree) a); - } - - public boolean equals(BlobTree b) { - if (b == this) return true; - if (b.count != count) return false; - int n = children.length; - for (int i = 0; i < n; i++) { - if (!children[i].equals(b.children[i])) return false; - } - return true; - } - - @Override - public boolean equalsBytes(byte[] bytes, int byteOffset) { - int clen=childLength(); - for (int i=0; i> shift) + 1); - if ((numChildren < 2) || (numChildren > FANOUT)) { - throw new BadFormatException( - "Invalid number of children [" + numChildren + "] for BlobTree with length: " + count); - } - - @SuppressWarnings("unchecked") - Ref[] children = (Ref[]) new Ref[numChildren]; - for (int i = 0; i < numChildren; i++) { - Ref ref = Format.readRef(bb); - children[i] = ref; - } - - return new BlobTree(children, shift, count); - } - - public static BlobTree read(Blob src, long count) throws BadFormatException { - int headerLength = (1 + Format.getVLCLength(count)); - long chunks = calcChunks(count); - int shift = calcShift(chunks); - int numChildren = Utils.checkedInt(((chunks - 1) >> shift) + 1); - - @SuppressWarnings("unchecked") - Ref[] children = (Ref[]) new Ref[numChildren]; - - ByteBuffer bb=src.getByteBuffer(); - bb.position(headerLength); - for (int i = 0; i < numChildren; i++) { - Ref ref = Format.readRef(bb); - children[i] = ref; - } - - return new BlobTree(children, shift, count); - } - - @Override - public int estimatedEncodingSize() { - return 1 + Format.MAX_VLC_LONG_LENGTH + Ref.INDIRECT_ENCODING_LENGTH * children.length; - } - - @Override - public ABlob append(ABlob d) { - // TODO: optimise - return toBlob().append(d); - } - - @Override - public Blob getChunk(long chunkIndex) { - long childSize = 1 << shift; - int child = Utils.checkedInt(chunkIndex >> shift); - return getChild(child).getChunk(chunkIndex - child * childSize); - } - - @Override - public void validate() throws InvalidDataException { - super.validate(); - int n = children.length; - if ((n < 2) | (n > FANOUT)) throw new InvalidDataException("Illegal number of BlobTree children: " + n, this); - int clen = childLength(); - long total = 0; - - // We need to validate and check the lengths of all child notes. Note that only the last child can - // be shorted than the defined childLength() for this shift level. - for (int i = 0; i < n; i++) { - ABlob child; - child = getChild(i); - child.validate(); - - long cl = child.count(); - total += cl; - if (i == (n - 1)) { - if (cl > clen) throw new InvalidDataException( - "Illegal last child length: " + cl + " expected less than or equal to " + clen, this); - } else { - if (cl != clen) - throw new InvalidDataException("Illegal child length: " + cl + " expected " + clen, this); - } - } - if (total != count) throw new InvalidDataException("Incorrect total child count: " + total, this); - } - - @Override - public ByteBuffer getByteBuffer() { - throw new UnsupportedOperationException("Can't get bytebuffer for " + this.getClass()); - } - - @Override - public String toHexString() { - StringBuilder sb = new StringBuilder(); - toHexString(sb); - return sb.toString(); - } - - @Override - public void toHexString(StringBuilder sb) { - for (int i = 0; i < children.length; i++) { - children[i].getValue().toHexString(sb); - } - } - - @Override - public void validateCell() throws InvalidDataException { - int n = children.length; - if ((n < 2) | (n > FANOUT)) throw new InvalidDataException("Illegal number of BlobTree children: " + n, this); - } - - @Override - public long commonHexPrefixLength(ABlob b) { - long cpl = 0; - long DIGITS_PER_CHUNK = Blob.CHUNK_LENGTH * 2; - for (long i = 0;; i++) { - long cl = getChunk(i).commonHexPrefixLength(b.getChunk(i)); - if (cl < DIGITS_PER_CHUNK) return cpl + cl; - cpl += DIGITS_PER_CHUNK; - } - } - - @Override - public long hexMatchLength(ABlob b, long start, long length) { - long HEX_CHUNK_LENGTH = (Blob.CHUNK_LENGTH * 2); - long end = start + length; - long endChunk = (end - 1) / HEX_CHUNK_LENGTH; - for (long ci = start / HEX_CHUNK_LENGTH; ci < endChunk; ci++) { - long cpos = ci * HEX_CHUNK_LENGTH; // position of chunk - long cs = Math.max(0, start - cpos); // start position within chunk - long ce = Math.min(HEX_CHUNK_LENGTH, end - cpos); // end position within chunk - long clen = ce - cs; // length to check within chunk - long match = getChunk(ci).hexMatchLength(b.getChunk(ci), cs, clen); - if (match < clen) return cpos + cs + match; - } - return length; - } - - @Override - public long longValue() { - if (count != 8) throw new IllegalStateException(Errors.wrongLength(8, count)); - return getChunk(0).longValue(); - } - - @Override - public long toLong() { - return slice(count-8,8).toLong(); - } - - @Override - public int getRefCount() { - return children.length; - } - - @SuppressWarnings("unchecked") - @Override - public Ref getRef(int i) { - return (Ref) children[i]; - } - - @Override - public BlobTree updateRefs(IRefFunction func) { - Ref[] newChildren = Ref.updateRefs(children, func); - return withChildren(newChildren); - } - - private BlobTree withChildren(Ref[] newChildren) { - if (children == newChildren) return this; - return new BlobTree(newChildren, shift, count); - } - - @Override - public boolean isRegularBlob() { - return true; - } - - @Override - public byte getTag() { - return Tag.BLOB; - } - - @Override - public ABlob toCanonical() { - if (isCanonical()) return this; - return Blobs.toCanonical(this); - } - - - -} diff --git a/convex-core/src/main/java/convex/core/data/Blobs.java b/convex-core/src/main/java/convex/core/data/Blobs.java deleted file mode 100644 index 71bbd6a98..000000000 --- a/convex-core/src/main/java/convex/core/data/Blobs.java +++ /dev/null @@ -1,96 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.util.Random; - -import convex.core.exceptions.BadFormatException; -import convex.core.util.Utils; - -public class Blobs { - - static final int CHUNK_SHIFT = 12; - - public static final int MAX_ENCODING_LENGTH = Math.max(Blob.MAX_ENCODING_LENGTH, BlobTree.MAX_ENCODING_LENGTH); - - public static T createRandom(long length) { - return createRandom(new Random(),length); - } - - @SuppressWarnings("unchecked") - public static T createRandom(Random r, long length) { - if (length <= Blob.CHUNK_LENGTH) return (T) Blob.createRandom(r, length); - - int numChunks = Utils.checkedInt(((length - 1) >> CHUNK_SHIFT) + 1); - - Blob[] blobs = new Blob[numChunks]; - for (int i = 0; i < numChunks; i++) { - Blob b = Blob.createRandom(r, Math.min(Blob.CHUNK_LENGTH, length - i * Blob.CHUNK_LENGTH)); - blobs[i] = b; - } - return (T) BlobTree.create(blobs); - } - - /** - * Converts any blob to a the correct canonical Blob format - * @param a Any Blob - * @return Canonical version s a Blob or BlobTree - */ - public static ABlob toCanonical(ABlob a) { - long length = a.count(); - if (length <= Blob.CHUNK_LENGTH) return a.toBlob(); - return BlobTree.create(a); - } - - /** - * Creates a blob from a hex string - * @param a Hex String - * @return Blob created, or null if String not valid hex - */ - public static ABlob fromHex(String a) { - long slength = a.length(); - if ((slength & 1) != 0) return null; - Blob fullBlob = Blob.fromHex(a); - - long length = slength / 2; - if (length <= Blob.CHUNK_LENGTH) return fullBlob; - return BlobTree.create(fullBlob); - } - - /** - * Reads a Blob from a ByteBuffer. - * - * @param bb ByteBuffer starting with a blob encoding - * @return Blob read from ByteBuffer - * @throws BadFormatException If format is invalid - */ - public static ABlob read(ByteBuffer bb) throws BadFormatException { - long len = Format.readVLCLong(bb); - if (len < 0L) throw new BadFormatException("Negative blob length?"); - if (len > Blob.CHUNK_LENGTH) return BlobTree.read(bb, len); - byte[] buff = new byte[Utils.checkedInt(len)]; - bb.get(buff); - return Blob.wrap(buff); - // TODO keep byte format representation? - } - - @SuppressWarnings("unchecked") - public static T readFromBlob(Blob source) throws BadFormatException { - int sLen = source.length; - if (sLen < 2) throw new BadFormatException("Trying to read Blob from insufficient source of size " + sLen); - // read length at position 1 (skipping tag) - long len = Format.readVLCLong(source.store, source.offset + 1); - - T result = null; - if (len < 0L) throw new BadFormatException("Negative blob length?"); - if (len > Blob.CHUNK_LENGTH) { - result = (T) BlobTree.read(source, len); - } else { - result = (T) Blob.read(source, len); - } - // we can attach original blob as source at this point - result.attachEncoding(source); - return result; - } - - -} diff --git a/convex-core/src/main/java/convex/core/data/Format.java b/convex-core/src/main/java/convex/core/data/Format.java deleted file mode 100644 index cef96f45f..000000000 --- a/convex-core/src/main/java/convex/core/data/Format.java +++ /dev/null @@ -1,981 +0,0 @@ -package convex.core.data; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.nio.BufferOverflowException; -import java.nio.BufferUnderflowException; -import java.nio.ByteBuffer; -import java.nio.charset.CharacterCodingException; -import java.nio.charset.Charset; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.CharsetEncoder; -import java.nio.charset.CodingErrorAction; - -import convex.core.Belief; -import convex.core.Block; -import convex.core.BlockResult; -import convex.core.Order; -import convex.core.Result; -import convex.core.State; -import convex.core.data.prim.CVMBool; -import convex.core.data.prim.CVMByte; -import convex.core.data.prim.CVMChar; -import convex.core.data.prim.CVMDouble; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.lang.AFn; -import convex.core.lang.Core; -import convex.core.lang.Ops; -import convex.core.lang.RT; -import convex.core.lang.impl.Fn; -import convex.core.lang.impl.MultiFn; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Call; -import convex.core.transactions.Invoke; -import convex.core.transactions.Transfer; -import convex.core.util.Utils; - -/** - * Static utility class for message format encoding - * - * "Standards are always out of date. That's what makes them standards." - Alan - * Bennett - */ -public class Format { - - /** - * 8191 byte system-wide limit on the legal length of a data object encoding. - * - * Technical reasons for this choice: - *
    - *
  • This is the max length that can be VLC encoded in a 2 byte message header. This simplifies message encoding and decoding.
  • - *
  • It is big enough to include a 4096-byte Blob
  • - *
- */ - public static final int LIMIT_ENCODING_LENGTH = 0x1FFF; - - /** - * Maximum length for a VLC encoded Long - */ - public static final int MAX_VLC_LONG_LENGTH = 10; // 70 bits - - /** - * Maximum size in bytes of an embedded value, including tag - */ - public static final int MAX_EMBEDDED_LENGTH=140; // TODO: reconsider - - /** - * Encoded length of a null value - */ - public static final int NULL_ENCODING_LENGTH = 1; - - - /** - * Maximum length in bytes of a Ref encoding (may be an embedded data object) - */ - public static final int MAX_REF_LENGTH = Math.max(Ref.INDIRECT_ENCODING_LENGTH, MAX_EMBEDDED_LENGTH); - - /** - * Gets the length in bytes of VLC encoding for the given long value - * @param x Long value to encode - * @return Length of VLC encoding - */ - public static int getVLCLength(long x) { - if ((x < 64) && (x >= -64)) { - return 1; - } - int bitLength = Utils.bitLength(x); - int blen = (bitLength + 6) / 7; - return blen; - } - - /** - * Puts a VLC encoded long into the specified bytebuffer (with no tag) - * - * Format: - *
    - *
  • MSB of each byte 0=last octet, 1=more octets
  • - *
  • Following MSB, 7 bits of integer representation for each octet
  • - *
  • Second highest bit of first byte is interpreted as the sign
  • - *
- * @param bb ByteBuffer to write to - * @param x Value to VLC encode - * @return Updated ByteBuffer - */ - public static ByteBuffer writeVLCLong(ByteBuffer bb, long x) { - if ((x < 64) && (x >= -64)) { - // single byte, cleared high bit - byte single = (byte) (x & 0x7F); - return bb.put(single); - } - int bitLength = Utils.bitLength(x); - int blen = (bitLength + 6) / 7; - for (int i = blen - 1; i >= 1; i--) { - byte single = (byte) (0x80 | (x >> (7 * i))); // 7 bits with high bit set - bb = bb.put(single); - } - byte end = (byte) (x & 0x7F); // last 7 bits of long, high bit zero - return bb.put(end); - } - - /** - * Puts a variable length integer into the specified byte array (with no tag) - * - * Format: - *
    - *
  • MSB of each byte 0=last octet, 1=more octets
  • - *
  • Following MSB, 7 bits of integer representation for each octet
  • - *
  • Second highest bit of first byte is interpreted as the sign
  • - *
- * - * @param bs Byte array to write to - * @param pos Initial position in byte array - * @param x Long value to write - * @return end position in byte array after writing VLC long - */ - public static int writeVLCLong(byte[] bs, int pos, long x) { - if ((x < 64) && (x >= -64)) { - // single byte, cleared high bit - byte single = (byte) (x & 0x7F); - bs[pos++]=single; - return pos; - } - - int bitLength = Utils.bitLength(x); - int blen = (bitLength + 6) / 7; - for (int i = blen - 1; i >= 1; i--) { - byte single = (byte) (0x80 | (x >> (7 * i))); // 7 bits with high bit set - bs[pos++]=single; - } - byte end = (byte) (x & 0x7F); // last 7 bits of long, high bit zero - bs[pos++]=end; - return pos; - } - - /** - * Reads a VLC encoded long from the given ByteBuffer. Assumes no tag - * - * @param bb ByteBuffer from which to read - * @return Long value from ByteBuffer - * @throws BadFormatException If encoding is invalid - */ - public static long readVLCLong(ByteBuffer bb) throws BadFormatException { - byte octet = bb.get(); - long result = vlcSignExtend(octet); // sign extend 7th bit to all bits - int bitsRead = 7; - int sevenBits = octet & 0x7F; - final boolean signOnly = (sevenBits == 0x00) || (sevenBits == 0x7F); // flag for continuation with sign only - while ((octet & 0x80) != 0) { - if (bitsRead > 64) throw new BadFormatException("VLC long encoding too long for long value"); - octet = bb.get(); - sevenBits = octet & 0x7F; - if (signOnly && (bitsRead == 7)) { // only need to test on first iteration - boolean signBit = (sevenBits & 0x40) != 0; // top bit from current 7 bits - boolean resultSignBit = (result < 0L); // sign bit from first octet - if (signBit == resultSignBit) - throw new BadFormatException("VLC long encoding not canonical, excess leading sign byte(s)"); - } - - // continue while high bit of byte set - result = (result << 7) | sevenBits; // shift and set next 7 lowest bits - bitsRead += 7; - } - if ((bitsRead > 63) && !signOnly) { - throw new BadFormatException("VLC long encoding not canonical, non-sign information beyond 63 bits read"); - } - return result; - } - - /** - * Sign extend 7th bit (sign) of a byte to all bits in a long - * - * @param b Byte to extend - * @return The sign-extended byte as a long - */ - public static long vlcSignExtend(byte b) { - return (((long) b) << 57) >> 57; - } - - /** - * Reads a VLC encoded long as a long from the given location in a byte byte - * array. Assumes no tag - * @param data Byte array - * @param pos Position from which to read in byte array - * @return long value from byte array - * @throws BadFormatException If format is invalid, or reading beyond end of - * array - */ - public static long readVLCLong(byte[] data, int pos) throws BadFormatException { - byte octet = data[pos++]; - long result = (((long) octet) << 57) >> 57; // sign extend 7th bit to 64th bit - int bits = 7; - while ((octet & 0x80) != 0) { - if (pos >= data.length) throw new BadFormatException("VLC encoding beyong end of array"); - if (bits > 64) throw new BadFormatException("VLC encoding too long for long value"); - octet = data[pos++]; - // continue while high bit of byte set - result = (result << 7) | (octet & 0x7F); // shift and set next 7 lowest bits - bits += 7; - } - return result; - } - - /** - * Peeks for a VLC encoded message length at the start of a ByteBuffer, which - * must contain at least 1 byte, maximum 2. - * - * Does not move the buffer position. - * - * @param bb ByteBuffer containing a message length - * @return The message length - * @throws BadFormatException If the ByteBuffer does not start with a valid - * message length - */ - public static int peekMessageLength(ByteBuffer bb) throws BadFormatException { - int len = bb.get(0); - - // Zero message length not allowed - if (len == 0) { - throw new BadFormatException( - "Format.peekMessageLength: Zero message length:" + Utils.readBufferData(bb)); - } - - if ((len & 0x40) != 0) { - // sign bit from top byte looks wrong! - String hex = Utils.toHexString((byte) len); - throw new BadFormatException( - "Format.peekMessageLength: Expected positive message length, got first byte [" + hex + "]"); - } - - if ((len & 0x80) == 0) { - // 1 byte header (without high bit set) - return len & 0x3F; - } - - int lsb = bb.get(1); - if ((lsb & 0x80) != 0) { - String hex = Utils.toHexString((byte) len) + Utils.toHexString((byte) lsb); - throw new BadFormatException( - "Format.peekMessageLength: Max 2 bytes allowed in VLC encoded message length, got [" + hex + "]"); - } - len = ((len & 0x3F) << 7) + lsb; - - return len; - } - - /** - * Writes a message length as a VLC encoded long - * - * @param bb ByteBuffer with capacity available for writing - * @param len Length of message to write - * @return The ByteBuffer after writing the message length - */ - public static ByteBuffer writeMessageLength(ByteBuffer bb, int len) { - if ((len <= 0) || (len > LIMIT_ENCODING_LENGTH)) { - throw new IllegalArgumentException("Invalid message length: " + len); - } - return writeVLCLong(bb, len); - } - - public static ByteBuffer writeVLCBigInteger(ByteBuffer bb, BigInteger value) { - int bitLength = value.bitLength() + 1; // bits required including sign bit - if (bitLength <= 64) { - return writeVLCLong(bb, value.longValue()); - } - byte[] bs = value.toByteArray(); - int bslen = bs.length; - int blen = (bitLength + 6) / 7; // number of octets required for encoding - for (int i = blen - 1; i >= 1; i--) { - int bits7 = Utils.extractBits(bs, 7, i * 7); // get 7 bits from source bytes - byte single = (byte) (0x80 | bits7); // 7 bits with high bit set - bb = bb.put(single); - } - byte end = (byte) (bs[bslen - 1] & 0x7F); // last 7 bits of last byte - return bb.put(end); - } - - /** - * Finds the first byte in a bytebuffer which is a VLC terminal byte, starting - * from the current position. - * - * @param bb A ByteBuffer starting with a VLC encoded value. - * @return VLC terminal byte position (relative to position of bytebuffer), or - * -1 if not found - */ - private static int findVLCTerminal(ByteBuffer bb) { - int pos = bb.position(); - int len = bb.remaining(); - for (int i = 0; i < len; i++) { - byte x = bb.get(pos + i); - if ((x & 0x80) == 0) return i; - } - return -1; - } - - /** - * Reads a BigInteger from the ByteBuffer. Assumes tag already read. - * - * @param bb ByteBuffer to read from - * @return A BigInteger - * @throws BadFormatException If format is invalid - */ - public static BigInteger readVLCBigInteger(ByteBuffer bb) throws BadFormatException { - int vlclen = findVLCTerminal(bb) + 1; - if (vlclen == 0) throw new BadFormatException("No terminal byte found for VLC encoding of BigInteger"); - - /** get bytes of VLC encoding */ - byte[] vlc = new byte[vlclen]; - bb.get(vlc); - assert ((vlc[vlclen - 1] & 0x80) == 0); // check for terminal byte in correct position - - /** bytes needed to contain VLC encoding bits */ - int blen = (vlclen * 7 + 7) / 8; - byte[] bs = new byte[blen]; - boolean signBit = (vlc[0] & 0x40) != 0; - boolean signOnly = (vlc[0] == (byte) 0xFF) | (vlc[0] == (byte) 0x80); // continuation with sign - bs[0] = (byte) (signBit ? -1 : 0); // initialise first byte with sign - for (int i = 0; i < vlclen; i++) { // iterate over all bytes in VLC encoding starting from highest - byte bits7 = (byte) (vlc[i] & 0x7F); // 7 bits from VLC byte - - // if the top byte could have been sign extended, need to check for canonical - // encoding on the next highest byte - if (signOnly && (i == 1)) { - boolean thisSign = (bits7 & 0x40) != 0; - if (thisSign == signBit) - throw new BadFormatException("Non-canonical BigInteger with VLC bytes " + Blob.wrap(vlc)); - } - Utils.setBits(bs, 7, 7 * (vlclen - 1 - i), bits7); - } - return new BigInteger(bs); - } - - /** - * Writes a canonical object to a ByteBuffer, preceded by the appropriate tag - * - * @param bb ByteBuffer to write to - * @param cell Cell to write (may be null) - * @return The ByteBuffer after writing the specified object - */ - public static ByteBuffer write(ByteBuffer bb, ACell cell) { - // first check for null - if (cell == null) { - return bb.put(Tag.NULL); - } - // Generic handling for all non-null CVM types - return cell.write(bb); - } - - /** - * Writes a canonical object to a byte array, preceded by the appropriate tag - * - * @param bs Byte array to write to - * @param pos Starting position to write in byte array - * @param cell Cell to write (may be null) - * @return Position in byte array after writing the specified object - */ - public static int write(byte[] bs, int pos, ACell cell) { - if (cell==null) { - bs[pos++]=Tag.NULL; - return pos; - } - return cell.encode(bs,pos); - } - - public static ByteBuffer writeVLCBigDecimal(ByteBuffer bb, BigDecimal value) { - bb = bb.put((byte) value.scale()); - bb = writeVLCBigInteger(bb, value.unscaledValue()); - return bb; - } - - public static BigDecimal readVLCBigDecimal(ByteBuffer bb) throws BadFormatException { - byte scale = bb.get(); - BigInteger value = readVLCBigInteger(bb); - return new BigDecimal(value, scale); - } - - /** - * Writes a UTF-8 String to the byteBuffer. Includes string tag and length - * - * @param bb ByteBuffer to write to - * @param s String to write - * @return ByteBuffer after writing - */ - public static ByteBuffer writeUTF8String(ByteBuffer bb, String s) { - bb = bb.put(Tag.STRING); - return writeRawUTF8String(bb, s); - } - - /** - * Writes a raw string without tag to the byteBuffer. Includes length in bytes - * of UTF-8 representation - * - * @param bb ByteBuffer to write to - * @param s String to write - * @return ByteBuffer after writing - */ - public static ByteBuffer writeRawUTF8String(ByteBuffer bb, String s) { - if (s.length() == 0) { - bb = writeLength(bb, 0); - } else { - byte[] bs = Utils.toByteArray(s); - bb = writeLength(bb, bs.length); - bb = bb.put(bs); - } - return bb; - } - - /** - * Writes a raw string without tag to the byte array. Includes length in bytes - * of UTF-8 representation - * - * @param bs Byte array - * @param pos Starting position to write in byte array - * @param s String to write - * @return Position in byte array after writing - */ - public static int writeRawUTF8String(byte[] bs, int pos, String s) { - if (s.length() == 0) { - // zero length, no string bytes - return writeVLCLong(bs,pos,0); - } - - byte[] sBytes = Utils.toByteArray(s); - int n=sBytes.length; - pos = writeVLCLong(bs,pos, sBytes.length); - System.arraycopy(sBytes, 0, bs, pos, n); - return pos+n; - } - - private static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); - private static final ThreadLocal UTF8_DECODERS = new ThreadLocal() { - @Override - protected CharsetDecoder initialValue() { - CharsetDecoder dec = UTF8_CHARSET.newDecoder(); - dec.onUnmappableCharacter(CodingErrorAction.REPORT); - dec.onMalformedInput(CodingErrorAction.REPORT); - return dec; - } - }; - - private static final ThreadLocal UTF8_ENCODERS = new ThreadLocal() { - @Override - protected CharsetEncoder initialValue() { - CharsetEncoder dec = UTF8_CHARSET.newEncoder(); - dec.onUnmappableCharacter(CodingErrorAction.REPORT); - dec.onMalformedInput(CodingErrorAction.REPORT); - return dec; - } - }; - - - /** - * Reads a UTF-8 String from a ByteBuffer. Assumes the object tag has already been - * read - * - * @param bb ByteBuffer to read from - * @return String from ByteBuffer - * @throws BadFormatException If encoding is invalid - */ - public static String readUTF8String(ByteBuffer bb) throws BadFormatException { - try { - int len = readLength(bb); - if (len == 0) return ""; - - byte[] bs = new byte[len]; - bb.get(bs); - - String s = UTF8_DECODERS.get().decode(ByteBuffer.wrap(bs)).toString(); - return s; - // return new String(bs, StandardCharsets.UTF_8); - } catch (BufferUnderflowException e) { - throw new BadFormatException("Buffer underflow", e); - } catch (CharacterCodingException e) { - throw new BadFormatException("Bad UTF-8 format", e); - } - } - - /** - * Reads a Symbol from a ByteBuffer. Assumes the object tag has already been - * read - * - * @param bb ByteBuffer from which to read a Symbol - * @return Symbol read from ByteBuffer - * @throws BadFormatException If encoding is invalid - */ - public static Symbol readSymbol(ByteBuffer bb) throws BadFormatException { - return Symbol.read(bb); - } - - public static ByteBuffer writeLength(ByteBuffer bb, int i) { - bb = writeVLCLong(bb, i); - return bb; - } - - /** - * Read an int length field (used for Strings etc.) - * - * @param bb ByteBuffer from which to read - * @return Length field - * @throws BadFormatException If encoding is invalid - */ - public static int readLength(ByteBuffer bb) throws BadFormatException { - // our strategy to to read along, then test if it is a valid non-negative int - long l = readVLCLong(bb); - int li = (int) l; - if (l != li) throw new BadFormatException("Bad length, out of integer range: " + l); - if (li < 0) throw new BadFormatException("Negative length: " + li); - return li; - } - - /** - * Writes a 64-bit long as 8 bytes to the ByteBuffer provided - * - * @param bb Destination ByteBuffer - * @param value Value to write - * @return ByteBuffer after writing - */ - public static ByteBuffer writeLong(ByteBuffer bb, long value) { - return bb.putLong(value); - } - - /** - * Reads a 64-bit long as 8 bytes from the ByteBuffer provided - * - * @param bb Destination ByteBuffer - * @return long value - */ - public static long readLong(ByteBuffer bb) { - return bb.getLong(); - } - - /** - * Reads a Ref from the ByteBuffer. - * - * Converts Embedded objects to Refs automatically. - * - * @param Type of referenced value - * @param bb ByteBuffer containing a ref to read - * @return Ref as read from ByteBuffer - * @throws BadFormatException If the data is badly formatted, or a non-embedded - * object is found. - */ - @SuppressWarnings("unchecked") - public static Ref readRef(ByteBuffer bb) throws BadFormatException { - byte tag=bb.get(); - if (tag==Tag.REF) return Ref.readRaw(bb); - ACell cell= Format.read(tag,bb); - if (cell==null) return (Ref) Ref.NULL_VALUE; - return cell.getRef(); - } - - @SuppressWarnings("unchecked") - private static T readDataStructure(ByteBuffer bb, byte tag) throws BadFormatException { - if (tag == Tag.VECTOR) return (T) Vectors.read(bb); - - if (tag == Tag.MAP) return (T) Maps.read(bb); - - if (tag == Tag.SYNTAX) return (T) Syntax.read(bb); - - if (tag == Tag.SET) return (T) Sets.read(bb); - - if (tag == Tag.LIST) return (T) List.read(bb); - - if (tag == Tag.BLOBMAP) return (T) BlobMap.read(bb); - - throw new BadFormatException("Can't read data structure with tag byte: " + tag); - } - - private static ACell readCode(ByteBuffer bb, byte tag) throws BadFormatException { - if (tag == Tag.OP) return Ops.read(bb); - if (tag == Tag.CORE_DEF) { - Symbol sym = Symbol.read(bb); - // TODO: consider if dependency of format on core bad? - ACell o = Core.ENVIRONMENT.get(sym); - if (o == null) throw new BadFormatException("Core definition not found [" + sym + "]"); - return o; - } - - if (tag == Tag.FN_MULTI) { - AFn fn = MultiFn.read(bb); - return fn; - } - - if (tag == Tag.FN) { - AFn fn = Fn.read(bb); - return fn; - } - - throw new BadFormatException("Can't read Op with tag byte: " + Utils.toHexString(tag)); - } - - /** - * Decodes a single Value from a Blob. Assumes the presence of a tag. - * throws an exception if the Blob contents are not fully consumed - * - * @param blob Blob representing the Encoding of the Value - * @return Value read from the blob of encoded data - * @throws BadFormatException In case of encoding error - */ - public static T read(Blob blob) throws BadFormatException { - byte tag = blob.byteAt(0); - return read(tag,blob); - } - - /** - * Read from a Blob with the specified tag - * @param Type of value to read - * @param tag Tag to use for reading - * @param blob Blob to read from - * @return Value decoded - * @throws BadFormatException If encoding is invalid for the given tag - */ - @SuppressWarnings("unchecked") - public static T read(byte tag, Blob blob) throws BadFormatException { - if (tag == Tag.NULL) { - long len=blob.count(); - if (len!=1) throw new BadFormatException("Bad null encoding with length"+len); - return null; - } - if (tag == Tag.BLOB) { - return (T) Blobs.readFromBlob(blob); - } else { - // TODO: maybe refactor to avoid read from byte buffers? - ByteBuffer bb = blob.getByteBuffer().position(1); - T result; - - try { - result = (T) read(tag,bb); - if (bb.hasRemaining()) throw new BadFormatException( - "Blob with type " + Utils.getClass(result) + " has excess bytes: " + bb.remaining()); - } catch (BufferUnderflowException e) { - throw new BadFormatException("Blob has insufficients bytes: " + blob.count(), e); - } - - result.attachEncoding(blob); - return result; - } - } - - /** - * Read a value encoded as a hex string - * @param Type of value to read - * @param hexString A valid hex String - * @return Value read - * @throws BadFormatException If encoding is invalid - */ - public static T read(String hexString) throws BadFormatException { - return read(Blob.fromHex(hexString)); - } - - /** - * Reads a basic type (primitives and numerics) with the given tag - * - * @param bb ByteBuffer to read from - * @param tag Tag byte indicating type to read - * @return Cell value read - - * @throws BadFormatException If encoding is invalid - * @throws BufferUnderflowException if the ByteBuffer contains insufficent bytes for Encoding - */ - @SuppressWarnings("unchecked") - private static T readBasicType(ByteBuffer bb, byte tag) throws BadFormatException, BufferUnderflowException { - try { - if (tag == Tag.NULL) return null; - if (tag == Tag.BYTE) return (T) CVMByte.create(bb.get()); - if (tag == Tag.CHAR) return (T) CVMChar.create(bb.getChar()); - if (tag == Tag.LONG) return (T) CVMLong.create(readVLCLong(bb)); - if (tag == Tag.DOUBLE) return (T) CVMDouble.create(bb.getDouble()); - - throw new BadFormatException("Can't read basic type with tag byte: " + tag); - } catch (IllegalArgumentException e) { - throw new BadFormatException("Format error basic type with tag byte: " + tag); - } - } - - /** - * Reads a Record with the given tag - * - * @param bb ByteBuffer to read from - * @param tag Tag byte indicating type to read - * @return Record value read - * @throws BadFormatException In case of a bad record encoding - */ - @SuppressWarnings("unchecked") - private static T readRecord(ByteBuffer bb, byte tag) throws BadFormatException { - if (tag == Tag.BLOCK) { - return (T) Block.read(bb); - } - if (tag == Tag.STATE) { - return (T) State.read(bb); - } - if (tag == Tag.ORDER) { - return (T) Order.read(bb); - } - if (tag == Tag.BELIEF) { - return (T) Belief.read(bb); - } - - if (tag == Tag.RESULT) { - return (T) Result.read(bb); - } - - if (tag == Tag.BLOCK_RESULT) { - return (T) BlockResult.read(bb); - } - - - throw new BadFormatException("Can't read record type with tag byte: " + tag); - } - - /** - *

- * Reads one complete Cell from a ByteBuffer. - *

- * - *

- * May return any valid Cell (including null) - *

- * - *

- * Assumes the presence of an object tag. - *

- * - * @param bb ByteBuffer from which to read - * @return Value read from the ByteBuffer - * @throws BadFormatException If encoding is invalid - */ - public static T read(ByteBuffer bb) throws BadFormatException { - byte tag = bb.get(); - return read(tag,bb); - } - - @SuppressWarnings("unchecked") - static T read(byte tag,ByteBuffer bb) throws BadFormatException { - try { - if ((tag & 0xF0) == 0x00) return readBasicType(bb, tag); - - if (tag == Tag.STRING) return (T) Strings.read(bb); - if (tag == Tag.BLOB) return (T) Blobs.read(bb); - if (tag == Tag.SYMBOL) return (T) readSymbol(bb); - if (tag == Tag.KEYWORD) return (T) Keyword.read(bb); - - if (tag == Tag.TRUE) return (T) CVMBool.TRUE; - if (tag == Tag.FALSE) return (T) CVMBool.FALSE; - - if (tag == Tag.ADDRESS) return (T) Address.readRaw(bb); - if (tag == Tag.SIGNED_DATA) return (T) SignedData.read(bb); - - if ((tag & 0xF0) == 0x80) return readDataStructure(bb, tag); - - if ((tag & 0xF0) == 0xA0) return (T) readRecord(bb, tag); - - if ((tag & 0xF0) == 0xD0) return (T) readTransaction(bb, tag); - - if (tag == Tag.PEER_STATUS) return (T) PeerStatus.read(bb); - if (tag == Tag.ACCOUNT_STATUS) return (T) AccountStatus.read(bb); - - if ((tag & 0xF0) == 0xC0) return (T) readCode(bb, tag); - } catch (IllegalArgumentException e) { - throw new BadFormatException("Illegal argument reading encoding", e); - } catch (ClassCastException e) { - throw new BadFormatException("Unexpected data type when decoding: "+e.getMessage(), e); - } - - // report error - int pos = bb.position() - 1; - bb.position(0); - ABlob data = Utils.readBufferData(bb); - throw new BadFormatException("Don't recognise tag: " + Utils.toHexString(tag) + " at position " + pos - + " Content: " + data.toHexString()); - } - - static ATransaction readTransaction(ByteBuffer bb, byte tag) throws BadFormatException { - if (tag == Tag.INVOKE) { - return Invoke.read(bb); - } else if (tag == Tag.TRANSFER) { - return Transfer.read(bb); - } else if (tag == Tag.CALL) { - return Call.read(bb); - } - throw new BadFormatException("Can't read Transaction with tag " + Utils.toHexString(tag)); - } - - /** - * Returns true if the object is a canonical data object. Canonical data objects - * can be used as first class decentralised data objects. - * - * @param o Value to test - * @return true if object is canonical, false otherwise. - */ - public static boolean isCanonical(ACell o) { - if (o==null) return true; - return o.isCanonical(); - } - - /** - * Determines if an object should be embedded directly in the encoding rather - * than referenced with a Ref / hash. Defined to be true for most small objects. - * - * @param cell Value to test - * @return true if object is embedded, false otherwise - */ - public static boolean isEmbedded(ACell cell) { - if (cell == null) return true; - return cell.isEmbedded(); - } - - /** - * Gets the encoded Blob for an object in canonical message format - * - * @param o The object to encode - * @return Encoded data as a blob - */ - public static Blob encodedBlob(ACell o) { - if (o==null) return Blob.NULL_ENCODING; - return o.getEncoding(); - } - - /** - * Gets an new encoded ByteBuffer for an Cell in wire format - * - * @param cell The Cell to encode - * @return A ByteBuffer ready to read (i.e. already flipped) - */ - public static ByteBuffer encodedBuffer(ACell cell) { - // estimate size of bytebuffer required, 33 bytes big enough for most small - // stuff - int initialLength; - - if (cell==null) { - return ByteBuffer.wrap(new byte[] {0}).flip(); - } - - ABlob b = cell.cachedEncoding(); - if (b != null) return b.getByteBuffer(); - - initialLength = cell.estimatedEncodingSize(); - - ByteBuffer bb = ByteBuffer.allocate(initialLength); - boolean done = false; - while (!done) { - try { - bb = cell.write(bb); - done = true; - } catch (BufferOverflowException be) { - // retry with larger buffer - bb = ByteBuffer.allocate(bb.capacity() * 2 + 10); - } - } - bb.flip(); - return bb; - } - - /** - * Writes hex digits from digit position start, total length - * - * @param bb ByteBuffer to read from - * @param src Blob containing hex digits - * @param start Start position (in hex digits) - * @param length Length (in hex digits) - * @return ByteBuffer after writing - */ - static ByteBuffer writeHexDigits(ByteBuffer bb, ABlob src, long start, long length) { - bb = Format.writeVLCLong(bb, start); - bb = Format.writeVLCLong(bb, length); - int nBytes = Utils.checkedInt((length + 1) >> 1); - byte[] bs = new byte[nBytes]; - for (int i = 0; i < length; i++) { - Utils.setBits(bs, 4, 4 * ((nBytes * 2) - i - 1), src.getHexDigit(start + i)); - } - bb = bb.put(bs); - return bb; - } - - /** - * Writes hex digits from digit position start, total length. - * - * Fills final hex digit with 0 if length is odd. - * - * @param bs Byte array - * @param pos Position to write into byte array - * @param src Source Blob for hex digits - * @param start Start position in source blob (hex digit number from beginning) - * @param length Number of hex digits to write - * @return position after writing - */ - public static int writeHexDigits(byte[] bs, int pos, ABlob src, long start, long length) { - pos = Format.writeVLCLong(bs,pos, start); - pos = Format.writeVLCLong(bs,pos, length); - int nBytes = Utils.checkedInt((length + 1) >> 1); - byte[] bs2 = new byte[nBytes]; - for (int i = 0; i < nBytes; i++) { - long ix=start+i*2; - int d0=src.getHexDigit(ix); - int d1=((i*2+1)> 1); - byte[] bs = new byte[nBytes]; - bb.get(bs); - if (length < nBytes * 2) { - // test for invalid high bits missing if we have an odd number of digits - - // should be zero - if (Utils.extractBits(bs, 4, 0) != 0) - throw new BadFormatException("Bytes for " + length + " hex digits: " + Utils.toHexString(bs)); - } - - int rBytes = Utils.checkedInt((start + length + 1) >> 1); // bytes covering the specified range completely - byte[] rs = new byte[rBytes]; - - for (int i = 0; i < length; i++) { - int digit = Utils.extractBits(bs, 4, 4 * ((nBytes * 2) - i - 1)); - int di = Utils.checkedInt(4 * ((rBytes * 2) - (start + i) - 1)); - Utils.setBits(rs, 4, di, digit); - } - - return rs; - } - - /** - * Gets a hex String representing an object's encoding - * @param cell Any cell - * @return Hex String - */ - public static String encodedString(ACell cell) { - return encodedBlob(cell).toHexString(); - } - - /** - * Gets a hex String representing an object's encoding. Used in testing only. - * @param o Any object, will be cast to appropriate CVM type - * @return Hex String - */ - public static String encodedString(Object o) { - return encodedString(RT.cvm(o)); - } - - public static int estimateSize(ACell cell) { - if (cell==null) return 1; - return cell.estimatedEncodingSize(); - } - - static boolean canEncodeUFT8(CharSequence s) { - return UTF8_ENCODERS.get().canEncode(s); - } - -} diff --git a/convex-core/src/main/java/convex/core/data/Hash.java b/convex-core/src/main/java/convex/core/data/Hash.java deleted file mode 100644 index fa41e125b..000000000 --- a/convex-core/src/main/java/convex/core/data/Hash.java +++ /dev/null @@ -1,223 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; - -import convex.core.Constants; -import convex.core.crypto.Hashing; -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.InvalidDataException; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * Class used to represent an immutable 32-byte Hash value. - * - * The Hash algorithm used may depend on context. - * - * This is intended to help with type safety vs. regular Blob objects and as a - * useful type as a key in relevant data structures. - * - * "Companies spend millions of dollars on firewalls, encryption and secure - * access devices, and it's money wasted, because none of these measures address - * the weakest link in the security chain." - Kevin Mitnick - * - */ -public class Hash extends AArrayBlob { - /** - * Standard length of a Hash in bytes - */ - public static final int LENGTH = Constants.HASH_LENGTH; - - /** - * Type of Hash values - */ - public static final AType TYPE = Types.BLOB; - - private Hash(byte[] hashBytes, int offset) { - super(hashBytes, offset, LENGTH); - } - - private Hash(byte[] hashBytes) { - super(hashBytes, 0, LENGTH); - } - - /* - * Hash of some common constant values These are useful to have pre-calculated - * for efficiency - */ - public static final Hash NULL_HASH = Hashing.sha3(new byte[] { Tag.NULL }); - public static final Hash TRUE_HASH = Hashing.sha3(new byte[] { Tag.TRUE }); - public static final Hash FALSE_HASH = Hashing.sha3(new byte[] { Tag.FALSE }); - public static final Hash EMPTY_HASH = Hashing.sha3(new byte[0]); - - - /** - * Wraps the specified bytes as a Data object Warning: underlying bytes are used - * directly. Use only if no external references to the byte array will be - * retained. - * - * @param hashBytes Bytes to wrap - * @return Hash wrapping the given byte array - */ - public static Hash wrap(byte[] hashBytes) { - return new Hash(hashBytes); - } - - /** - * Wraps the specified blob data as a Hash, sharing the underlying byte array. - * @param data Blob data of correct size for a Hash. Must have at least enough bytes for a Hash - * @return Wrapped data as a Hash - */ - public static Hash wrap(AArrayBlob data) { - if (data instanceof Hash) return (Hash)data; - return wrap(data.getInternalArray(),data.getInternalOffset()); - } - - /** - * Wraps the specified bytes as a Data object Warning: underlying bytes are used - * directly. Use only if no external references to the byte array will be - * retained. - * - * @param hashBytes Byte array containing hash value - * @param offset Offset into byte array for start of hash value - * @return Hash wrapping the given byte array segment - */ - public static Hash wrap(byte[] hashBytes, int offset) { - if ((offset < 0) || (offset + LENGTH > hashBytes.length)) - throw new IllegalArgumentException(Errors.badRange(offset, LENGTH)); - return new Hash(hashBytes, offset); - } - - @Override - public boolean equals(ABlob other) { - if (other==null) return false; - if (other instanceof Hash) return equals((Hash)other); - if (other.count()!=LENGTH) return false; - if (other.getType()!=TYPE) return false; - return other.equalsBytes(this.store, this.offset); - } - - /** - * Tests if the Hash value is precisely equal to another non-null Hash value. - * - * @param other Hash to comapre with - * @return true if Hashes are equal, false otherwise. - */ - public boolean equals(Hash other) { - if (other == this) return true; - return Utils.arrayEquals(other.store, other.offset, this.store, this.offset, LENGTH); - } - - /** - * Get the first 32 bits of this Hash. Used for Java hashCodes - * @return Int representing the first 32 bits - */ - public int firstInt() { - return Utils.readInt(this.store, this.offset); - } - - /** - * Constructs a Hash object from a hex string - * - * @param hexString Hex String - * @return Hash with the given hex string value, or null is String is not valid - */ - public static Hash fromHex(String hexString) { - byte [] bs=Utils.hexToBytes(hexString); - if (bs==null) return null; - if (bs.length!=LENGTH) return null; - return wrap(bs); - } - - public static Hash wrap(AArrayBlob data, int offset, int length) { - return wrap(data.store, data.offset + offset); - } - - /** - * Computes the Hash for any ACell value. - * - * May return a cached Hash if available in memory. - * - * @param value Any Cell - * @return Hash of the encoded data for the given value - */ - public static Hash compute(ACell value) { - if (value == null) return NULL_HASH; - return value.getHash(); - } - - /** - * Reads a Hash from a ByteBuffer Assumes no Tag, i.e. just Hash.LENGTH for the - * hash is read. - * - * @param bb ByteBuffer to read from - * @return Hash object read from ByteBuffer - */ - public static Hash readRaw(ByteBuffer bb) { - byte[] bs = new byte[Hash.LENGTH]; - bb.get(bs); - return Hash.wrap(bs); - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.BLOB; - bs[pos++]=LENGTH; - return encodeRaw(bs,pos); - } - - @Override - public boolean isCanonical() { - // always canonical, since class invariants are maintained - return false; - } - - @Override public final boolean isCVMValue() { - return true; - } - - @Override - public int estimatedEncodingSize() { - // tag plus raw data - return 1 + LENGTH; - } - - @Override - public long getEncodingLength() { - // Always a fixed encoding length, tag plus count plus length - return 2 + LENGTH; - } - - @Override - public Blob getChunk(long i) { - if (i != 0) throw new IndexOutOfBoundsException(Errors.badIndex(i)); - return toBlob(); - } - - @Override - public void validateCell() throws InvalidDataException { - if (length != LENGTH) throw new InvalidDataException("Address length must be 32 bytes = 256 bits", this); - } - - @Override - public boolean isEmbedded() { - // Hashes are always small enough to embed - return true; - } - - @Override - public boolean isRegularBlob() { - return true; - } - - @Override - public byte getTag() { - return Tag.BLOB; - } - - @Override - public Blob toCanonical() { - return toBlob(); - } -} diff --git a/convex-core/src/main/java/convex/core/data/IAssociative.java b/convex-core/src/main/java/convex/core/data/IAssociative.java deleted file mode 100644 index e47afae4e..000000000 --- a/convex-core/src/main/java/convex/core/data/IAssociative.java +++ /dev/null @@ -1,12 +0,0 @@ -package convex.core.data; - -/** - * Interface for associative data structures - * - * @param Type of associative keys - * @param Type of associative values - */ -public interface IAssociative { - - -} diff --git a/convex-core/src/main/java/convex/core/data/INumeric.java b/convex-core/src/main/java/convex/core/data/INumeric.java deleted file mode 100644 index 52b31978a..000000000 --- a/convex-core/src/main/java/convex/core/data/INumeric.java +++ /dev/null @@ -1,33 +0,0 @@ -package convex.core.data; - -import convex.core.data.prim.APrimitive; -import convex.core.data.prim.CVMDouble; -import convex.core.data.prim.CVMLong; - -/** - * Interface for CVM Numeric types - */ -public interface INumeric { - - - public CVMLong toLong(); - - public CVMDouble toDouble(); - - public double doubleValue(); - - /** - * Gets the numeric type that should be used as for calculations - * @return Double.class or Long.class, or null if not a numeric type - */ - public Class numericType(); - - /** - * Gets the signum of this numerical value. Will be -1, 0 or 1 for Longs, -1.0, 0.0 , 1.0 or ##NaN for doubles. - * @return Signum of the numeric value - */ - public APrimitive signum(); - - public INumeric toStandardNumber(); - -} diff --git a/convex-core/src/main/java/convex/core/data/IRefFunction.java b/convex-core/src/main/java/convex/core/data/IRefFunction.java deleted file mode 100644 index 83b85446c..000000000 --- a/convex-core/src/main/java/convex/core/data/IRefFunction.java +++ /dev/null @@ -1,16 +0,0 @@ -package convex.core.data; - -/** - * Functional interface for operations on Cell Refs that may throw a - * MissingDataException - * - * In general, IRefFunction is used to provide a visitor for data objects containing nested Refs. - */ -@FunctionalInterface -public interface IRefFunction { - - // Note we can't have a generic type parameter in a functional interface. - // So using a wildcard seems the best option? - - public Ref apply(Ref t); -} diff --git a/convex-core/src/main/java/convex/core/data/IValidated.java b/convex-core/src/main/java/convex/core/data/IValidated.java deleted file mode 100644 index 4b53f0297..000000000 --- a/convex-core/src/main/java/convex/core/data/IValidated.java +++ /dev/null @@ -1,26 +0,0 @@ -package convex.core.data; - -import convex.core.exceptions.InvalidDataException; - -/** - * Interface for classes that can be validated - */ -public interface IValidated { - - /** - * Validates the complete structure of this object. - * - * It is necessary to ensure all child Refs are validated, so the general contract for validate is: - * - *
    - *
  1. Call super.validate() - which will indirectly call validateCell()
  2. - *
  3. Call validate() on any contained cells in this class
  4. - *
- * - * @throws InvalidDataException If the data Valie is invalid in any way - */ - public void validate() throws InvalidDataException; - - - -} diff --git a/convex-core/src/main/java/convex/core/data/IWriteable.java b/convex-core/src/main/java/convex/core/data/IWriteable.java deleted file mode 100644 index 7ec54521c..000000000 --- a/convex-core/src/main/java/convex/core/data/IWriteable.java +++ /dev/null @@ -1,31 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; - -public interface IWriteable { - /** - * Writes this object to a byte array including an appropriate message tag - * - * @param bs byte array to write this object to - * @param pos position at which to write the value - * @return The updated position - */ - public int encode(byte[] bs, int pos); - - /** - * Writes this object to a ByteBuffer including an appropriate message tag - * - * @param bb ByteBuffer to write to - * @return The updated ByteBuffer - */ - public ByteBuffer write(ByteBuffer bb); - - /** - * Estimate the encoded data size for this Cell. Used for quickly sizing buffers. - * Implementations should try to return a size that is likely to contain the entire object - * when represented in binary format, including the tag byte. - * - * @return The estimated size for the binary representation of this object. - */ - public abstract int estimatedEncodingSize(); -} diff --git a/convex-core/src/main/java/convex/core/data/Keyword.java b/convex-core/src/main/java/convex/core/data/Keyword.java deleted file mode 100644 index daff8d0b6..000000000 --- a/convex-core/src/main/java/convex/core/data/Keyword.java +++ /dev/null @@ -1,150 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; - -import convex.core.Constants; -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; - -/** - * Keyword data type. Intended as human-readable map keys, tags and option - * specifiers etc. - * - * Keywords evaluate to themselves, and as such can be considered as literal - * constants. - * - * "Programs must be written for people to read, and only incidentally for - * machines to execute." ― Harold Abelson - */ -public class Keyword extends ASymbolic implements Comparable { - - /** Maximum size of a Keyword in UTF-16 chars representation */ - public static final int MAX_CHARS = Constants.MAX_NAME_LENGTH; - - /** Minimum size of a Keyword in UTF-16 chars representation */ - public static final int MIN_CHARS = 1; - - private Keyword(String name) { - super(name); - } - - public AType getType() { - return Types.KEYWORD; - } - - /** - * Creates a Keyword with the given name - * - * @param name A String to use as the keyword name - * @return The new Keyword, or null if the name is invalid for a Keyword - */ - public static Keyword create(String name) { - if (!validateName(name)) { - return null; - } - return new Keyword(name); - } - - public static Keyword create(AString name) { - if (name==null) return null; - return create(name.toString()); - } - - /** - * Creates a Keyword with the given name, throwing an exception if name is not - * valid - * - * @param aString A String of at least 1 and no more than 64 UTF-8 bytes in length - * @return The new Keyword - */ - public static Keyword createChecked(AString aString) { - Keyword k = create(aString); - if (k == null) throw new IllegalArgumentException("Invalid keyword name: " + aString); - return k; - } - - public static Keyword createChecked(String aString) { - Keyword k = create(aString); - if (k == null) throw new IllegalArgumentException("Invalid keyword name: " + aString); - return k; - } - - - @Override - public boolean isCanonical() { - return true; - } - - - /** - * Reads a Keyword from the given ByteBuffer, assuming tag already consumed - * - * @param bb ByteBuffer source - * @return The Keyword read - * @throws BadFormatException If a Keyword could not be read correctly - */ - public static Keyword read(ByteBuffer bb) throws BadFormatException { - String name=Format.readUTF8String(bb); - Keyword kw = Keyword.create(name); - if (kw == null) throw new BadFormatException("Can't read symbol"); - return kw; - - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.KEYWORD; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - return Format.writeRawUTF8String(bs, pos, name); - } - - @Override - public void print(StringBuilder sb) { - sb.append(':'); - sb.append(name); - } - - @Override - public int estimatedEncodingSize() { - return name.length()*2+3; - } - - @Override - public boolean equals(ACell other) { - if (other == this) return true; - if (!(other instanceof Keyword)) return false; - return name.equals(((Keyword) other).name); - } - - @Override - public int compareTo(Keyword k) { - return name.compareTo(k.name); - } - - @Override - public void validateCell() throws InvalidDataException { - super.validateCell(); - } - - @Override - public int getRefCount() { - return 0; - } - - @Override - public byte getTag() { - return Tag.KEYWORD; - } - - @Override - public ACell toCanonical() { - return this; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/Keywords.java b/convex-core/src/main/java/convex/core/data/Keywords.java deleted file mode 100644 index d20c68ec2..000000000 --- a/convex-core/src/main/java/convex/core/data/Keywords.java +++ /dev/null @@ -1,89 +0,0 @@ -package convex.core.data; - -/** - * Static Keyword values for configuration maps, records etc. - */ -public class Keywords { - - public static final Keyword STATE = Keyword.create("state"); - public static final Keyword KEYPAIR = Keyword.create("keypair"); - public static final Keyword PORT = Keyword.create("port"); - public static final Keyword ORDERS = Keyword.create("orders"); - public static final Keyword TRANSACTIONS = Keyword.create("transactions"); - public static final Keyword TIMESTAMP = Keyword.create("timestamp"); - public static final Keyword ACCOUNTS = Keyword.create("accounts"); - public static final Keyword PEERS = Keyword.create("peers"); - public static final Keyword BELIEF = Keyword.create("belief"); - public static final Keyword STATES = Keyword.create("states"); - public static final Keyword RESULTS = Keyword.create("results"); - public static final Keyword PERSIST = Keyword.create("persist"); - - - public static final Keyword STORE = Keyword.create("store"); - public static final Keyword RESTORE = Keyword.create("restore"); - - // for testing and suchlike - public static final Keyword FOO = Keyword.create("foo"); - public static final Keyword BAR = Keyword.create("bar"); - public static final Keyword BAZ = Keyword.create("baz"); - - - public static final Keyword SALT = Keyword.create("salt"); - public static final Keyword IV = Keyword.create("iv"); - public static final Keyword ROUNDS = Keyword.create("rounds"); - public static final Keyword CIPHERTEXT = Keyword.create("ciphertext"); - public static final Keyword GLOBALS = Keyword.create("globals"); - public static final Keyword SCHEDULE = Keyword.create("schedule"); - - public static final Keyword NAME = Keyword.create("name"); - - // source info - public static final Keyword START = Keyword.create("start"); - public static final Keyword END = Keyword.create("end"); - public static final Keyword SOURCE = Keyword.create("source"); - public static final Keyword TAG = Keyword.create("tag"); - public static final Keyword DOC = Keyword.create("doc"); - public static final Keyword DESCRIPTION = Keyword.create("description"); - public static final Keyword EXAMPLES = Keyword.create("examples"); - public static final Keyword CODE = Keyword.create("code"); - - public static final Keyword TYPE = Keyword.create("type"); - public static final Keyword SPECIAL_Q = Keyword.create("special?"); - - public static final Keyword PEER = Keyword.create("peer"); - public static final Keyword STAKE = Keyword.create("stake"); - public static final Keyword STAKES = Keyword.create("stakes"); - public static final Keyword DELEGATED_STAKE = Keyword.create("delegated-stake"); - public static final Keyword OWNER = Keyword.create("owner"); - - public static final Keyword HOST = Keyword.create("host"); - public static final Keyword URL = Keyword.create("url"); - - public static final Keyword SEQUENCE = Keyword.create("sequence"); - public static final Keyword BALANCE = Keyword.create("balance"); - public static final Keyword ENVIRONMENT = Keyword.create("environment"); - public static final Keyword HOLDINGS = Keyword.create("holdings"); - public static final Keyword ALLOWANCE = Keyword.create("allowance"); - public static final Keyword CONTROLLER = Keyword.create("controller"); - public static final Keyword KEY = Keyword.create("key"); - - public static final Keyword ID = Keyword.create("id"); - public static final Keyword RESULT = Keyword.create("result"); - public static final Keyword ERROR_CODE = Keyword.create("error-code"); - public static final Keyword TRACE = Keyword.create("trace"); - - public static final Keyword EXPANDER_Q = Keyword.create("expander?"); - public static final Keyword MACRO = Keyword.create("macro"); - - public static final Keyword CALLABLE_Q = Keyword.create("callable?"); - - public static final Keyword VALUE = Keyword.create("value"); - public static final Keyword FUNCTION = Keyword.create("function"); - public static final Keyword METADATA = Keyword.create("metadata"); - - - public static final Keyword OUTGOING_CONNECTIONS = Keyword.create("outgoing-connections"); - public static final Keyword AUTO_MANAGE = Keyword.create("auto-manage"); - public static final Keyword TIMEOUT = Keyword.create("timeout"); - public static final Keyword EVENT_HOOK = Keyword.create("event-hook"); -} diff --git a/convex-core/src/main/java/convex/core/data/List.java b/convex-core/src/main/java/convex/core/data/List.java deleted file mode 100644 index 8576e2232..000000000 --- a/convex-core/src/main/java/convex/core/data/List.java +++ /dev/null @@ -1,425 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.util.Iterator; -import java.util.ListIterator; -import java.util.function.Consumer; -import java.util.function.Function; - -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * Implementation of a list wrapping a vector. - * - * Note that we embed the vector directly, avoiding going via a Ref. This is - * important for serialisation efficiency / avoiding excess Cells. - * - * "One can even conjecture that Lisp owes its survival specifically to the fact - * that its programs are lists, which everyone, including me, has regarded as a - * disadvantage." - John McCarthy, "Early History of Lisp" - * - * @param Type of List elements - */ -public class List extends AList { - - public static final List EMPTY = wrap(VectorLeaf.EMPTY); - - public static final Ref> EMPTY_REF = EMPTY.getRef(); - - static { - // Set empty Ref flags as internal embedded constant - EMPTY_REF.setFlags(Ref.INTERNAL_FLAGS); - } - - /** - * Wrapped vector containing reversed elements - */ - private AVector data; - - private List(AVector data) { - super(data.count); - this.data = data.toVector(); // ensure canonical, not a mapentry etc. - } - - /** - * Wraps a Vector as a list (will reverse element order) - * @param Type of elements - * @param vector Vector to wrap - * @return New List instance - */ - public static List wrap(AVector vector) { - return new List(vector); - } - - /** - * Creates a List containing the elements of the provided vector in reverse - * order - * - * @param Type of list elements - * @param vector Vector to reverse into a List - * @return Vector representing this list in reverse order - */ - @SuppressWarnings("unchecked") - public static List reverse(AVector vector) { - if (vector.isEmpty()) return (List) Lists.empty(); - return new List(vector); - } - - @SuppressWarnings("unchecked") - public static List of(Object... elements) { - if (elements.length == 0) return (List) Lists.empty(); - Utils.reverse(elements); - return new List(Vectors.of(elements)); - } - - /*** - * Creates a list wrapping the given array. May destructively alter the array - * @param Type of element - * @param args Elements to include - * @return New List - */ - @SuppressWarnings("unchecked") - public static List create(ACell... args) { - if (args.length==0) return (List) Lists.empty(); - Utils.reverse(args); - return new List(Vectors.create(args)); - } - - @Override - public Object[] toArray() { - Object[] arr = data.toArray(); - Utils.reverse(arr, size()); - return arr; - } - - @Override - public V[] toArray(V[] a) { - V[] arr = data.toArray(a); - Utils.reverse(arr, size()); - return arr; - } - - @Override - public T get(long index) { - return data.get(count - 1 - index); - } - - @Override - public Ref getElementRef(long i) { - return data.getElementRef(count - 1 - i); - } - - @SuppressWarnings("unchecked") - @Override - public AList assoc(long i, R value) { - AVector newData; - newData = data.assoc(count - 1 - i, value); - if (data == newData) return (AList) this; - if (newData==null) return null; - return new List<>(newData); - } - - @Override - public int indexOf(Object o) { - int pos = data.lastIndexOf(o); - if (pos < 0) return -1; - return size() - 1 - pos; - } - - @Override - public int lastIndexOf(Object o) { - int pos = data.indexOf(o); - if (pos < 0) return -1; - return size() - 1 - pos; - } - - @Override - public long longIndexOf(Object o) { - long pos = data.longLastIndexOf(o); - if (pos < 0) return -1; - return count - 1 - pos; - } - - @Override - public long longLastIndexOf(Object o) { - long pos = data.longIndexOf(o); - if (pos < 0) return -1; - return count - 1 - pos; - } - - @Override - public ListIterator listIterator() { - return new MyListIterator(0); - } - - @Override - public ListIterator listIterator(int index) { - return new MyListIterator(index); - } - - @Override - public ListIterator listIterator(long index) { - return new MyListIterator(index); - } - - private class MyListIterator implements ListIterator { - - private final ListIterator dataIterator; - - public MyListIterator(long pos) { - this.dataIterator = data.listIterator(count - pos); - } - - @Override - public boolean hasNext() { - return dataIterator.hasPrevious(); - } - - @Override - public T next() { - return dataIterator.previous(); - } - - @Override - public boolean hasPrevious() { - return dataIterator.hasNext(); - } - - @Override - public T previous() { - return dataIterator.next(); - } - - @Override - public int nextIndex() { - return Utils.checkedInt(count - 1 - dataIterator.previousIndex()); - } - - @Override - public int previousIndex() { - return Utils.checkedInt(count - 1 - dataIterator.nextIndex()); - } - - @Override - public void remove() { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public void set(T e) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public void add(T e) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - } - - @Override - public int getRefCount() { - return data.getRefCount(); - } - - @Override - public Ref getRef(int i) { - return data.getRef(i); - } - - @Override - public List updateRefs(IRefFunction func) { - AVector newData = (AVector) data.updateRefs(func); - if (newData == data) return this; - return new List(newData); - } - - @Override - public boolean isCanonical() { - return true; - } - - @Override public final boolean isCVMValue() { - return data.isCVMValue(); - } - - @Override - public void print(StringBuilder sb) { - sb.append('('); - long n = count; - for (long i = 0; i < n; i++) { - if (i > 0) sb.append(' '); - Utils.print(sb,data.get(n - 1 - i)); - } - sb.append(')'); - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.LIST; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = data.encodeRaw(bs,pos); - return pos; - } - - /** - * Reads a List from the specified bytebuffer. Assumes Tag byte already consumed. - * @param bb ByteBuffer to read from - * @return List instance read from ByteBuffer - * @throws BadFormatException If Encoding is invalid - * - */ - public static List read(ByteBuffer bb) throws BadFormatException { - try { - AVector data = Vectors.read(bb); - if (data == null) throw new BadFormatException("Expected vector but got null in List format"); - return new List(data); - } catch (ClassCastException e) { - throw new BadFormatException("Expected vector in List format", e); - } - } - - @Override - public Iterator iterator() { - return listIterator(); - } - - @Override - public int estimatedEncodingSize() { - return data.estimatedEncodingSize(); - } - - /** - * Prepends an element to the list in first position. - * - * @param value Value to prepend - * @return Updated list - */ - @Override - public List conj(R value) { - return new List((AVector) data.conj(value)); - } - - @Override - public AList cons(T x) { - return new List((AVector) data.conj(x)); - } - - public List conjAll(ACollection xs) { - return reverse(data.conjAll(xs)); - } - - - @SuppressWarnings("unchecked") - @Override - public AVector toVector() { - return (AVector) Vectors.create(toCellArray()); - } - - @Override - public ASequence next() { - if (count <= 1) return null; - return slice(1, count - 1); - } - - @Override - public ASequence slice(long start, long length) { - long end = start + length; - if ((start == 0) && (end == count)) return this; - return reverse(data.slice(count - end, length)); - } - - @Override - public AList map(Function mapper) { - // TODO: reverse map order? - return new List<>(data.map(mapper)); - } - - @SuppressWarnings("unchecked") - @Override - public List concat(ASequence vals) { - AVector rvals; - if (vals instanceof List) { - rvals = ((List) vals).data; - } else { - rvals = Vectors.empty(); - long n = vals.count(); - for (long i = 0; i < n; i++) { - rvals = rvals.conj(vals.get(n - i - 1)); - } - } - return List.reverse(rvals.concat((AVector)data)); - } - - @Override - public void visitElementRefs(Consumer> f) { - data.visitElementRefs(f); - } - - @SuppressWarnings("unchecked") - @Override - public AVector subVector(long start, long length) { - checkRange(start, length); - - // Create using an Object array. Probably fastest? - ACell[] arr = new ACell[Utils.checkedInt(length)]; - for (int i = 0; i < length; i++) { - arr[i] = get(start + i); - } - return (AVector) Vectors.create(arr); - } - - @Override - public void validateCell() throws InvalidDataException { - long dc = data.count(); - if (count != dc) - throw new InvalidDataException("Bad data count " + count + " with underlying data count " + dc, this); - data.validateCell(); - } - - @Override - public void forEach(Consumer action) { - data.forEach(action); - } - - @Override - public AList drop(long n) { - if (n==0) return this; - long newLen=count-n; - if (newLen<0) return null; - if (newLen==0) return Lists.empty(); - return new List(data.subVector(0, newLen)); - } - - @Override - public byte getTag() { - return Tag.LIST; - } - - - @Override - public AVector reverse() { - return data; - } - - @SuppressWarnings("unchecked") - @Override - protected void copyToArray(R[] arr, int offset) { - int n=Utils.checkedInt(count); - for (int i=0; i AList empty() { - return (List) List.EMPTY; - } - - @SuppressWarnings("unchecked") - public static > L create(java.util.List list) { - return (L) List.of(list.toArray()); - } - - @SafeVarargs - public static AList of(Object... vals) { - return List.of(vals); - } -} diff --git a/convex-core/src/main/java/convex/core/data/LongBlob.java b/convex-core/src/main/java/convex/core/data/LongBlob.java deleted file mode 100644 index 9ab930f4b..000000000 --- a/convex-core/src/main/java/convex/core/data/LongBlob.java +++ /dev/null @@ -1,146 +0,0 @@ -package convex.core.data; - -import java.security.MessageDigest; - -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * Wrapper for an 8-byte long blob - * - * We use this for efficient management of indexes using longs in BlobMaps. - * - */ -public final class LongBlob extends ALongBlob { - - private LongBlob(long value) { - super(value); - } - - public static LongBlob create(String string) { - byte[] bs = Utils.hexToBytes(string); - if (bs.length != LENGTH) throw new IllegalArgumentException("Long blob requires a length 8 hex string"); - return new LongBlob(Utils.readLong(bs, 0)); - } - - public static LongBlob create(long value) { - return new LongBlob(value); - } - - - @Override public final boolean isCVMValue() { - return true; - } - - @Override - public void getBytes(byte[] dest, int destOffset) { - Utils.writeLong(dest, destOffset,value); - } - - @Override - public ABlob slice(long start, long length) { - if ((start == 0) && (length == LENGTH)) return this; - - if (start < 0) throw new IndexOutOfBoundsException(Errors.badRange(start, length)); - return getEncoding().slice(start + 2, length); - } - - @Override - public Blob toBlob() { - return getEncoding().slice(2, LENGTH); - } - - @Override - protected void updateDigest(MessageDigest digest) { - byte[] bs = getEncoding().getInternalArray(); - digest.update(bs, 2, (int) LENGTH); - } - - @Override - public int getHexDigit(long i) { - if ((i < 0) || (i >= LENGTH * 2)) throw new IndexOutOfBoundsException(Errors.badIndex(i)); - return 0x0F & (int) (value >> ((LENGTH * 2 - i - 1) * 4)); - } - - @Override - public long commonHexPrefixLength(ABlob b) { - if (b == this) return LENGTH * 2; - - long max = Math.min(LENGTH, b.count()); - for (long i = 0; i < max; i++) { - byte ai = getUnchecked(i); - byte bi = b.getUnchecked(i); - if (ai != bi) return (i * 2) + (Utils.firstDigitMatch(ai, bi) ? 1 : 0); - } - return max * 2; - } - - @Override - public boolean equals(ABlob a) { - if (a instanceof LongBlob) return (((LongBlob) a).value == value); - if (a instanceof Blob) { - Blob b=(Blob)a; - return ((b.count()==LENGTH)&& (b.longValue()== value)); - } - return false; - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.BLOB; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - bs[pos++]=((byte) 8); - Utils.writeLong(bs, pos, value); - return pos+8; - } - - @Override - public int estimatedEncodingSize() { - return (int) (2 + LENGTH); - } - - @Override - public long longValue() { - return value; - } - - @Override - public long hexMatchLength(ABlob b, long start, long length) { - if (b == this) return length; - long end = start + length; - for (long i = start; i < end; i++) { - if (!(getHexDigit(i) == b.getHexDigit(i))) return i - start; - } - return length; - } - - @Override - public boolean isRegularBlob() { - return true; - } - - @Override - public byte getTag() { - return Tag.BLOB; - } - - @Override - public boolean equalsBytes(byte[] bytes, int byteOffset) { - return value==Utils.readLong(bytes, byteOffset); - } - - @Override - public boolean isCanonical() { - return false; - } - - @Override - public Blob toCanonical() { - return toBlob(); - } - -} diff --git a/convex-core/src/main/java/convex/core/data/MapEntry.java b/convex-core/src/main/java/convex/core/data/MapEntry.java deleted file mode 100644 index ea5b77dd4..000000000 --- a/convex-core/src/main/java/convex/core/data/MapEntry.java +++ /dev/null @@ -1,345 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; - -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.RT; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * Map.Entry implementation for persistent maps. - * - * Contains exactly 2 Refs, one for key and one for value - * - * Implements Comparable using the hash value of keys. - * - * @param The type of keys - * @param The type of values - */ -public class MapEntry extends AMapEntry implements Comparable> { - - private final Ref keyRef; - private final Ref valueRef; - - private MapEntry(Ref key, Ref value) { - super(2); - this.keyRef = key; - this.valueRef = value; - } - - @Override - public AType getType() { - return Types.VECTOR; - } - - @SuppressWarnings("unchecked") - public static MapEntry createRef(Ref keyRef, Ref valueRef) { - // ensure we have a hash at least - return new MapEntry((Ref) keyRef, (Ref) valueRef); - } - - /** - * Creates a new MapEntry with the provided key and value - * @param Type of Key - * @param Type of value - * @param key Key to use for MapEntry - * @param value Value to use for MapEntry - * @return New MapEntry instance - */ - public static MapEntry create(K key, V value) { - return createRef(Ref.get(key), Ref.get(value)); - } - - /** - * Create a map entry, converting key and value to correct CVM types. - * @param Type of Keys - * @param Type of Values - * @param key Key to use for map entry - * @param value Value to use for map entry - * @return New MapEntry - */ - public static MapEntry of(Object key, Object value) { - return create(RT.cvm(key),RT.cvm(value)); - } - - @Override - public MapEntry withValue(V value) { - if (value == getValue()) return this; - return new MapEntry(keyRef, Ref.get(value)); - } - - @SuppressWarnings("unchecked") - @Override - public AVector assoc(long i, R a) { - if (i == 0) return (AVector) withKey((K) a); - if (i == 1) return (AVector) withValue((V) a); - return null; - } - - @Override - protected MapEntry withKey(K key) { - if (key == getKey()) return this; - return new MapEntry(Ref.get(key), valueRef); - } - - @Override - public K getKey() { - return keyRef.getValue(); - } - - @Override - public AVector map(Function mapper) { - return Vectors.of(mapper.apply(getKey()), mapper.apply(getValue())); - } - - @Override - public R reduce(BiFunction func, R value) { - R result = func.apply(value, getKey()); - result = func.apply(result, getKey()); - return result; - } - - @SuppressWarnings("unchecked") - @Override - protected void copyToArray(R[] arr, int offset) { - arr[offset] = (R) getKey(); - arr[offset + 1] = (R) getValue(); - } - - /** - * Gets the hash of the key for this MapEntry - * - * @return the Hash of the Key - */ - public Hash getKeyHash() { - return getKeyRef().getHash(); - } - - @Override - public V getValue() { - return valueRef.getValue(); - } - - public Ref getKeyRef() { - return keyRef; - } - - public Ref getValueRef() { - return valueRef; - } - - /** - * Reads a MapEntry from a ByteBuffer. Assumes Tag already handled. - * @param bb ByteBuffer to read from - * @return MapEntry instance - * @throws BadFormatException If encoding is invalid - */ - public static MapEntry read(ByteBuffer bb) throws BadFormatException { - Ref kr = Format.readRef(bb); - Ref vr = Format.readRef(bb); - return new MapEntry(kr, vr); - } - - /** - * Reads a MapEntry or null from a ByteBuffer. Assumes no Tag. - * @param bb ByteBuffer to read from - * @return MapEntry instance, or null - * @throws BadFormatException If encoding is invalid - */ - public static MapEntry readCompressed(ByteBuffer bb) throws BadFormatException { - byte b=bb.get(); - if (b==Tag.NULL) return null; - if (b!=Tag.VECTOR) throw new BadFormatException("Bad header byte for compressed MapEntry: "+Utils.toHexString(b)); - Ref kr = Format.readRef(bb); - Ref vr = Format.readRef(bb); - return new MapEntry(kr, vr); - } - - @Override - public int compareTo(MapEntry o) { - if (this == o) return 0; - return keyRef.compareTo(o.keyRef); - } - - @Override - public int getRefCount() { - return 2; - } - - @SuppressWarnings("unchecked") - @Override - public Ref getRef(int i) { - if ((i >> 1) != 0) throw new IndexOutOfBoundsException(i); - return (Ref) (((i & 1) == 0) ? keyRef : valueRef); - } - - @SuppressWarnings("unchecked") - @Override - public MapEntry updateRefs(IRefFunction func) { - Ref newKeyRef = (Ref) func.apply(keyRef); - Ref newValueRef = (Ref) func.apply(valueRef); - if ((keyRef == newKeyRef) && (valueRef == newValueRef)) return this; - return new MapEntry(newKeyRef, newValueRef); - } - - @Override - public boolean equals(ACell o) { - if (o==null) return false; - if (o.getTag()!=Tag.VECTOR) return false; - AVector v=(AVector) o; - if (v.count()!=2) return false; - return getEncoding().equals(o.getEncoding()); - } - - public boolean equals(MapEntry b) { - if (this == b) return true; - return keyRef.equals(b.keyRef) && valueRef.equals(b.valueRef); - } - - /** - * Checks if the keys of two map entries are equal - * - * @param b MapEntry to compare with this MapEntry - * @return true if this entry's key equals that of the other entry, false - * otherwise. - */ - public boolean keyEquals(MapEntry b) { - return keyRef.equals(b.keyRef); - } - - @Override - @SuppressWarnings("unchecked") - public AVector toVector() { - return new VectorLeaf(new Ref[] { keyRef, valueRef }); - } - - @Override - public boolean contains(Object o) { - return (Utils.equals(o, getKey()) || Utils.equals(o, getValue())); - } - - @Override - public ACell get(long i) { - if (i == 0) return getKey(); - if (i == 1) return getValue(); - throw new IndexOutOfBoundsException(Errors.badIndex(i)); - } - - @SuppressWarnings("unchecked") - @Override - public Ref getElementRef(long i) { - if (i == 0) return (Ref) keyRef; - if (i == 1) return (Ref) valueRef; - throw new IndexOutOfBoundsException(Errors.badIndex(i)); - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.VECTOR; - pos = Format.writeVLCLong(bs,pos, 2); // Size of 2, to match VectorLeaf encoding - return encodeRaw(bs,pos); - } - - /** - * Writes the raw MapEntry content. Puts the key and value Refs onto the given - * ByteBuffer - * - * @param bs Byte array to write to - * @return Updated position after writing - */ - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = keyRef.encode(bs,pos); - pos = valueRef.encode(bs,pos); - return pos; - } - - /** - * Writes a MapEntry or null content in compressed format (no count). Useful for - * embedding an optional MapEntry inside a larger Encoding - * - * @param me MapEntry to encode - * @param bs Byte array to write to - * @param pos Starting position for encoding in byte array - * @return Updated position after writing - */ - public static int encodeCompressed(MapEntry me,byte[] bs, int pos) { - if (me==null) { - bs[pos++]=Tag.NULL; - } else { - bs[pos++]=Tag.VECTOR; - pos = me.encodeRaw(bs,pos); - } - return pos; - } - - - @Override public final boolean isCVMValue() { - return true; - } - - @Override - public int estimatedEncodingSize() { - return 1+Format.MAX_EMBEDDED_LENGTH*2; // header plus two embedded objects - } - - @SuppressWarnings("unchecked") - @Override - public void visitElementRefs(Consumer> f) { - f.accept((Ref) keyRef); - f.accept((Ref) valueRef); - } - - @Override - public AVector concat(ASequence b) { - return toVector().concat(b); - } - - @Override - public AVector subVector(long start, long length) { - AVector vec=toVector(); - return vec.subVector(start, length); - } - - @Override - public void validate() throws InvalidDataException { - super.validate(); - keyRef.validate(); - valueRef.validate(); - } - - - @Override - public void validateCell() throws InvalidDataException { - // TODO: is there really Nothing to do? - } - - @Override - public byte getTag() { - return Tag.VECTOR; - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - public static MapEntry convertOrNull(AVector v) { - if (v.count()!=2) return null; - return createRef(v.getElementRef(0),v.getElementRef(1)); - } - - @Override - public boolean isCanonical() { - return false; - } - - @Override - public ACell toCanonical() { - // Vector is the canonical form of a MapEntry - return toVector(); - } -} diff --git a/convex-core/src/main/java/convex/core/data/MapLeaf.java b/convex-core/src/main/java/convex/core/data/MapLeaf.java deleted file mode 100644 index ef8bdd5b4..000000000 --- a/convex-core/src/main/java/convex/core/data/MapLeaf.java +++ /dev/null @@ -1,757 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Function; - -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.exceptions.TODOException; -import convex.core.util.MergeFunction; -import convex.core.util.Utils; - -/** - * Limited size Persistent Merkle Map implemented as a small sorted list of - * Key/Value pairs - * - * Must be sorted by Key hash value to ensure uniqueness of representation - * - * @param Type of keys - * @param Type of values - */ -public class MapLeaf extends AHashMap { - /** - * Maximum number of entries in a MapLeaf - */ - public static final int MAX_ENTRIES = 8; - - static final MapEntry[] EMPTY_ENTRIES = new MapEntry[0]; - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private static final MapLeaf EMPTY = new MapLeaf(EMPTY_ENTRIES); - - private final MapEntry[] entries; - - private MapLeaf(MapEntry[] items) { - super(items.length); - entries = items; - } - - /** - * Creates a ListMap with the specified entries. Entries must have distinct keys - * but may otherwise be specified in any order. - * - * Null entries are ignored/removed. - * - * @param entries Entries for map - * @return New ListMap - */ - public static MapLeaf create(MapEntry[] entries) { - return create(entries, 0, entries.length); - } - - /** - * Creates a ListMap with the specified entries. Null entries are - * ignored/removed. - * - * @param entries - * @param offset Offset into entries array - * @param length Number of entries to take from entries array, starting at - * offset - * @return A new ListMap - */ - protected static MapLeaf create(MapEntry[] entries, int offset, int length) { - if (length == 0) return emptyMap(); - if (length > MAX_ENTRIES) throw new IllegalArgumentException("Too many entries: " + entries.length); - MapEntry[] sorted = Utils.copyOfRangeExcludeNulls(entries, offset, offset + length); - if (sorted.length == 0) return emptyMap(); - Arrays.sort(sorted); - return new MapLeaf(sorted); - } - - @SuppressWarnings("unchecked") - public static MapLeaf create(MapEntry item) { - return new MapLeaf((MapEntry[]) new MapEntry[] { item }); - } - - @SuppressWarnings("unchecked") - @Override - public boolean containsKey(ACell key) { - return getEntry((K) key) != null; - } - - @Override - public MapEntry getEntry(ACell k) { - // if we have an ACell instance, use (or compute) the cached hash. Probably - // faster. - if (k instanceof ACell) return getEntryByHash(((ACell) k).getHash()); - - int len = size(); - for (int i = 0; i < len; i++) { - MapEntry e = entries[i]; - if (Utils.equals(k, e.getKey())) return e; - } - return null; - } - - @Override - public MapEntry getKeyRefEntry(Ref ref) { - int len = size(); - for (int i = 0; i < len; i++) { - MapEntry e = entries[i]; - if (ref.equals(e.getKeyRef())) return e; - } - return null; - } - - @Override - protected MapEntry getEntryByHash(Hash hash) { - int len = size(); - for (int i = 0; i < len; i++) { - MapEntry e = entries[i]; - if (hash.equals(e.getKeyHash())) return e; - } - return null; - } - - @Override - public boolean containsValue(Object value) { - int len = size(); - for (int i = 0; i < len; i++) { - if (Utils.equals(value, entries[i].getValue())) return true; - } - return false; - } - - @SuppressWarnings("unchecked") - @Override - public V get(ACell key) { - MapEntry me = getEntry((K) key); - return (me == null) ? null : me.getValue(); - } - - /** - * Gets the index of key k in the internal array, or -1 if not found - * - * @param key - * @return - */ - private int seek(K key) { - int len = size(); - for (int i = 0; i < len; i++) { - if (Utils.equals(key, entries[i].getKey())) return i; - } - return -1; - } - - private int seekKeyRef(Ref key) { - int len = size(); - for (int i = 0; i < len; i++) { - if (Utils.equals(key, entries[i].getKeyRef())) return i; - } - return -1; - } - - @SuppressWarnings("unchecked") - @Override - public MapLeaf dissoc(ACell key) { - int i = seek((K)key); - if (i < 0) return this; // not found - return dissocEntry(i); - } - - @Override - public MapLeaf dissocRef(Ref key) { - int i = seekKeyRef(key); - if (i < 0) return this; // not found - return dissocEntry(i); - } - - @SuppressWarnings("unchecked") - private MapLeaf dissocEntry(int internalIndex) { - int len = size(); - if (len == 1) return emptyMap(); - MapEntry[] newEntries = (MapEntry[]) new MapEntry[len - 1]; - System.arraycopy(entries, 0, newEntries, 0, internalIndex); - System.arraycopy(entries, internalIndex + 1, newEntries, internalIndex, len - internalIndex - 1); - return new MapLeaf(newEntries); - } - - @Override - public AHashMap assocEntry(MapEntry e) { - return assocEntry(e, 0); - } - - @Override - public AHashMap assocEntry(MapEntry e, int shift) { - int len = size(); - - // first check for update with existing key - for (int i = 0; i < len; i++) { - MapEntry me = entries[i]; - if (e.equals(me)) return this; - if (me.getKeyRef().equals(e.getKeyRef())) { - // replace current entry - MapEntry[] newEntries = entries.clone(); - newEntries[i] = e; - return new MapLeaf(newEntries); - } - } - - // need to extend array, use new shift if promoting to TreeMap - int newLen = len + 1; - @SuppressWarnings("unchecked") - MapEntry[] newEntries = (MapEntry[]) new MapEntry[newLen]; - System.arraycopy(entries, 0, newEntries, 0, len); - newEntries[newLen - 1] = e; - if (newLen <= MAX_ENTRIES) { - // new size implies a ListMap - Arrays.sort(newEntries); - return new MapLeaf(newEntries); - } else { - // new size implies a TreeMap with the current given shift - return MapTree.create(newEntries, shift); - } - } - - @SuppressWarnings("unchecked") - @Override - public AHashMap assoc(ACell key, ACell value) { - return assoc((K)key, (V) value, 0); - } - - protected AHashMap assoc(K key, V value, int shift) { - int len = size(); - - // first check for update with existing key - for (int i = 0; i < len; i++) { - MapEntry me = entries[i]; - if (Utils.equals(me.getKey(), key)) { - if (Utils.equals(me.getValue(), value)) return this; - MapEntry newEntry = me.withValue(value); - if (me == newEntry) return this; - - // need to clone and update array - MapEntry[] newEntries = entries.clone(); - newEntries[i] = newEntry; - return new MapLeaf(newEntries); - } - } - - // Key not found, so need to extend array - @SuppressWarnings("unchecked") - MapEntry[] newEntries = (MapEntry[]) new MapEntry[len + 1]; - System.arraycopy(entries, 0, newEntries, 0, len); - newEntries[len] = MapEntry.create(key, value); - if (len + 1 <= MAX_ENTRIES) { - // new size should be a ListMap - Arrays.sort(newEntries); - return new MapLeaf(newEntries); - } else { - // new Size should be a TreeMap with current shift - return MapTree.create(newEntries, shift); - } - } - - @Override - protected AHashMap assocRef(Ref keyRef, V value, int shift) { - return assoc(keyRef.getValue(), value, shift); - } - - @Override - public AHashMap assocRef(Ref keyRef, V value) { - return assocRef(keyRef, value, 0); - } - - @Override - public Set keySet() { - int len = size(); - HashSet h = new HashSet(len); - ; - for (int i = 0; i < len; i++) { - MapEntry me = entries[i]; - h.add(me.getKey()); - } - return h; - } - - @Override - protected void accumulateKeySet(HashSet h) { - for (int i = 0; i < entries.length; i++) { - MapEntry me = entries[i]; - h.add(me.getKey()); - } - } - - @Override - protected void accumulateValues(ArrayList al) { - for (int i = 0; i < entries.length; i++) { - MapEntry me = entries[i]; - al.add(me.getValue()); - } - } - - @Override - public Set> entrySet() { - int len = size(); - HashSet> h = new HashSet>(len); - ; - accumulateEntrySet(h); - return h; - } - - @Override - protected void accumulateEntrySet(HashSet> h) { - for (int i = 0; i < entries.length; i++) { - MapEntry me = entries[i]; - h.add(me); - } - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.MAP; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = Format.writeVLCLong(bs,pos, count); - - for (int i = 0; i < count; i++) { - pos = entries[i].encodeRaw(bs,pos); - } - return pos; - } - - @Override - public int estimatedEncodingSize() { - // allow space for header, size byte, 2 refs per entry - return 2 + 2* Format.MAX_EMBEDDED_LENGTH * size(); - } - - public static int MAX_ENCODING_LENGTH= 2 + 2 * MAX_ENTRIES * Format.MAX_EMBEDDED_LENGTH; - - /** - * Reads a MapLeaf from the provided ByteBuffer Assumes the header byte is - * already read. - * - * @param bb ByteBuffer to read from - * @param count Count of map elements - * @return A Map as deserialised from the provided ByteBuffer - * @throws BadFormatException If encoding is invalid - */ - @SuppressWarnings("unchecked") - public static MapLeaf read(ByteBuffer bb, long count) throws BadFormatException { - if (count == 0) return (MapLeaf) EMPTY; - if (count < 0) throw new BadFormatException("Negative count of map elements!"); - if (count > MAX_ENTRIES) throw new BadFormatException("MapLeaf too big: " + count); - - MapEntry[] items = (MapEntry[]) new MapEntry[(int) count]; - for (int i = 0; i < count; i++) { - items[i] = MapEntry.read(bb); - } - - if (!isValidOrder(items)) { - throw new BadFormatException("Bad ordering of keys!"); - } - - return new MapLeaf(items); - } - - - @SuppressWarnings("unchecked") - public static MapLeaf emptyMap() { - return (MapLeaf) EMPTY; - } - - @Override - public void forEach(BiConsumer action) { - for (MapEntry e : entries) { - action.accept(e.getKey(), e.getValue()); - } - } - - @Override - public boolean isCanonical() { - return true; - } - - @Override public final boolean isCVMValue() { - return true; - } - - private static boolean isValidOrder(MapEntry[] entries) { - long count = entries.length; - for (int i = 0; i < count - 1; i++) { - Hash a = entries[i].getKeyHash(); - Hash b = entries[i + 1].getKeyHash(); - if (a.compareTo(b) >= 0) { - return false; - } - } - return true; - } - - @Override - public int getRefCount() { - return 2 * entries.length; - } - - @SuppressWarnings("unchecked") - @Override - public Ref getRef(int i) { - MapEntry e = entries[i >> 1]; // IndexOutOfBoundsException if out of range - if ((i & 1) == 0) { - return (Ref) e.getKeyRef(); - } else { - return (Ref) e.getValueRef(); - } - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public MapLeaf updateRefs(IRefFunction func) { - int n = entries.length; - if (n == 0) return this; - MapEntry[] newEntries = entries; - for (int i = 0; i < n; i++) { - MapEntry e = newEntries[i]; - MapEntry newEntry = e.updateRefs(func); - if (e!=newEntry) { - if (newEntries==entries) newEntries=entries.clone(); - newEntries[i]=newEntry; - } - } - if (newEntries==entries) return this; - // Note: we assume no key hashes have changed - return new MapLeaf(newEntries); - } - - /** - * Filters this ListMap to contain only key hashes with the hex digits specified - * in the given Mask - * - * @param digitPos Position of the hex digit to filter - * @param mask Mask of digits to include - * @return Filtered ListMap - */ - public MapLeaf filterHexDigits(int digitPos, int mask) { - mask = mask & 0xFFFF; - if (mask == 0) return emptyMap(); - if (mask == 0xFFFF) return this; - int sel = 0; - int n = size(); - for (int i = 0; i < n; i++) { - Hash h = entries[i].getKeyHash(); - if ((mask & (1 << h.getHexDigit(digitPos))) != 0) { - sel = sel | (1 << i); // include this index in selection - } - } - if (sel == 0) return emptyMap(); // no entries selected - return filterEntries(sel); - } - - /** - * Filters entries using the given bit mask - * - * @param selection - * @return - */ - @SuppressWarnings("unchecked") - private MapLeaf filterEntries(int selection) { - if (selection == 0) return emptyMap(); // no items selected - int n = size(); - if (selection == ((1 << n) - 1)) return this; // all items selected - MapEntry[] newEntries = new MapEntry[Integer.bitCount(selection)]; - int ix = 0; - for (int i = 0; i < n; i++) { - if ((selection & (1 << i)) != 0) { - newEntries[ix++] = entries[i]; - } - } - assert (ix == Integer.bitCount(selection)); - return new MapLeaf(newEntries); - } - - @Override - public MapEntry entryAt(long i) { - return entries[Utils.checkedInt(i)]; - } - - @Override - public AHashMap mergeWith(AHashMap b, MergeFunction func) { - return mergeWith(b, func, 0); - } - - @Override - protected AHashMap mergeWith(AHashMap b, MergeFunction func, int shift) { - if (b instanceof MapLeaf) return mergeWith((MapLeaf) b, func, shift); - if (b instanceof MapTree) return ((MapTree) b).mergeWith(this, func.reverse()); - throw new TODOException("Unhandled map type: " + b.getClass()); - } - - private AHashMap mergeWith(MapLeaf b, MergeFunction func, int shift) { - int al = this.size(); - int bl = b.size(); - int ai = 0; - int bi = 0; - // Complexity to manage: - // 1. Must step through two ListMaps in order, comparing for key hashes - // 2. nulls can be produced to remove entries - // 3. We use the creation of a results ArrayList to signal a change from - // original value - ArrayList> results = null; - while ((ai < al) || (bi < bl)) { - MapEntry ae = (ai < al) ? this.entries[ai] : null; - MapEntry be = (bi < bl) ? b.entries[bi] : null; - - // comparison - int c = (ae == null) ? 1 : ((be == null) ? -1 : ae.getKeyHash().compareTo(be.getKeyHash())); - - // new entry - MapEntry newE = null; - if (c < 0) { - V r = func.merge(ae.getValue(), null); - if (r != null) newE = ae.withValue(r); - } else if (c > 0) { - V r = func.merge(null, be.getValue()); - if (r != null) newE = be.withValue(r); - } else { - // we have matched keys - V r = func.merge(ae.getValue(), be.getValue()); - if (r != null) newE = ae.withValue(r); - } - if ((results == null) && (newE != ((c <= 0) ? ae : null))) { - // create new results array if difference detected - results = new ArrayList<>(16); - for (int i = 0; i < ai; i++) { // copy previous values in this map, up to ai - results.add(entries[i]); - } - } - if (c <= 0) ai++; // inc ai if we used ae - if (c >= 0) bi++; // inc bi if we used be - if ((results != null) && (newE != null)) results.add(newE); - } - if (results == null) return this; // no change detected - return Maps.createWithShift(shift, results); - } - - @Override - public AHashMap mergeDifferences(AHashMap b, MergeFunction func) { - return mergeDifferences(b,func,0); - } - - @Override - protected AHashMap mergeDifferences(AHashMap b, MergeFunction func, int shift) { - if (b instanceof MapLeaf) return mergeDifferences((MapLeaf) b, func,shift); - if (b instanceof MapTree) return b.mergeWith(this, func.reverse()); - throw new TODOException("Unhandled map type: " + b.getClass()); - } - - public AHashMap mergeDifferences(MapLeaf b, MergeFunction func,int shift) { - if (this.equals(b)) return this; // no change in identical case - int al = this.size(); - int bl = b.size(); - int ai = 0; - int bi = 0; - ArrayList> results = null; - while ((ai < al) || (bi < bl)) { - MapEntry ae = (ai < al) ? this.entries[ai] : null; - MapEntry be = (bi < bl) ? b.entries[bi] : null; - int c = (ae == null) ? 1 : ((be == null) ? -1 : ae.getKeyHash().compareTo(be.getKeyHash())); - MapEntry newE = null; - if (c < 0) { - // lowest key in this map only - V r = func.merge(ae.getValue(), null); - if (r != null) newE = ae.withValue(r); - } else if (c > 0) { - // lowest key in other map b only - V r = func.merge(null, be.getValue()); - if (r != null) newE = be.withValue(r); - } else { - // keys are equal (i.e. value in both maps) - V av = ae.getValue(); - V bv = be.getValue(); - V r = (Utils.equals(av, bv)) ? av : func.merge(ae.getValue(), be.getValue()); - if (r != null) newE = ae.withValue(r); - } - if ((results == null) && (newE != ((c <= 0) ? ae : null))) { - // create new results array if difference detected - results = new ArrayList<>(16); - for (int i = 0; i < ai; i++) { - results.add(entries[i]); - } - } - if (c <= 0) ai++; // inc ai if we used ae - if (c >= 0) bi++; // inc bi if we used be - if ((results != null) && (newE != null)) results.add(newE); - } - if (results == null) return this; - return Maps.createWithShift(shift,results); - } - - @Override - public R reduceValues(BiFunction func, R initial) { - int n = size(); - R result = initial; - for (int i = 0; i < n; i++) { - result = func.apply(result, entries[i].getValue()); - } - return result; - } - - @Override - public R reduceEntries(BiFunction, ? extends R> func, R initial) { - int n = size(); - R result = initial; - for (int i = 0; i < n; i++) { - result = func.apply(result, entries[i]); - } - return result; - } - - @Override - public boolean equalsKeys(AMap a) { - if (a instanceof MapLeaf) return equalsKeys((MapLeaf) a); - // different map type cannot possibly be equal - return false; - } - - /** - * Returns true if this map has all keys equal to the other Map - * - * @param a A map to compare keys with - * @return Boolean true if the two maps have the same keys - */ - public boolean equalsKeys(MapLeaf a) { - if (this == a) return true; - int n = this.size(); - if (n != a.size()) return false; - for (int i = 0; i < n; i++) { - if (!this.entries[i].keyEquals(a.entries[i])) return false; - } - return true; - } - - @Override - public boolean equals(AMap a) { - if (!(a instanceof MapLeaf)) return false; - return equals((MapLeaf) a); - } - - public boolean equals(MapLeaf a) { - if (this == a) return true; - int n = size(); - if (n != a.size()) return false; - for (int i = 0; i < n; i++) { - if (!entries[i].equals(a.entries[i])) return false; - } - return true; - } - - @Override - public MapLeaf mapEntries(Function, MapEntry> func) { - MapEntry[] newEntries = entries; - for (int i = 0; i < entries.length; i++) { - MapEntry e = entries[i]; - MapEntry newE = func.apply(e); - if (e != newE) { - if ((newE != null) && (!(e.keyEquals(newE)))) - throw new IllegalArgumentException("Function changed Key: " + e.getKey()); - if (newEntries == entries) { - newEntries = entries.clone(); - } - newEntries[i] = newE; - } - } - if (newEntries == entries) return this; - return create(newEntries); - } - - @Override - protected void validateWithPrefix(String prefix) throws InvalidDataException { - validate(); - for (int i = 0; i < entries.length; i++) { - MapEntry e = entries[i]; - Hash h = e.getKeyRef().getHash(); - if (!h.toHexString().startsWith(prefix)) { - throw new InvalidDataException("Prefix " + prefix + " invalid for map entry: " + e + " with hash: " + h, - this); - } - e.validate(); - } - } - - @Override - public void validateCell() throws InvalidDataException { - if ((count == 0) && (this != EMPTY)) { - throw new InvalidDataException("Empty map not using canonical instance", this); - } - - if (count > MAX_ENTRIES) { - throw new InvalidDataException("Too many items in list map: " + entries.length, this); - } - - // validates both key uniqueness and sort order - if (!isValidOrder(entries)) { - throw new InvalidDataException("Invalid key ordering", this); - } - } - - @Override - public boolean containsAllKeys(AHashMap b) { - if (this==b) return true; - - // if map is too big, can't possibly contain all keys - if (b.count()>count) return false; - - // must be a mapleaf if this size or smaller - return containsAllKeys((MapLeaf)b); - } - - protected boolean containsAllKeys(MapLeaf b) { - int ix=0; - for (MapEntry meb:b.entries) { - Hash bh=meb.getKeyHash(); - - if (ix>=count) return false; // no remaining entries in this - while (ix mea=entries[ix]; - Hash ah=mea.getKeyHash(); - int c=ah.compareTo(bh); - if (c<0) { - // ah is smaller than bh - // need to advance ix and try next entry - ix++; - if (ix>=count) return false; // not found - continue; - } else if (c>0) { - return false; // didn't contain the key entry - } else { - // found it, so advance to next entry in b and update ix - ix++; - break; - } - } - } - - return true; - } - - @Override - public byte getTag() { - return Tag.MAP; - } - - @Override - public ACell toCanonical() { - return this; - } -} diff --git a/convex-core/src/main/java/convex/core/data/MapTree.java b/convex-core/src/main/java/convex/core/data/MapTree.java deleted file mode 100644 index 44cdb926a..000000000 --- a/convex-core/src/main/java/convex/core/data/MapTree.java +++ /dev/null @@ -1,880 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Function; - -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.exceptions.TODOException; -import convex.core.util.Bits; -import convex.core.util.MergeFunction; -import convex.core.util.Utils; - -/** - * Persistent Map for large hash maps requiring tree structure. - * - * Internally implemented as a radix tree, indexed by key hash. Uses an array of - * child Maps, with a bitmap mask indicating which hex digits are present, i.e. - * have non-empty children. - * - * @param Type of map keys - * @param Type of map values - */ -public class MapTree extends AHashMap { - /** - * Child maps, one for each present bit in the mask, max 16 - */ - private final Ref>[] children; - - /** - * Shift position of this treemap node in number of hex digits - */ - private final int shift; - - /** - * Mask indicating which hex digits are present in the child array e.g. 0x0001 - * indicates all children are in the '0' digit. e.g. 0xFFFF indicates there are - * children for every digit. - */ - private final short mask; - - private MapTree(Ref>[] blocks, int shift, short mask, long count) { - super(count); - this.children = blocks; - this.shift = shift; - this.mask = mask; - } - - /** - * Computes the total count from an array of Refs to maps Ignores null Refs in - * child array - * - * @param children - * @return The total count of all child maps - */ - private static long computeCount(Ref>[] children) { - long n = 0; - for (Ref> cref : children) { - if (cref == null) continue; - AMap m = cref.getValue(); - n += m.count(); - } - return n; - } - - @SuppressWarnings("unchecked") - public static MapTree create(MapEntry[] newEntries, int shift) { - int n = newEntries.length; - if (n <= MapLeaf.MAX_ENTRIES) { - throw new IllegalArgumentException( - "Insufficient distinct entries for TreeMap construction: " + newEntries.length); - } - - // construct full child array - Ref>[] children = new Ref[16]; - for (int i = 0; i < n; i++) { - MapEntry e = newEntries[i]; - int ix = e.getKeyHash().getHexDigit(shift); - Ref> ref = children[ix]; - if (ref == null) { - children[ix] = MapLeaf.create(e).getRef(); - } else { - AHashMap newChild=ref.getValue().assocEntry(e, shift + 1); - children[ix] = newChild.getRef(); - } - } - return (MapTree) createFull(children, shift); - } - - /** - * Creates a Tree map given child refs for each digit - * - * @param children An array of children, may refer to nulls or empty maps which - * will be filtered out - * @return - */ - private static AHashMap createFull(Ref>[] children, int shift, long count) { - if (children.length != 16) throw new IllegalArgumentException("16 children required!"); - Ref>[] newChildren = Utils.filterArray(children, a -> { - if (a == null) return false; - AMap m = a.getValue(); - return ((m != null) && !m.isEmpty()); - }); - - if (children != newChildren) { - return create(newChildren, shift, Utils.computeMask(children, newChildren), count); - } else { - return create(children, shift, (short) 0xFFFF, count); - } - } - - /** - * Create a MapTree with a full compliment of children. - * @param - * @param - * @param newChildren - * @param shift - * @return - */ - private static AHashMap createFull(Ref>[] newChildren, int shift) { - return createFull(newChildren, shift, computeCount(newChildren)); - } - - /** - * Creates a Map with the specified child map Refs. Removes empty maps passed as - * children. - * - * Returns a ListMap for small maps. - * - * @param children Array of Refs to child maps for each bit in mask - * @param shift Shift position (hex digit of key hashes for this map) - * @param mask Mask specifying the hex digits included in the child array at - * this shift position - * @return A new map as specified @ - */ - @SuppressWarnings("unchecked") - private static AHashMap create(Ref>[] children, int shift, short mask, long count) { - int cLen = children.length; - if (Integer.bitCount(mask & 0xFFFF) != cLen) { - throw new IllegalArgumentException( - "Invalid child array length " + cLen + " for bit mask " + Utils.toHexString(mask)); - } - - // compress small counts to ListMap - if (count <= MapLeaf.MAX_ENTRIES) { - MapEntry[] entries = new MapEntry[Utils.checkedInt(count)]; - int ix = 0; - for (Ref> childRef : children) { - AMap child = childRef.getValue(); - long cc = child.count(); - for (long i = 0; i < cc; i++) { - entries[ix++] = child.entryAt(i); - } - } - assert (ix == count); - return MapLeaf.create(entries); - } - int sel = (1 << cLen) - 1; - short newMask = mask; - for (int i = 0; i < cLen; i++) { - AMap child = children[i].getValue(); - if (child.isEmpty()) { - newMask = (short) (newMask & ~(1 << digitForIndex(i, mask))); // remove from mask - sel = sel & ~(1 << i); // remove from selection - } - } - if (mask != newMask) { - return new MapTree(Utils.filterSmallArray(children, sel), shift, newMask, count); - } - return new MapTree(children, shift, mask, count); - } - - @Override - public boolean containsKey(ACell key) { - return containsKeyRef(Ref.get(key)); - } - - @Override - public MapEntry getEntry(ACell k) { - return getKeyRefEntry(Ref.get(k)); - } - - @Override - public MapEntry getKeyRefEntry(Ref ref) { - int digit = Utils.extractDigit(ref.getHash(), shift); - int i = Bits.indexForDigit(digit, mask); - if (i < 0) return null; // -1 case indicates not found - return children[i].getValue().getKeyRefEntry(ref); - } - - @Override - public boolean containsValue(Object value) { - for (Ref> b : children) { - if (b.getValue().containsValue(value)) return true; - } - return false; - } - - @Override - public V get(ACell key) { - MapEntry me = getKeyRefEntry(Ref.get(key)); - if (me == null) return null; - return me.getValue(); - } - - @Override - public MapEntry entryAt(long i) { - long pos = i; - for (Ref> c : children) { - AHashMap child = c.getValue(); - long cc = child.count(); - if (pos < cc) return child.entryAt(pos); - pos -= cc; - } - throw new IndexOutOfBoundsException("Entry index: " + i); - } - - @Override - protected MapEntry getEntryByHash(Hash hash) { - int digit = Utils.extractDigit(hash, shift); - int i = Bits.indexForDigit(digit, mask); - if (i < 0) return null; // not present - return children[i].getValue().getEntryByHash(hash); - } - - @SuppressWarnings("unchecked") - @Override - public AHashMap dissoc(ACell key) { - return dissocRef((Ref) Ref.get(key)); - } - - @Override - @SuppressWarnings("unchecked") - public AHashMap dissocRef(Ref keyRef) { - int digit = Utils.extractDigit(keyRef.getHash(), shift); - int i = Bits.indexForDigit(digit, mask); - if (i < 0) return this; // not present - - // dissoc entry from child - AHashMap child = children[i].getValue(); - AHashMap newChild = child.dissocRef(keyRef); - if (child == newChild) return this; // no removal, no change - - if (count - 1 == MapLeaf.MAX_ENTRIES) { - // reduce to a ListMap - HashSet> eset = entrySet(); - boolean removed = eset.removeIf(e -> Utils.equals(((MapEntry) e).getKeyRef(), keyRef)); - if (!removed) throw new Error("Expected to remove at least one entry!"); - return MapLeaf.create(eset.toArray((MapEntry[]) MapLeaf.EMPTY_ENTRIES)); - } else { - // replace child - if (newChild.isEmpty()) return dissocChild(i); - return replaceChild(i, newChild.getRef()); - } - } - - @SuppressWarnings("unchecked") - private AHashMap dissocChild(int i) { - int bsize = children.length; - AHashMap child = children[i].getValue(); - Ref>[] newBlocks = (Ref>[]) new Ref[bsize - 1]; - System.arraycopy(children, 0, newBlocks, 0, i); - System.arraycopy(children, i + 1, newBlocks, i, bsize - i - 1); - short newMask = (short) (mask & (~(1 << digitForIndex(i, mask)))); - long newCount = count - child.count(); - return create(newBlocks, shift, newMask, newCount); - } - - @SuppressWarnings("unchecked") - private MapTree insertChild(int digit, Ref> newChild) { - int bsize = children.length; - int i = Bits.positionForDigit(digit, mask); - short newMask = (short) (mask | (1 << digit)); - if (mask == newMask) throw new Error("Digit already present!"); - - Ref>[] newChildren = (Ref>[]) new Ref[bsize + 1]; - System.arraycopy(children, 0, newChildren, 0, i); - System.arraycopy(children, i, newChildren, i + 1, bsize - i); - newChildren[i] = newChild; - long newCount = count + newChild.getValue().count(); - return (MapTree) create(newChildren, shift, newMask, newCount); - } - - /** - * Replaces the child ref at a given index position. Will return the same - * TreeMap if no change - * - * @param i - * @param newChild - * @return @ - */ - private MapTree replaceChild(int i, Ref> newChild) { - if (children[i] == newChild) return this; - AHashMap oldChild = children[i].getValue(); - Ref>[] newChildren = children.clone(); - newChildren[i] = newChild; - long newCount = count + newChild.getValue().count() - oldChild.count(); - return (MapTree) create(newChildren, shift, mask, newCount); - } - - public static int digitForIndex(int index, short mask) { - // scan mask for specified index - int found = 0; - for (int i = 0; i < 16; i++) { - if ((mask & (1 << i)) != 0) { - if (found++ == index) return i; - } - } - throw new IllegalArgumentException("Index " + index + " not available in mask map: " + Utils.toHexString(mask)); - } - - @SuppressWarnings("unchecked") - @Override - public MapTree assoc(ACell key, ACell value) { - K k= (K)key; - Ref keyRef = Ref.get(k); - return assocRef(keyRef, (V) value, shift); - } - - @Override - public MapTree assocRef(Ref keyRef, V value) { - return assocRef(keyRef, value, shift); - } - - @Override - protected MapTree assocRef(Ref keyRef, V value, int shift) { - if (this.shift != shift) { - throw new Error("Invalid shift!"); - } - int digit = Utils.extractDigit(keyRef.getHash(), shift); - int i = Bits.indexForDigit(digit, mask); - if (i < 0) { - // location not present, need to insert new child - AHashMap newChild = MapLeaf.create(MapEntry.createRef(keyRef, Ref.get(value))); - return insertChild(digit, newChild.getRef()); - } else { - // child exists, so assoc in new ref at lower shift level - AHashMap child = children[i].getValue(); - AHashMap newChild = child.assocRef(keyRef, value, shift + 1); - return replaceChild(i, newChild.getRef()); - } - } - - @Override - public AHashMap assocEntry(MapEntry e) { - assert (this.shift == 0); // should never call this on a different shift - return assocEntry(e, 0); - } - - @Override - public MapTree assocEntry(MapEntry e, int shift) { - assert (this.shift == shift); // should always be correct shift - Ref keyRef = e.getKeyRef(); - int digit = Utils.extractDigit(keyRef.getHash(), shift); - int i = Bits.indexForDigit(digit, mask); - if (i < 0) { - // location not present - AHashMap newChild = MapLeaf.create(e); - return insertChild(digit, newChild.getRef()); - } else { - // location needs update - AHashMap child = children[i].getValue(); - AHashMap newChild = child.assocEntry(e, shift + 1); - if (child == newChild) return this; - return replaceChild(i, newChild.getRef()); - } - } - - @Override - public Set keySet() { - int len = size(); - HashSet h = new HashSet(len); - accumulateKeySet(h); - return h; - } - - @Override - protected void accumulateKeySet(HashSet h) { - for (Ref> mr : children) { - mr.getValue().accumulateKeySet(h); - } - } - - @Override - protected void accumulateValues(ArrayList al) { - for (Ref> mr : children) { - mr.getValue().accumulateValues(al); - } - } - - @Override - public HashSet> entrySet() { - int len = size(); - HashSet> h = new HashSet>(len); - accumulateEntrySet(h); - return h; - } - - @Override - protected void accumulateEntrySet(HashSet> h) { - for (Ref> mr : children) { - mr.getValue().accumulateEntrySet(h); - } - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.MAP; - return encodeRaw(bs,pos); - } - - - @Override - public int encodeRaw(byte[] bs, int pos) { - int ilength = children.length; - pos = Format.writeVLCLong(bs,pos, count); - - bs[pos++] = (byte) shift; - pos = Utils.writeShort(bs, pos,mask); - - for (int i = 0; i < ilength; i++) { - pos = children[i].encode(bs,pos); - } - return pos; - } - - @Override - public int estimatedEncodingSize() { - // allow space for tag, shift byte byte, 2 byte mask, embedded child refs - return 4 + Format.MAX_EMBEDDED_LENGTH * children.length; - } - - public static int MAX_ENCODING_LENGTH = 4 + Format.MAX_EMBEDDED_LENGTH * 16; - - /** - * Reads a ListMap from the provided ByteBuffer Assumes the header byte and count is - * already read. - * - * @param bb ByteBuffer to read from - * @param count Count of map entries - * @return TreeMap instance as read from ByteBuffer - * @throws BadFormatException If encoding is invalid - */ - @SuppressWarnings("unchecked") - public static MapTree read(ByteBuffer bb, long count) throws BadFormatException { - int shift = bb.get(); - short mask = bb.getShort(); - - int ilength = Integer.bitCount(mask & 0xFFFF); - Ref>[] blocks = (Ref>[]) new Ref[ilength]; - - for (int i = 0; i < ilength; i++) { - // need to read as a Ref - Ref> ref = Format.readRef(bb); - blocks[i] = ref; - } - // create directly, we have all values - MapTree result = new MapTree(blocks, shift, mask, count); - if (!result.isValidStructure()) throw new BadFormatException("Problem with TreeMap invariants"); - return result; - } - - @Override - public void forEach(BiConsumer action) { - for (Ref> sub : children) { - sub.getValue().forEach(action); - } - } - - @Override - public boolean isCanonical() { - if (count <= MapLeaf.MAX_ENTRIES) return false; - return true; - } - - @Override public final boolean isCVMValue() { - return shift==0; - } - - @Override - public int getRefCount() { - return children.length; - } - - @SuppressWarnings("unchecked") - @Override - public Ref getRef(int i) { - return (Ref) children[i]; - } - - @SuppressWarnings("unchecked") - @Override - public MapTree updateRefs(IRefFunction func) { - int n = children.length; - if (n == 0) return this; - Ref>[] newChildren = children; - for (int i = 0; i < n; i++) { - Ref> child = children[i]; - Ref> newChild = (Ref>) func.apply(child); - if (child != newChild) { - if (children == newChildren) { - newChildren = children.clone(); - } - newChildren[i] = newChild; - } - } - if (newChildren == children) return this; - // Note: we assume no key hashes have changed, so structure is the same - return new MapTree<>(newChildren, shift, mask, count); - } - - @Override - public AHashMap mergeWith(AHashMap b, MergeFunction func) { - return mergeWith(b, func, this.shift); - } - - @Override - protected AHashMap mergeWith(AHashMap b, MergeFunction func, int shift) { - if ((b instanceof MapTree)) { - MapTree bt = (MapTree) b; - if (this.shift != bt.shift) throw new Error("Misaligned shifts!"); - return mergeWith(bt, func, shift); - } - if ((b instanceof MapLeaf)) return mergeWith((MapLeaf) b, func, shift); - throw new Error("Unrecognised map type: " + b.getClass()); - } - - @SuppressWarnings("unchecked") - private AHashMap mergeWith(MapTree b, MergeFunction func, int shift) { - // assume two TreeMaps with identical prefix and shift - assert (b.shift == shift); - int fullMask = mask | b.mask; - // We are going to build full child list only if needed - Ref>[] newChildren = null; - for (int digit = 0; digit < 16; digit++) { - int bitMask = 1 << digit; - if ((fullMask & bitMask) == 0) continue; // nothing to merge at this index - AHashMap ac = childForDigit(digit).getValue(); - AHashMap bc = b.childForDigit(digit).getValue(); - AHashMap rc = ac.mergeWith(bc, func, shift + 1); - if (ac != rc) { - if (newChildren == null) { - newChildren = (Ref>[]) new Ref[16]; - for (int ii = 0; ii < digit; ii++) { // copy existing children up to this point - int chi = Bits.indexForDigit(ii, mask); - if (chi >= 0) newChildren[ii] = children[chi]; - } - } - } - if (newChildren != null) newChildren[digit] = rc.getRef(); - } - if (newChildren == null) return this; - return createFull(newChildren, shift); - } - - @SuppressWarnings("unchecked") - private AHashMap mergeWith(MapLeaf b, MergeFunction func, int shift) { - Ref>[] newChildren = null; - int ix = 0; - for (int i = 0; i < 16; i++) { - int imask = (1 << i); // mask for this digit - if ((mask & imask) == 0) continue; - Ref> cref = children[ix++]; - AHashMap child = cref.getValue(); - MapLeaf bSubset = b.filterHexDigits(shift, imask); // filter only relevant elements in b - AHashMap newChild = child.mergeWith(bSubset, func, shift + 1); - if (child != newChild) { - if (newChildren == null) { - newChildren = (Ref>[]) new Ref[16]; - for (int ii = 0; ii < children.length; ii++) { // copy existing children - int chi = digitForIndex(ii, mask); - newChildren[chi] = children[ii]; - } - } - } - if (newChildren != null) { - newChildren[i] = newChild.getRef(); - } - } - assert (ix == children.length); - // if any new children created, create a new Map, else use this - AHashMap result = (newChildren == null) ? this : createFull(newChildren, shift); - - MapLeaf extras = b.filterHexDigits(shift, ~mask); - int en = extras.size(); - for (int i = 0; i < en; i++) { - MapEntry e = extras.entryAt(i); - V value = func.merge(null, e.getValue()); - if (value != null) { - // include only new keys where function result is not null. Re-use existing - // entry if possible. - result = result.assocEntry(e.withValue(value), shift); - } - } - return result; - } - - @Override - public AHashMap mergeDifferences(AHashMap b, MergeFunction func) { - return mergeDifferences(b, func,0); - } - - @Override - protected AHashMap mergeDifferences(AHashMap b, MergeFunction func,int shift) { - if ((b instanceof MapTree)) { - MapTree bt = (MapTree) b; - // this is OK, top levels should both have shift 0 and be aligned down the tree. - if (this.shift != bt.shift) throw new Error("Misaligned shifts!"); - return mergeDifferences(bt, func,shift); - } else { - // must be ListMap - return mergeDifferences((MapLeaf) b, func,shift); - } - } - - @SuppressWarnings("unchecked") - private AHashMap mergeDifferences(MapTree b, MergeFunction func, int shift) { - // assume two treemaps with identical prefix and shift - if (this.equals(b)) return this; // no differences to merge - int fullMask = mask | b.mask; - Ref>[] newChildren = null; // going to build new full child list if needed - for (int i = 0; i < 16; i++) { - int bitMask = 1 << i; - if ((fullMask & bitMask) == 0) continue; // nothing to merge at this index - Ref> aref = childForDigit(i); - Ref> bref = b.childForDigit(i); - if (aref.equalsValue(bref)) continue; // identical children, no differences - AHashMap ac = aref.getValue(); - AHashMap bc = bref.getValue(); - AHashMap newChild = ac.mergeDifferences(bc, func,shift+1); - if (newChild != ac) { - if (newChildren == null) { - newChildren = (Ref>[]) new Ref[16]; - for (int ii = 0; ii < 16; ii++) { // copy existing children - int chi = Bits.indexForDigit(ii, mask); - if (chi >= 0) newChildren[ii] = children[chi]; - } - } - } - if (newChildren != null) newChildren[i] = (newChild == bc) ? bref : newChild.getRef(); - } - if (newChildren == null) return this; - return createFull(newChildren, shift); - } - - @SuppressWarnings("unchecked") - private AHashMap mergeDifferences(MapLeaf b, MergeFunction func, int shift) { - Ref>[] newChildren = null; - int ix = 0; - for (int i = 0; i < 16; i++) { - int imask = (1 << i); // mask for this digit - if ((mask & imask) == 0) continue; - Ref> cref = children[ix++]; - AHashMap child = cref.getValue(); - MapLeaf bSubset = b.filterHexDigits(shift, imask); // filter only relevant elements in b - AHashMap newChild = child.mergeDifferences(bSubset, func,shift+1); - if (child != newChild) { - if (newChildren == null) { - newChildren = (Ref>[]) new Ref[16]; - for (int ii = 0; ii < children.length; ii++) { // copy existing children - int chi = digitForIndex(ii, mask); - newChildren[chi] = children[ii]; - } - } - } - if (newChildren != null) newChildren[i] = newChild.getRef(); - } - assert (ix == children.length); - AHashMap result = (newChildren == null) ? this : createFull(newChildren, shift); - - MapLeaf extras = b.filterHexDigits(shift, ~mask); - int en = extras.size(); - for (int i = 0; i < en; i++) { - MapEntry e = extras.entryAt(i); - V value = func.merge(null, e.getValue()); - if (value != null) { - // include only new keys where function result is not null. Re-use existing - // entry if possible. - result = result.assocEntry(e.withValue(value), shift); - } - } - return result; - } - - /** - * Gets the Ref for the child at the given digit, or an empty map if not found - * - * @param digit The hex digit to query at this TreeMap's shift position - * @return The child map for this digit, or an empty map if the child does not - * exist - */ - private Ref> childForDigit(int digit) { - int ix = Bits.indexForDigit(digit, mask); - if (ix < 0) return Maps.emptyRef(); - return children[ix]; - } - - @Override - public R reduceValues(BiFunction func, R initial) { - int n = children.length; - R result = initial; - for (int i = 0; i < n; i++) { - result = children[i].getValue().reduceValues(func, result); - } - return result; - } - - @Override - public R reduceEntries(BiFunction, ? extends R> func, R initial) { - int n = children.length; - R result = initial; - for (int i = 0; i < n; i++) { - result = children[i].getValue().reduceEntries(func, result); - } - return result; - } - - @Override - public boolean equalsKeys(AMap a) { - if (a instanceof MapTree) return equalsKeys((MapTree) a); - // different map type cannot possibly be equal - return false; - } - - boolean equalsKeys(MapTree a) { - if (this == a) return true; - if (this.count != a.count) return false; - if (this.mask != a.mask) return false; - int n = children.length; - for (int i = 0; i < n; i++) { - if (!children[i].getValue().equalsKeys(a.children[i].getValue())) return false; - } - return true; - } - - @Override - public boolean equals(AMap a) { - if (!(a instanceof MapTree)) return false; - return equals((MapTree) a); - } - - boolean equals(MapTree b) { - if (this == b) return true; - long n = count; - if (n != b.count) return false; - if (mask != b.mask) return false; - if (shift != b.shift) return false; - - // Fall back to comparing hashes. Probably most efficient in general. - if (getHash().equals(b.getHash())) return true; - return false; - } - - @Override - public AHashMap mapEntries(Function, MapEntry> func) { - int n = children.length; - if (n == 0) return this; - Ref>[] newChildren = children; - for (int i = 0; i < n; i++) { - AHashMap child = children[i].getValue(); - AHashMap newChild = child.mapEntries(func); - if (child != newChild) { - if (children == newChildren) { - newChildren = children.clone(); - } - newChildren[i] = newChild.getRef(); - } - } - if (newChildren == children) return this; - - // Note: creation should remove any empty children. Need to recompute count - // since - // entries may have been removed. - return create(newChildren, shift, mask, computeCount(newChildren)); - } - - @Override - public void validate() throws InvalidDataException { - super.validate(); - // Perform validation for this tree position - validateWithPrefix(""); - } - - @Override - protected void validateWithPrefix(String prefix) throws InvalidDataException { - if (mask == 0) throw new InvalidDataException("TreeMap must have children!", this); - if (shift != prefix.length()) { - throw new InvalidDataException("Invalid prefix [" + prefix + "] for TreeMap with shift=" + shift, this); - } - int bsize = children.length; - - long childCount=0;; - for (int i = 0; i < bsize; i++) { - if (children[i] == null) - throw new InvalidDataException("Null child ref at " + prefix + Utils.toHexChar(digitForIndex(i, mask)), - this); - ACell o = children[i].getValue(); - if (!(o instanceof AHashMap)) { - throw new InvalidDataException( - "Expected map child at " + prefix + Utils.toHexChar(digitForIndex(i, mask)), this); - } - @SuppressWarnings("unchecked") - AHashMap child = (AHashMap) o; - if (child.isEmpty()) - throw new InvalidDataException("Empty child at " + prefix + Utils.toHexChar(digitForIndex(i, mask)), - this); - int d = digitForIndex(i, mask); - child.validateWithPrefix(prefix + Utils.toHexChar(d)); - - childCount += child.count(); - } - - if (count != childCount) { - throw new InvalidDataException("Bad child count, expected " + count + " but children had: " + childCount, this); - } - } - - private boolean isValidStructure() { - if (count <= MapLeaf.MAX_ENTRIES) return false; - if (children.length != Integer.bitCount(mask & 0xFFFF)) return false; - for (int i = 0; i < children.length; i++) { - if (children[i] == null) return false; - } - return true; - } - - @Override - public void validateCell() throws InvalidDataException { - if (!isValidStructure()) throw new InvalidDataException("Bad structure", this); - } - - @SuppressWarnings("unchecked") - @Override - public boolean containsAllKeys(AHashMap map) { - if (map instanceof MapTree) { - return containsAllKeys((MapTree)map); - } - // must be a MapLeaf - long n=map.count; - for (long i=0; i me=map.entryAt(i); - if (!this.containsKeyRef((Ref) me.getKeyRef())) return false; - } - return true; - } - - protected boolean containsAllKeys(MapTree map) { - // fist check this mask contains all of target mask - if ((this.mask|map.mask)!=this.mask) return false; - - for (int i=0; i<16; i++) { - Ref> child=this.childForDigit(i); - if (child==null) continue; - - Ref> mchild=map.childForDigit(i); - if (mchild==null) continue; - - if (!(child.getValue().containsAllKeys(mchild.getValue()))) return false; - } - return true; - } - - @Override - public byte getTag() { - return Tag.MAP; - } - - @Override - public AHashMap toCanonical() { - if (count > MapLeaf.MAX_ENTRIES) return this; - // shouldn't be possible? - throw new TODOException(); - } - -} diff --git a/convex-core/src/main/java/convex/core/data/Maps.java b/convex-core/src/main/java/convex/core/data/Maps.java deleted file mode 100644 index 6c2e298f8..000000000 --- a/convex-core/src/main/java/convex/core/data/Maps.java +++ /dev/null @@ -1,156 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.util.HashMap; - -import convex.core.exceptions.BadFormatException; -import convex.core.lang.RT; - -/** - * Utility class for map functions - * - */ -public class Maps { - - private static final AMap EMPTY_MAP = MapLeaf.emptyMap(); - - static { - // Set empty Ref flags as internal embedded constant - EMPTY_MAP.getRef().setFlags(Ref.INTERNAL_FLAGS); - } - - private static final Ref EMPTY_REF = EMPTY_MAP.getRef(); - - @SuppressWarnings("unchecked") - public static > R empty() { - return (R) EMPTY_MAP; - } - - @SuppressWarnings("unchecked") - public static > Ref emptyRef() { - return (Ref) EMPTY_REF; - } - - public static MapLeaf create(K k, V v) { - return MapLeaf.create(MapEntry.create(k, v)); - } - - /** - * Constructs a map with the given keys and values. If keys are repreated, later keys will - * overwrite earlier ones. Performs conversion to CVM types. - * @param Map type - * @param Key type - * @param Value type - * @param keysAndValues Keys and values to include - * @return Map with given keys and values - */ - @SuppressWarnings("unchecked") - public static , K extends ACell, V extends ACell> R of(Object... keysAndValues) { - int n = keysAndValues.length >> 1; - if (keysAndValues.length != n * 2) - throw new IllegalArgumentException("Even number of values need for key-value pairs"); - - AMap result = Maps.empty(); - for (int i = 0; i < n; i++) { - K key = (K) RT.cvm(keysAndValues[i * 2]); - V value = (V) RT.cvm(keysAndValues[i * 2 + 1]); - result = result.assoc(key, value); - } - return (R) result; - } - - /** - * Constructs a map with the given keys and values. If keys are repreated, later keys will - * overwrite earlier ones. Performs conversion to CVM types. - * @param Map type - * @param Key type - * @param Value type - * @param keysAndValues Keys and values to include - * @return Map with given keys and values - */ - @SuppressWarnings("unchecked") - public static , K extends ACell, V extends ACell> R create(ACell[] keysAndValues) { - int n = keysAndValues.length >> 1; - if (keysAndValues.length != n * 2) - throw new IllegalArgumentException("Even number of values need for key-value pairs"); - - AMap result = Maps.empty(); - for (int i = 0; i < n; i++) { - K key = (K) keysAndValues[i * 2]; - V value = (V) keysAndValues[i * 2 + 1]; - result = result.assoc(key, value); - } - return (R) result; - } - - @SuppressWarnings("unchecked") - public static HashMap hashMapOf(Object... keysAndValues) { - int n = keysAndValues.length >> 1; - HashMap result = new HashMap<>(n); - if (keysAndValues.length != n * 2) - throw new IllegalArgumentException("Even number of values need for key-value pairs"); - for (int i = 0; i < n; i++) { - K key = (K) keysAndValues[i * 2]; - V value = (V) keysAndValues[i * 2 + 1]; - result.put(key, value); - } - return result; - } - - /** - * Create a map with a collection of entries. - * - * @param Key type - * @param Value type - * @param entries Entries to include - * @return AHashMap instance - */ - public static AHashMap create(java.util.List> entries) { - return createWithShift(0, entries); - } - - /** - * Create a hashmap with the correct shift and given entries. - * - * @param Key type - * @param Value type - * @param shift Shift level of map - * @param entries Entries to include - * @return AHashMap instance - */ - public static AHashMap createWithShift(int shift, java.util.List> entries) { - int n = entries.size(); - if (n == 0) return empty(); - AHashMap result = Maps.empty(); - for (MapEntry e : entries) { - result = result.assocEntry(e, shift); - } - return result; - } - - @SuppressWarnings("unchecked") - public static > R coerce(AMap m) { - return (R) m; - } - - /** - * Read a Hashmap from a ByteBuffer. Assumes tag byte already read. - * @param Key type - * @param Value type - * @param bb ByteBuffer to read from - * @return Map instance - * @throws BadFormatException If encoding is invalid - */ - public static AHashMap read(ByteBuffer bb) throws BadFormatException { - long count = Format.readVLCLong(bb); - if (count <= MapLeaf.MAX_ENTRIES) { - return MapLeaf.read(bb, count); - } else { - return MapTree.read(bb, count); - } - } - - public static int MAX_ENCODING_SIZE = Math.max(MapTree.MAX_ENCODING_LENGTH, MapLeaf.MAX_ENCODING_LENGTH); - - -} diff --git a/convex-core/src/main/java/convex/core/data/PeerStatus.java b/convex-core/src/main/java/convex/core/data/PeerStatus.java deleted file mode 100644 index bcdbe0ccc..000000000 --- a/convex-core/src/main/java/convex/core/data/PeerStatus.java +++ /dev/null @@ -1,283 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; - -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.RT; -import convex.core.lang.impl.RecordFormat; -import convex.core.util.Utils; - -/** - * Class describing the on-chain state of a Peer declared on the network. - * - * State includes: - Stake placed by this Peer - A host address for peer - * connections / client requests - * - */ -public class PeerStatus extends ARecord { - private static final Keyword[] PEER_KEYS = new Keyword[] { Keywords.CONTROLLER, Keywords.STAKE, Keywords.STAKES,Keywords.DELEGATED_STAKE, - Keywords.METADATA}; - - private static final RecordFormat FORMAT = RecordFormat.of(PEER_KEYS); - - private final Address controller; - private final long stake; - private final long delegatedStake; - - private final ABlobMap stakes; - - /** - * Metadata for the Peer. Can be null internally, which is interpreted as an empty Map. - */ - private final AHashMap metadata; - - private PeerStatus(Address controller, long stake, ABlobMap stakes, long delegatedStake, AHashMap metadata) { - super(FORMAT); - this.controller = controller; - this.stake = stake; - this.delegatedStake = delegatedStake; - this.metadata = metadata; - this.stakes = stakes; - } - - public static PeerStatus create(Address controller, long stake) { - return create(controller, stake, null); - } - - public static PeerStatus create(Address controller, long stake, AHashMap metadata) { - return new PeerStatus(controller, stake, BlobMaps.empty(), 0L, metadata); - } - /** - * Gets the stake of this peer - * - * @return Total stake, including own stake + delegated stake - */ - public long getTotalStake() { - // TODO: include rewards? - return stake + delegatedStake; - } - - /** - * Gets the self-owned stake of this peer - * - * @return Own stake, excluding delegated stake - */ - public long getPeerStake() { - // TODO: include rewards? - return stake; - } - - /** - * Gets the controller of this peer - * - * @return The controller of this peer - */ - public Address getController() { - return controller; - } - - /** - * Gets the delegated stake of this peer - * - * @return Total of delegated stake - */ - public long getDelegatedStake() { - return delegatedStake; - } - - /** - * Gets the String representation of the hostname set for the current Peer status, - * or null if not specified. - * - * @return Hostname String - */ - public AString getHostname() { - if (metadata == null) return null; - return RT.ensureString(metadata.get(Keywords.URL)); - } - - /** - * Gets the Metadata of this Peer - * - * @return Host String - */ - public AHashMap getMetadata() { - return metadata==null?Maps.empty():metadata; - } - - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.PEER_STATUS; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = Format.write(bs,pos, controller); - pos = Format.writeVLCLong(bs,pos, stake); - pos = Format.write(bs,pos, stakes); - pos = Format.writeVLCLong(bs,pos, delegatedStake); - pos = Format.write(bs,pos, metadata); - return pos; - } - - public static PeerStatus read(ByteBuffer bb) throws BadFormatException { - Address owner = Format.read(bb); - long stake = Format.readVLCLong(bb); - ABlobMap stakes = Format.read(bb); - long delegatedStake = Format.readVLCLong(bb); - - AHashMap metadata = Format.read(bb); - - return new PeerStatus(owner, stake,stakes,delegatedStake,metadata); - } - - @Override - public int estimatedEncodingSize() { - return 100; - } - - @Override - public boolean isCanonical() { - return true; - } - - - /** - * Gets the delegated stake on this peer for the given delegator. - * - * Returns 0 if the delegator has no stake. - * - * @param delegator Address of delegator - * @return Value of delegated stake - */ - public long getDelegatedStake(Address delegator) { - // TODO: include rewards? - - CVMLong a = stakes.get(delegator); - if (a == null) return 0; - return a.longValue(); - } - - /** - * Sets the delegated stake on this peer for the given delegator. - * - * A value of 0 will remove the delegator's stake entirely - * - * @param delegator Address of delegator - * @param newStake New Delegated stake for the given Address - * @return Value of delegated stake - */ - public PeerStatus withDelegatedStake(Address delegator, long newStake) { - long oldStake = getDelegatedStake(delegator); - if (oldStake == newStake) return this; - - // compute adjustment to total delegated stake - long newDelegatedStake = delegatedStake + newStake - oldStake; - - ABlobMap newStakes = (newStake == 0L) ? stakes.dissoc(delegator) - : stakes.assoc(delegator, CVMLong.create(newStake)); - return new PeerStatus(controller, stake, newStakes, newDelegatedStake, metadata); - } - - /** - * Sets the Peer Stake on this peer for the given delegator. - * - * A value of 0 will remove the Peer stake entirely - * - * @param newStake New Delegated stake for the given Address - * @return Value of delegated stake - */ - public PeerStatus withPeerStake(long newStake) { - if (stake == newStake) return this; - - return new PeerStatus(controller, newStake, stakes, delegatedStake, metadata); - } - - public PeerStatus withHostname(AString newHostname) { - AHashMap newMeta=metadata; - if (newMeta==null) { - newMeta=Maps.create(Keywords.URL, newHostname); - } else { - newMeta=newMeta.assoc(Keywords.URL, newHostname); - } - if (metadata==newMeta) return this; - - return new PeerStatus(controller, stake, stakes, delegatedStake, newMeta); - } - - @Override - public void validateCell() throws InvalidDataException { - // TODO: Nothing? - } - - @Override - public ACell get(ACell key) { - if (Keywords.CONTROLLER.equals(key)) return controller; - if (Keywords.STAKE.equals(key)) return CVMLong.create(stake); - if (Keywords.STAKES.equals(key)) return stakes; - if (Keywords.DELEGATED_STAKE.equals(key)) return CVMLong.create(delegatedStake); - if (Keywords.METADATA.equals(key)) return metadata; - - return null; - } - - @Override - public byte getTag() { - return Tag.PEER_STATUS; - } - - @SuppressWarnings("unchecked") - @Override - protected PeerStatus updateAll(ACell[] newVals) { - Address newOwner = (Address) newVals[0]; - long newStake = ((CVMLong) newVals[1]).longValue(); - ABlobMap newStakes = (ABlobMap) newVals[2]; - long newDelStake = ((CVMLong) newVals[3]).longValue(); - AHashMap newMeta = (AHashMap) newVals[4]; - - if ((this.stake==newStake)&&(this.stakes==newStakes) - &&(this.metadata==newMeta)&&(this.delegatedStake==newDelStake)) { - return this; - } - return new PeerStatus(newOwner, newStake, newStakes, newDelStake, newMeta); - } - - protected static long computeDelegatedStake(ABlobMap stakes) { - long ds = stakes.reduceValues((acc, e)->acc+e.longValue(), 0L); - return ds; - } - - @Override - public boolean equals(AMap a) { - if (this == a) return true; // important optimisation for e.g. hashmap equality - if (a == null) return false; - if (a.getTag()!=getTag()) return false; - PeerStatus as=(PeerStatus)a; - return equals(as); - } - - /** - * Tests if this PeerStatus is equal to another - * @param a PeerStatus to compare with - * @return true if equal, false otherwise - */ - public boolean equals(PeerStatus a) { - if (a == null) return false; - Hash h=this.cachedHash(); - if (h!=null) { - Hash ha=a.cachedHash(); - if (ha!=null) return Utils.equals(h, ha); - } - - if (stake!=a.stake) return false; - if (delegatedStake!=a.delegatedStake) return false; - if (!(Utils.equals(metadata, a.metadata))) return false; - if (!(Utils.equals(stakes, a.stakes))) return false; - if (!(Utils.equals(controller, a.controller))) return false; - return true; - } -} diff --git a/convex-core/src/main/java/convex/core/data/Ref.java b/convex-core/src/main/java/convex/core/data/Ref.java deleted file mode 100644 index 2ae779599..000000000 --- a/convex-core/src/main/java/convex/core/data/Ref.java +++ /dev/null @@ -1,687 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.util.HashSet; -import java.util.function.Consumer; - -import convex.core.data.prim.CVMBool; -import convex.core.exceptions.InvalidDataException; -import convex.core.exceptions.MissingDataException; -import convex.core.lang.RT; -import convex.core.store.AStore; -import convex.core.store.Stores; -import convex.core.util.Utils; - -/** - * Class representing a smart reference to a decentralised data object. - * - * "The greatest trick the Devil ever pulled was convincing the world he didn’t - * exist." - The Usual Suspects - * - * A Ref itself is not a cell, but may be contained within a cell, in which case - * the cell class must implement IRefContainer in order to persist and update - * contained Refs correctly - * - * Refs include a status that indicates the level of validation proven. It is - * important not to rely on the value of a Ref until it has a sufficient status - * - e.g. a minimum status of PERSISTED is required to be able to guarantee - * walking an entire nested data structure. - * - * Guarantees: - O(1) access to the Hash value, cached on first access - O(1) - * access to the referenced object (though may required hitting storage if not - * cached) - Indirectly referenced values may be collected by the garbage - * collector, with the assumption that they can be retrieved from storage if - * required - * - * @param Type of stored value - */ -public abstract class Ref extends AObject implements Comparable>, IWriteable, IValidated { - - /** - * Ref status indicating the status of this Ref is unknown. This is the default - * for new Refs - */ - public static final int UNKNOWN = 0; - - /** - * Ref status indicating the Ref has been shallowly persisted in long term - * storage. The Ref can be made soft, and retrieved from storage if needed. No - * guarantee about the existence / status of any child objects. - */ - public static final int STORED = 1; - - /** - * Ref status indicating the Ref has been deeply persisted in long term storage. - * The Ref and its children can be assumed to be accessible for the life of the - * storage subsystem execution. - */ - public static final int PERSISTED = 2; - - /** - * Ref status indicating the Ref has been both persisted and validated as genuine - * valid CVM data. - */ - public static final int VALIDATED = 3; - - /** - * Ref status indicating the Ref has been shared by this peer in an announced - * Belief. This means that the Peer has a commitment to maintain this data - */ - public static final int ANNOUNCED = 4; - - /** - * Ref status indicating the Ref is an internal embedded value that can be - * encoded and used independency of any given store state - */ - public static final int INTERNAL = 5; - - /** - * Maximum Ref status - */ - public static final int MAX_STATUS = INTERNAL; - - /** - * MAsk for Ref flag bits representing the Status - */ - public static final int STATUS_MASK = 0x0F; - - /** - * Mask bit for a proven embedded value - */ - public static final int KNOWN_EMBEDDED_MASK = 0x10; - - /** - * Mask bit for a proven non-embedded value - */ - public static final int NON_EMBEDDED_MASK = 0x20; - - /** - * Mask for embedding status - */ - public static final int EMBEDDING_MASK = KNOWN_EMBEDDED_MASK | NON_EMBEDDED_MASK; - - /** - * Mask bit for verified data, especially signatures - */ - public static final int VERIFIED_MASK = 0x40; - - /** - * Mask bit for bad data, especially signatures proved invalid - */ - public static final int BAD_MASK = 0x80; - - /** - * Mask bit for bad data, especially signatures proved invalid - */ - public static final int VERIFICATION_MASK = VERIFIED_MASK | BAD_MASK; - - /** - * Flags for internal constant values - */ - public static final int INTERNAL_FLAGS=INTERNAL|KNOWN_EMBEDDED_MASK|VERIFIED_MASK; - - /** - * Ref status indicating that the Ref refers to data that has been proven to be invalid - */ - public static final int INVALID = -1; - - /** - * Ref for null value. Important because we can't persist this, since null - * collides with the result of an empty soft reference. - */ - public static final RefDirect NULL_VALUE = RefDirect.create(null, Hash.NULL_HASH, INTERNAL_FLAGS); - - public static final RefDirect TRUE_VALUE = RefDirect.create(CVMBool.TRUE, Hash.TRUE_HASH, INTERNAL_FLAGS); - public static final RefDirect FALSE_VALUE = RefDirect.create(CVMBool.FALSE, Hash.FALSE_HASH, INTERNAL_FLAGS); - - /** - * Length of an external Reference encoding. Will be a tag byte plus the Hash length - */ - public static final int INDIRECT_ENCODING_LENGTH = 1+Hash.LENGTH; - - - - /** - * Hash of the serialised representation of the value Computed and stored upon - * demand. - */ - protected Hash hash; - - /** - * Flag values including Status of this Ref. See public Ref status constants. - * - * May be incremented atomically in the event of validation, proven storage. - */ - protected int flags; - - protected Ref(Hash hash, int flags) { - this.hash = hash; - this.flags = flags; - } - - /** - * Gets the status of this Ref - * - * @return UNKNOWN, PERSISTED, VERIFIED, ACCOUNCED or INVALID Ref status - * constants - */ - public int getStatus() { - return flags&STATUS_MASK; - } - - /** - * Gets flags with an updated status - * @param newStatus New status to apply to flags - * @return Updated flags (does not change this Ref) - */ - public int flagsWithStatus(int newStatus) { - return (flags&~STATUS_MASK)|(newStatus&STATUS_MASK); - } - - /** - * Gets the flags for this Ref - * - * @return flag int value - */ - public int getFlags() { - return flags; - } - - /** - * Ensures the Ref has the given status, at minimum - * - * Assumes any necessary changes to storage will be made separately. - * SECURITY: Dangerous if misused since may invalidate storage assumptions - * @param newStatus New status to apply to Ref - * @return Updated Ref - */ - public Ref withMinimumStatus(int newStatus) { - newStatus&=STATUS_MASK; - int status=getStatus(); - if (status >= newStatus) return this; - if (status > MAX_STATUS) { - throw new IllegalArgumentException("Ref status not recognised: " + newStatus); - } - int newFlags=(flags&(~STATUS_MASK))|newStatus; - return withFlags(newFlags); - } - - /** - * Create a new Ref of the same type with updated flags - * @param newFlags New flags to set - * @return Updated Ref - */ - public abstract Ref withFlags(int newFlags); - - /** - * Gets the value from this Ref. - * - * Important notes: - May throw a MissingDataException if the data does not - * exist in available storage - Will return null if and only if the Ref refers - * to the null value - * - * @return The value contained in this Ref - */ - public abstract T getValue(); - - @Override - public int hashCode() { - return getHash().hashCode(); - } - - @Override - public void print(StringBuilder sb) { - sb.append("#ref {:hash #hash "); - sb.append((hash==null)?"nil":hash.toString()); - sb.append(", :flags "); - sb.append(flags); - sb.append("}"); - } - - @Override - public String toString() { - // TODO. Why protected by a try-catch? Looks like it will never throw. - StringBuilder sb = new StringBuilder(); - try { - print(sb); - } catch (MissingDataException e) { - throw Utils.sneakyThrow(e); - } - return sb.toString(); - } - - @SuppressWarnings("unchecked") - @Override - public boolean equals(Object o) { - if (!(o instanceof Ref)) return false; - return equalsValue((Ref) o); - } - - /** - * Checks if two Ref Values are equal. Equality is defined as referring to the - * same data, i.e. have an identical hash. - * - * @param a The Ref to compare with - * @return true if Refs have the same value, false otherwise - */ - public abstract boolean equalsValue(Ref a); - - @Override - public int compareTo(Ref a) { - if (this == a) return 0; - return getHash().compareTo(a.getHash()); - } - - /** - * Gets the Hash of this ref's value. - * - * @return Hash of the value - */ - public abstract Hash getHash(); - - /** - * Gets the Hash of this ref's value, or null if not yet computed - * - * @return Hash of the value - */ - public final Hash cachedHash() { - return hash; - } - - /** - * Returns a direct Ref wrapping the given value. Does not perform any Ref - * lookup in stores etc. - * - * @param value Value to wrap in the Ref - * @return New Ref wrapping the given value. - */ - @SuppressWarnings("unchecked") - public static Ref get(T value) { - if (value==null) return (Ref) NULL_VALUE; - return value.getRef(); - } - - @SuppressWarnings("unchecked") - public static Ref get(Object value) { - if (value==null) return (Ref) NULL_VALUE; - if (value instanceof ACell) return ((ACell)value).getRef(); - return RT.cvm(value).getRef(); - } - - /** - * Creates a RefSoft using a specific Hash. Fetches the actual value lazily from the - * store on demand. - * - * Internal soft reference may be initially empty: This Ref might not have - * available data in the store, in which case calls to getValue() may result in - * a MissingDataException - * - * WARNING: Does not mark as either embedded or non-embedded, as this might be a top level - * entry in the store. isEmbedded() will query the store to determine status. - * - * @param hash The hash value for this Ref to refer to - * @return Ref for the specific hash. - */ - public static RefSoft forHash(Hash hash) { - return RefSoft.createForHash(hash); - } - - public Ref markEmbedded(boolean isEmbedded) { - int newFlags=mergeFlags(flags,(isEmbedded?KNOWN_EMBEDDED_MASK:NON_EMBEDDED_MASK)); - flags=newFlags; - return this; - } - - /** - * Sets the Flags for this Ref. WARNING: caller must have performed any necessary validation - * @param newFlags Flags to set - * @return Updated Ref - */ - public Ref setFlags(int newFlags) { - flags=newFlags; - return this; - } - - /** - * Reads a ref from the given ByteBuffer. Assumes no tag. - * - * Marks as non-embedded - * - * @param data ByteBuffer containing the data to read at the current position - * @return Ref read from ByteBuffer - */ - public static Ref readRaw(ByteBuffer data) { - Hash h = Hash.readRaw(data); - Ref ref=Ref.forHash(h); - return ref.markEmbedded(false); - } - - public void validate() throws InvalidDataException { - if (hash != null) hash.validate(); - // TODO is this sane? - if (getStatus() < VALIDATED) { - T o = getValue(); - o.validate(); - } - } - - /** - * Return true if this Ref is a direct reference, i.e. the value is pinned in - * memory and cannot be garbage collected - * - * @return true if this Ref is direct, false otherwise - */ - public abstract boolean isDirect(); - - /** - * Return true if this Ref's status indicates it has definitely been persisted - * to storage. - * - * May return false negatives, e.g. the object could be in the store but this - * Ref instance still has a status of "UNKNOWN". - * - * @return true if this Ref has a status of PERSISTED or above, false otherwise - */ - public boolean isPersisted() { - return getStatus() >= PERSISTED; - } - - /** - * Persists this Ref in the current store if not embedded and not already - * persisted. - * - * This may convert the Ref from a direct reference to a soft reference. - * - * If the persisted Ref represents novelty, will trigger the specified novelty - * handler - * - * @param noveltyHandler Novelty handler to call (may be null) - * @return the persisted Ref - * @throws MissingDataException If the Ref's value does not exist or has been - * garbage collected before being persisted - */ - @SuppressWarnings("unchecked") - public Ref persist(Consumer> noveltyHandler) { - int status = getStatus(); - if (status >= PERSISTED) return (Ref) this; // already persisted in some form - AStore store=Stores.current(); - return (Ref) store.storeRef(this, Ref.PERSISTED,noveltyHandler); - } - - /** - * Persists this Ref in the current store if not embedded and not already - * persisted. Resulting status will be PERSISTED or higher. - * - * This may convert the Ref from a direct reference to a soft reference. - * - * @throws MissingDataException if the Ref cannot be fully persisted. - * @return the persisted Ref - */ - public Ref persist() { - return persist(null); - } - - /** - * Accumulates the set of all unique Refs in the given object. - * - * Might stack overflow if nesting is too deep - not for use in on-chain code. - * - * @param a Ref or Cell - * @return Set containing all unique refs (accoumulated recursively) within the - * given object - */ - public static java.util.Set> accumulateRefSet(Object a) { - HashSet> hs = new HashSet<>(); - accumulateRefSet(a, hs); - return hs; - } - - private static void accumulateRefSet(Object a, HashSet> hs) { - if (a instanceof Ref) { - Ref ref = (Ref) a; - if (hs.contains(ref)) return; - hs.add(ref); - accumulateRefSet(ref.getValue(), hs); - } else if (a instanceof ACell) { - ACell rc = (ACell) a; - rc.updateRefs(r -> { - accumulateRefSet(r, hs); - return r; - }); - } - } - - - - /** - * Updates an array of Refs with the given function. - * - * Returns the original array unchanged if no refs were changed, otherwise - * returns a new array. - * - * @param refs Array of Refs to update - * @param func Ref update function - * @return Array of updated Refs - */ - public static Ref[] updateRefs(Ref[] refs, IRefFunction func) { - Ref[] newRefs = refs; - int n = refs.length; - for (int i = 0; i < n; i++) { - Ref ref = refs[i]; - @SuppressWarnings("unchecked") - Ref newRef = (Ref) func.apply(ref); - if (ref != newRef) { - // Ensure newRefs is a new copy since we are making at least one change - if (newRefs == refs) { - newRefs = refs.clone(); - } - newRefs[i] = newRef; - } - } - return newRefs; - } - - @SuppressWarnings("unchecked") - public static Ref[] createArray(T[] values) { - int n = values.length; - Ref[] refs = new Ref[n]; - for (int i = 0; i < n; i++) { - refs[i] = Ref.get(values[i]); - } - return refs; - } - - /** - * Adds the value of this Ref and all non-embedded child values to a given set. - * - * Logically, provides the guarantee that the set will contain all cells needed - * to recreate the complete value of this Ref. - * - * @param store Store to add to - * @return Set containing this Ref and all direct or indirect child refs - */ - @SuppressWarnings("unchecked") - public ASet addAllToSet(ASet store) { - store = store.includeRef((Ref) this); - ACell rc = getValue(); - - int n = rc.getRefCount(); - for (int i = 0; i < n; i++) { - Ref rr = rc.getRef(i); - if (rr.isEmbedded()) continue; - store = rr.addAllToSet(store); - } - return store; - } - - /** - * Check if the Ref's value is embedded. - * - * If false, the value must be an ACell instance. - * - * @return true if embedded, false otherwise - */ - public final boolean isEmbedded() { - if ((flags&KNOWN_EMBEDDED_MASK)!=0) return true; - if ((flags&NON_EMBEDDED_MASK)!=0) return false; - boolean em= Format.isEmbedded(getValue()); - flags=flags|(em?KNOWN_EMBEDDED_MASK:NON_EMBEDDED_MASK); - return em; - } - - /** - * Converts this Ref to a RefDirect - * @return Direct Ref - */ - public Ref toDirect() { - return RefDirect.create(getValue(), hash, flags); - } - - /** - * Persists a Ref shallowly in the current store. - * - * Status will be updated to STORED or higher. - * - * @return Ref with status of STORED or above - */ - public Ref persistShallow() { - return persistShallow(null); - } - - /** - * Persists a Ref shallowly in the current store. - * - * Status will be updated STORED or higher. Novelty handler will be called exactly once if and only if - * the ref was not previously stored - * - * @param noveltyHandler Novelty handler to call (may be null) - * @return Ref with status of STORED or above - */ - @SuppressWarnings("unchecked") - public Ref persistShallow(Consumer> noveltyHandler) { - AStore store=Stores.current(); - return (Ref) store.storeTopRef((Ref)this, Ref.STORED, noveltyHandler); - } - - /** - * Updates the value stored within this Ref. New value must be equal in value to the old value - * (identical hash), but may have updated internal refs etc. - * - * @param newValue New value - * @return Updated Ref - */ - public abstract Ref withValue(T newValue); - - /** - * Writes the ref to a byte array. Embeds embedded values as necessary. - * @param bs Byte array to encode to - * @return Updated position - */ - @Override - public final int encode(byte[] bs, int pos) { - if (isEmbedded()) { - T value=getValue(); - if (value==null) { - bs[pos++]=Tag.NULL; - return pos; - } - return value.encode(bs, pos); - } else { - bs[pos++]=Tag.REF; - return getHash().encodeRaw(bs, pos); - } - } - - @Override - public final ByteBuffer write(ByteBuffer bb) { - if (isEmbedded()) { - return Format.write(bb, getValue()); - } else { - bb=bb.put(Tag.REF); - return getHash().writeToBuffer(bb); - } - } - - @Override - protected Blob createEncoding() { - if (isEmbedded()) { - return Format.encodedBlob(getValue()); - } - - byte[] bs=new byte[Ref.INDIRECT_ENCODING_LENGTH]; - Hash h=getHash(); - int pos=0; - bs[pos++]=Tag.REF; - pos=h.encodeRaw(bs, pos); - return Blob.wrap(bs,0,pos); - } - - /** - * Gets the encoding length for writing this Ref. Will be equal to the encoding length - * of the Ref's value if embedded, otherwise INDIRECT_ENCODING_LENGTH - * - * @return Exact length of encoding - */ - public final long getEncodingLength() { - if (isEmbedded()) { - T value=getValue(); - if (value==null) return 1; - return value.getEncodingLength(); - } else { - return Ref.INDIRECT_ENCODING_LENGTH; - } - } - - /** - * Gets the indirect memory size for this Ref - * @return 0 for fully embedded values with no child refs, memory size of referred value otherwise - */ - public long getMemorySize() { - T value=getValue(); - if (value==null) return 0; - return value.getMemorySize(); - } - - /** - * Finds all instances of missing data in this Ref, and adds them to the missing set - * @param missingSet Set to add missing instances to - */ - public void findMissing(HashSet missingSet) { - if (getStatus()>=Ref.PERSISTED) return; - if (isMissing()) { - missingSet.add(getHash()); - } else { - // Should be OK to get value, since non-missing! - T val=getValue(); - - // TODO: maybe needs to be non-stack-consuming? - // recursively scan for missing children - int n=val.getRefCount(); - for (int i=0; i r=val.getRef(i); - r.findMissing(missingSet); - } - } - } - - /** - * Checks if this Ref refers to missing data, i.e. a Cell that does not exist in the - * currect store. - * - * @return true if this specific Ref has missing data, false otherwise. - */ - public abstract boolean isMissing(); - - /** - * Merges flags in an idempotent way. Assume flags are valid - * @param a First set of flags - * @param b Second set of flags - * @return Merged flags - */ - public static int mergeFlags(int a, int b) { - return ((a|b)&~STATUS_MASK)|Math.max(a&STATUS_MASK, b& STATUS_MASK); - } - - - -} diff --git a/convex-core/src/main/java/convex/core/data/RefDirect.java b/convex-core/src/main/java/convex/core/data/RefDirect.java deleted file mode 100644 index 907c553c3..000000000 --- a/convex-core/src/main/java/convex/core/data/RefDirect.java +++ /dev/null @@ -1,130 +0,0 @@ -package convex.core.data; - -import convex.core.exceptions.InvalidDataException; -import convex.core.util.Utils; - -/** - * Ref subclass for direct in-memory references. - * - * Direct Refs store the underlying value directly with a regular Java strong reference. - * - *

Care must be taken to ensure recursive structures do not exceed reasonable memory bounds. - * In smart contract execution, juice limits serve this purpose.

- * - * @param Type of Value referenced - */ -public class RefDirect extends Ref { - /** - * Direct value of this Ref - */ - private final T value; - - private RefDirect(T value, Hash hash, int flags) { - super(hash, flags); - - this.value = value; - } - - /** - * Construction function for a Direct Ref - * @param Type of value - * @param value Value for the Ref - * @param hash Hash (may be null) - * @param status Status for the Ref - * @return New Direct Ref - */ - public static RefDirect create(T value, Hash hash, int status) { - int flags=status&Ref.STATUS_MASK; - return new RefDirect(value, hash, flags); - } - - /** - * Creates a direct Ref to the given value - * @param Type of value - * @param value Any value (may be embedded or otherwise, but should not be null) - * @param hash Hash of value's encoding, or null if not known - * @return Direct Ref to Value - */ - public static RefDirect create(T value, Hash hash) { - return create(value, hash, UNKNOWN); - } - - /** - * Creates a new Direct ref to the given value. Does not compute hash. - * @param Type of Value - * @param value Value - * @return Direct Ref to Value - */ - @SuppressWarnings("unchecked") - public static RefDirect create(T value) { - if (value==null) return (RefDirect) Ref.NULL_VALUE; - return create(value, null, UNKNOWN); - } - - public T getValue() { - return value; - } - - @Override - public boolean isDirect() { - return true; - } - - @Override - public Hash getHash() { - if (hash!=null) return hash; - Hash newHash=(value==null)?Hash.NULL_HASH:value.getHash(); - hash=newHash; - return newHash; - } - - @Override - public Ref toDirect() { - return this; - } - - @Override - public boolean equalsValue(Ref a) { - if (a == this) return true; - if (this.hash != null) { - // use hash if available - if (a.hash != null) return this.hash.equals(a.hash); - } - return Utils.equals(this.value, a.getValue()); - } - - @Override - public void validate() throws InvalidDataException { - super.validate(); - if (isEmbedded() != Format.isEmbedded(value)) throw new InvalidDataException("Embedded flag is wrong!", this); - if (value == null) { - if (this != Ref.NULL_VALUE) throw new InvalidDataException("Null ref not singleton!", this); - } - } - - @Override - public Ref withValue(T newValue) { - if (newValue!=value) return new RefDirect(newValue,hash,flags); - return this; - } - - @Override - public int estimatedEncodingSize() { - if(value==null) return Format.NULL_ENCODING_LENGTH; - return isEmbedded()?value.estimatedEncodingSize():Ref.INDIRECT_ENCODING_LENGTH; - } - - @Override - public boolean isMissing() { - // Never missing, since we have the value at hand - return false; - } - - @Override - public RefDirect withFlags(int newFlags) { - return new RefDirect(value,hash,newFlags); - } - - - -} diff --git a/convex-core/src/main/java/convex/core/data/RefSoft.java b/convex-core/src/main/java/convex/core/data/RefSoft.java deleted file mode 100644 index 9b0dcecce..000000000 --- a/convex-core/src/main/java/convex/core/data/RefSoft.java +++ /dev/null @@ -1,151 +0,0 @@ -package convex.core.data; - -import java.lang.ref.SoftReference; - -import convex.core.exceptions.InvalidDataException; -import convex.core.exceptions.MissingDataException; -import convex.core.store.Stores; -import convex.core.util.Utils; - -/** - * Reference class implemented via a soft reference and store lookup. - * - * Ref makes use of a soft reference to values, allowing memory to be reclaimed - * by the garbage collector when not required. A MissingDataException will occur - * with any attempt to deference this Ref when the value is not present - * and not stored in the current store. - * - * Instances of this class should usually be be STORED, otherwise data loss - * may occur due to garbage collection. However UNKNOWN RefSoft may exist temporarily - * (e.g. reading Refs from external messages) - * - * SoftRef must always have a non-null hash, to ensure lookup capability in - * store. - * - * @param Type of referenced Cell - */ -public class RefSoft extends Ref { - - /** - * SoftReference to value. Might get updated to a fresh instance. - */ - protected SoftReference softRef; - - protected RefSoft(SoftReference ref, Hash hash, int flags) { - super(hash, flags); - this.softRef = ref; - } - - protected RefSoft(T value, Hash hash, int flags) { - this(new SoftReference(value), hash, flags); - } - - protected RefSoft(Hash hash) { - // We don't know anything about this Ref. - this(new SoftReference(null), hash, UNKNOWN); - } - - - @Override - public RefSoft withFlags(int newFlags) { - return new RefSoft(softRef,hash,newFlags); - } - - public static RefSoft create(T value, int flags) { - Hash hash=Hash.compute(value); - return new RefSoft(value, hash, flags); - } - - /** - * Create a RefSoft with a Hash reference. - * - * Attempts to get the value will trigger a store lookup, which may in turn - * cause a MissingDataException if not found. - * - * @param Type of value - * @param hash Hash ID of value. - * @return New RefSoft instance - */ - public static RefSoft createForHash(Hash hash) { - return new RefSoft(hash); - } - - @Override - public T getValue() { - T result = softRef.get(); - if (result == null) { - Ref storeRef = Stores.current().refForHash(hash); - if (storeRef == null) throw Utils.sneakyThrow(new MissingDataException(hash)); - result = storeRef.getValue(); - - if (storeRef instanceof RefSoft) { - // Update soft reference to the fresh version. No point keeping old one.... - this.softRef = ((RefSoft) storeRef).softRef; - } else { - this.softRef = new SoftReference(result); - } - } - return result; - } - - @Override - public boolean isMissing() { - T result = softRef.get(); - if (result != null) return false; // still in memory, so not missing - - // check store - Ref storeRef = Stores.current().refForHash(hash); - if (storeRef == null) return true; // must be missing, couldn't find in store - - // We know we have in store. - // Update soft reference to the fresh version. No point keeping old one.... - if (storeRef instanceof RefSoft) { - this.softRef = ((RefSoft) storeRef).softRef; - } else { - this.softRef = new SoftReference(storeRef.getValue()); - } - return false; - } - - @Override - public boolean equalsValue(Ref a) { - if (a.hash!=null) { - return hash.equals(a.hash); - } - // compare by value - return Utils.equals(getValue(),a.getValue()); - } - - @Override - public boolean isDirect() { - return false; - } - - @Override - public Hash getHash() { - return hash; - } - - - @Override - public void validate() throws InvalidDataException { - super.validate(); - if (hash == null) throw new InvalidDataException("Hash should never be null in soft ref", this); - ACell val = softRef.get(); - boolean embedded=isEmbedded(); - if (embedded!=Format.isEmbedded(val)) { - throw new InvalidDataException("Embedded flag ["+embedded+"] inconsistent with value", this); - } - } - - @Override - public Ref withValue(T newValue) { - if (softRef.get()!=newValue) return new RefSoft(newValue,hash,flags); - return this; - } - - @Override - public int estimatedEncodingSize() { - return isEmbedded()?Format.MAX_EMBEDDED_LENGTH: INDIRECT_ENCODING_LENGTH; - } -} diff --git a/convex-core/src/main/java/convex/core/data/SetLeaf.java b/convex-core/src/main/java/convex/core/data/SetLeaf.java deleted file mode 100644 index a83ac8409..000000000 --- a/convex-core/src/main/java/convex/core/data/SetLeaf.java +++ /dev/null @@ -1,535 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.function.BiFunction; - -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.exceptions.TODOException; -import convex.core.util.Utils; - -/** - * Limited size Persistent Merkle Set implemented as a small sorted list of - * Values - * - * Must be sorted by Key hash value to ensure uniqueness of representation - * - * @param Type of values - */ -public class SetLeaf extends AHashSet { - /** - * Maximum number of entries in a SetLeaf - */ - public static final int MAX_ENTRIES = 16; - - private final Ref[] entries; - - SetLeaf(Ref[] items) { - super(items.length); - entries = items; - } - - /** - * Creates a SetLeaf with the specified elements. - * - * Null entries are ignored/removed. - * - * @param elements Refs of Elements to include - * @return New ListMap - */ - @SafeVarargs - public static SetLeaf create(Ref... elements) { - return create(elements, 0, elements.length); - } - - /** - * Creates a SetLeaf with the specified elements. Null references are - * ignored/removed. - * - * @param entries Refs to elements to include (some may be null) - * @param offset Offset into entries array - * @param length Number of entries to take from entries array, starting at - * offset - * @return A new ListMap - */ - protected static SetLeaf create(Ref[] entries, int offset, int length) { - if (length == 0) return Sets.empty(); - if (length > MAX_ENTRIES) throw new IllegalArgumentException("Too many elements: " + entries.length); - Ref[] sorted = Utils.copyOfRangeExcludeNulls(entries, offset, offset + length); - if (sorted.length == 0) return Sets.empty(); - Arrays.sort(sorted); - return new SetLeaf(sorted); - } - - @SuppressWarnings("unchecked") - public static SetLeaf create(V item) { - return new SetLeaf(new Ref[] { Ref.get(item) }); - } - - @Override - public Ref getValueRef(ACell k) { - // Use cached hash if available - Hash h=(k==null)?Hash.NULL_HASH:k.cachedHash(); - if (h!=null) return getRefByHash(h); - - int len = size(); - for (int i = 0; i < len; i++) { - Ref e = entries[i]; - if (Utils.equals(k, e.getValue())) return e; - } - return null; - } - - @Override - public boolean containsHash(Hash hash) { - return getRefByHash(hash) != null; - } - - @Override - protected Ref getRefByHash(Hash hash) { - int len = size(); - for (int i = 0; i < len; i++) { - Ref e = entries[i]; - if (hash.equals(e.getHash())) return e; - } - return null; - } - - /** - * Gets the index of key k in the internal array, or -1 if not found - * - * @param key - * @return - */ - private int seek(T key) { - int len = size(); - for (int i = 0; i < len; i++) { - if (Utils.equals(key, entries[i].getValue())) return i; - } - return -1; - } - - /** - * Gets the index of key k in the internal array, or -1 if not found - * - * @param key - * @return - */ - private int seekKeyRef(Ref key) { - Hash h=key.getHash(); - int len = size(); - for (int i = 0; i < len; i++) { - if (h.compareTo(entries[i].getHash())==0) return i; - } - return -1; - } - - @SuppressWarnings("unchecked") - @Override - public SetLeaf exclude(ACell key) { - int i = seek((T)key); - if (i < 0) return this; // not found - return excludeAt(i); - } - - @Override - public SetLeaf excludeRef(Ref key) { - int i = seekKeyRef(key); - if (i < 0) return this; // not found - return excludeAt(i); - } - - @SuppressWarnings("unchecked") - private SetLeaf excludeAt(int index) { - int len = size(); - if (len == 1) return Sets.empty(); - Ref[] newEntries = (Ref[]) new Ref[len - 1]; - System.arraycopy(entries, 0, newEntries, 0, index); - System.arraycopy(entries, index + 1, newEntries, index, len - index - 1); - return new SetLeaf(newEntries); - } - - protected void accumulateValues(ArrayList al) { - for (int i = 0; i < entries.length; i++) { - Ref me = entries[i]; - al.add(me.getValue()); - } - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.SET; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - long n=count(); - pos = Format.writeVLCLong(bs,pos, n); - - for (int i = 0; i < n; i++) { - pos = entries[i].encode(bs, pos);; - } - return pos; - } - - @Override - public int estimatedEncodingSize() { - // allow space for header, size byte, 2 refs per entry - return 2 + Format.MAX_EMBEDDED_LENGTH * size(); - } - - public static int MAX_ENCODING_LENGTH= 2 + MAX_ENTRIES * Format.MAX_EMBEDDED_LENGTH; - - /** - * Reads a MapLeaf from the provided ByteBuffer Assumes the header byte is - * already read. - * - * @param bb ByteBuffer to read from - * @param count Count of map elements - * @return A Map as deserialised from the provided ByteBuffer - * @throws BadFormatException If encoding is invalid - */ - @SuppressWarnings("unchecked") - public static SetLeaf read(ByteBuffer bb, long count) throws BadFormatException { - if (count == 0) return Sets.empty(); - if (count < 0) throw new BadFormatException("Negative count of map elements!"); - if (count > MAX_ENTRIES) throw new BadFormatException("MapLeaf too big: " + count); - - Ref[] items = (Ref[]) new Ref[(int) count]; - for (int i = 0; i < count; i++) { - Ref ref=Format.readRef(bb); - items[i]=ref; - } - - if (!isValidOrder(items)) { - throw new BadFormatException("Bad ordering of keys!"); - } - - return new SetLeaf(items); - } - - - @SuppressWarnings("unchecked") - public static SetLeaf emptySet() { - return (SetLeaf) Sets.EMPTY; - } - - @Override - public boolean isCanonical() { - // validation for both key uniqueness and sort order - return isValidOrder(entries); - } - - @Override public final boolean isCVMValue() { - return true; - } - - private static boolean isValidOrder(Ref[] entries) { - long count = entries.length; - for (int i = 0; i < count - 1; i++) { - Hash a = entries[i].getHash(); - Hash b = entries[i + 1].getHash(); - if (a.compareTo(b) >= 0) { - return false; - } - } - return true; - } - - @Override - public int getRefCount() { - return entries.length; - } - - @SuppressWarnings("unchecked") - @Override - public Ref getRef(int i) { - Ref e = entries[i]; // IndexOutOfBoundsException if out of range - return e; - } - - @Override - public Ref getElementRef(long i) { - Ref e = entries[Utils.checkedInt(i)]; // Exception if out of range - return e; - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public SetLeaf updateRefs(IRefFunction func) { - int n = entries.length; - if (n == 0) return this; - Ref[] newEntries = entries; - for (int i = 0; i < n; i++) { - Ref e = newEntries[i]; - Ref newEntry = (Ref) func.apply(e); - if (e!=newEntry) { - if (newEntries==entries) newEntries=entries.clone(); - newEntries[i]=newEntry; - } - } - if (newEntries==entries) return this; - // Note: we assume no key hashes have changed - return new SetLeaf(newEntries); - } - - /** - * Filters this ListMap to contain only key hashes with the hex digits specified - * in the given Mask - * - * @param digitPos Position of the hex digit to filter - * @param mask Mask of digits to include - * @return Filtered ListMap - */ - public SetLeaf filterHexDigits(int digitPos, int mask) { - mask = mask & 0xFFFF; - if (mask == 0) return Sets.empty(); - if (mask == 0xFFFF) return this; - int sel = 0; - int n = size(); - for (int i = 0; i < n; i++) { - Hash h = entries[i].getHash(); - if ((mask & (1 << h.getHexDigit(digitPos))) != 0) { - sel = sel | (1 << i); // include this index in selection - } - } - if (sel == 0) return Sets.empty(); // no entries selected - return filterEntries(sel); - } - - /** - * Filters entries using the given bit mask - * - * @param selection - * @return - */ - @SuppressWarnings("unchecked") - private SetLeaf filterEntries(int selection) { - if (selection == 0) return Sets.empty(); // no items selected - int n = size(); - if (selection == ((1 << n) - 1)) return this; // all items selected - Ref[] newEntries = new Ref[Integer.bitCount(selection)]; - int ix = 0; - for (int i = 0; i < n; i++) { - if ((selection & (1 << i)) != 0) { - newEntries[ix++] = entries[i]; - } - } - assert (ix == Integer.bitCount(selection)); - return new SetLeaf(newEntries); - } - - @Override - public AHashSet mergeWith(AHashSet b, int setOp) { - return mergeWith(b,setOp,0); - } - - @Override - protected AHashSet mergeWith(AHashSet b, int setOp, int shift) { - if (b instanceof SetLeaf) return mergeWith((SetLeaf) b, setOp,shift); - if (b instanceof SetTree) return b.mergeWith(this, reverseOp(setOp),shift); - throw new TODOException("Unhandled map type: " + b.getClass()); - } - - public AHashSet mergeWith(SetLeaf b, int setOp,int shift) { - if (this.equals(b)) return applySelf(setOp); // no change in identical case - int al = this.size(); - int bl = b.size(); - int ai = 0; - int bi = 0; - ArrayList> results = null; - while ((ai < al) || (bi < bl)) { - Ref ae = (ai < al) ? this.entries[ai] : null; - Ref be = (bi < bl) ? b.entries[bi] : null; - int c = (ae == null) ? 1 : ((be == null) ? -1 : ae.getHash().compareTo(be.getHash())); - - Ref newE; - if (c==0) { - newE= applyOp(setOp,ae,be); - } else if (c<0) { - // apply to a - newE= applyOp(setOp,ae,null); - } else { - // apply to a - newE= applyOp(setOp,null,be); - } - - // Create results arraylist if any difference from this - if ((results == null) && (newE != ((c <= 0) ? ae : null))) { - // create new results array if difference detected - results = new ArrayList<>(2*MAX_ENTRIES); // space for all if needed - // include new entries - for (int i = 0; i < ai; i++) { - results.add(entries[i]); - } - } - if (c <= 0) ai++; // inc ai if we used ae - if (c >= 0) bi++; // inc bi if we used be - if ((results != null) && (newE != null)) results.add(newE); - } - if (results == null) return this; - return Sets.createWithShift(shift,results); - } - - public R reduceValues(BiFunction func, R initial) { - int n = size(); - R result = initial; - for (int i = 0; i < n; i++) { - result = func.apply(result, entries[i].getValue()); - } - return result; - } - - @Override - public boolean equals(ASet a) { - if (!(a instanceof SetLeaf)) return false; - return equals((SetLeaf) a); - } - - public boolean equals(SetLeaf a) { - if (this == a) return true; - int n = size(); - if (n != a.size()) return false; - for (int i = 0; i < n; i++) { - if (!entries[i].equals(a.entries[i])) return false; - } - return true; - } - - @Override - protected void validateWithPrefix(Hash prefix, int digit, int shift) throws InvalidDataException { - for (int i = 0; i < entries.length; i++) { - Ref e = entries[i]; - Hash h = e.getHash(); - long match=h.commonHexPrefixLength(prefix); - if (match<(shift-1)) { - throw new InvalidDataException("Parent prefix did not match",this); - } - int mydigit=h.getHexDigit(shift); - if (mydigit!=digit) { - throw new InvalidDataException("Bad hex digit at position: "+shift,this); - } - e.validate(); - } - } - - @Override - public void validateCell() throws InvalidDataException { - if ((count == 0) && (this != Sets.EMPTY)) { - throw new InvalidDataException("Empty map not using canonical instance", this); - } - - if (count > MAX_ENTRIES) { - throw new InvalidDataException("Too many items in list map: " + entries.length, this); - } - - // validates both key uniqueness and sort order - if (!isCanonical()) { - throw new InvalidDataException("Non-canonical key ordering", this); - } - } - - @Override - public boolean containsAll(ASet b) { - if (this==b) return true; - - // if set is too big, can't possibly contain all keys - if (b.count()>count) return false; - - // must be a setleaf if this size or smaller - return containsAll((SetLeaf)b); - } - - @Override - public boolean isSubset(ASet b) { - return b.containsAll(this); - } - - protected boolean containsAll(SetLeaf b) { - int ix=0; - for (Ref meb:b.entries) { - Hash bh=meb.getHash(); - - if (ix>=count) return false; // no remaining entries in this - while (ix mea=entries[ix]; - Hash ah=mea.getHash(); - int c=ah.compareTo(bh); - if (c<0) { - // ah is smaller than bh - // need to advance ix and try next entry - ix++; - if (ix>=count) return false; // not found - continue; - } else if (c>0) { - return false; // didn't contain the key entry - } else { - // found it, so advance to next entry in b and update ix - ix++; - break; - } - } - } - - return true; - } - - @Override - public AHashSet includeRef(Ref ref) { - return includeRef(ref,0); - } - - @Override - protected AHashSet includeRef(Ref e, int shift) { - int n=entries.length; - Hash h=e.getHash(); - int pos=0; - for (; pos iref=entries[pos]; - int c=h.compareTo(iref.getHash()); - if (c==0) return this; - if (c<0) break; // need to add at this position - } - - // New element must be added at pos - @SuppressWarnings("unchecked") - Ref[] newEntries=new Ref[n+1]; - System.arraycopy(entries, 0, newEntries, 0, pos); - System.arraycopy(entries, pos, newEntries, pos+1, n-pos); - newEntries[pos]=e; - - if (n(newEntries); - } else { - // expand to tree - return SetTree.create(newEntries, shift); - } - } - - @SuppressWarnings("unchecked") - @Override - protected void copyToArray(R[] arr, int offset) { - for (int i=0; i toCanonical() { - if (count<=MAX_ENTRIES) return this; - return SetTree.create(entries, 0); - } - - - - -} diff --git a/convex-core/src/main/java/convex/core/data/SetTree.java b/convex-core/src/main/java/convex/core/data/SetTree.java deleted file mode 100644 index 640dd6962..000000000 --- a/convex-core/src/main/java/convex/core/data/SetTree.java +++ /dev/null @@ -1,655 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; - -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.util.Bits; -import convex.core.util.Utils; - -/** - * Persistent Set for large hash sets requiring tree structure. - * - * Internally implemented as a radix tree, indexed by key hash. Uses an array of - * child Maps, with a bitmap mask indicating which hex digits are present, i.e. - * have non-empty children. - * - * @param Type of set elemets - */ -public class SetTree extends AHashSet { - - /** - * Child maps, one for each present bit in the mask, max 16 - */ - private final Ref>[] children; - - /** - * Shift position of this treemap node in number of hex digits - */ - private final int shift; - - /** - * Mask indicating which hex digits are present in the child array e.g. 0x0001 - * indicates all children are in the '0' digit. e.g. 0xFFFF indicates there are - * children for every digit. - */ - private final short mask; - - private SetTree(Ref>[] blocks, int shift, short mask, long count) { - super(count); - this.children = blocks; - this.shift = shift; - this.mask = mask; - } - - /** - * Computes the total count from an array of Refs to sets. Ignores null Refs in - * child array - * - * @param children - * @return The total count of all child maps - */ - private static long computeCount(Ref>[] children) { - long n = 0; - for (Ref> cref : children) { - if (cref == null) continue; - ASet m = cref.getValue(); - n += m.count(); - } - return n; - } - - @SuppressWarnings("unchecked") - public static SetTree create(Ref[] newEntries, int shift) { - int n = newEntries.length; - if (n <= SetLeaf.MAX_ENTRIES) { - throw new IllegalArgumentException( - "Insufficient distinct entries for TreeMap construction: " + newEntries.length); - } - - // construct full child array - Ref>[] children = new Ref[16]; - for (int i = 0; i < n; i++) { - Ref e = newEntries[i]; - int ix = e.getHash().getHexDigit(shift); - Ref> ref = children[ix]; - if (ref == null) { - children[ix] = SetLeaf.create(e).getRef(); - } else { - AHashSet newChild=ref.getValue().includeRef(e,shift+1); - children[ix] = newChild.getRef(); - } - } - return (SetTree) createFull(children, shift); - } - - /** - * Creates a Tree map given child refs for each digit - * - * @param children An array of children, may refer to nulls or empty maps which - * will be filtered out - * @return - */ - private static AHashSet createFull(Ref>[] children, int shift, long count) { - if (children.length != 16) throw new IllegalArgumentException("16 children required!"); - Ref>[] newChildren = Utils.filterArray(children, a -> { - if (a == null) return false; - AHashSet m = a.getValue(); - return ((m != null) && !m.isEmpty()); - }); - - if (children != newChildren) { - return create(newChildren, shift, Utils.computeMask(children, newChildren), count); - } else { - return create(children, shift, (short) 0xFFFF, count); - } - } - - /** - * Create a MapTree with a full compliment of children. - * @param - * @param - * @param newChildren - * @param shift Shift for child node - * @return - */ - private static AHashSet createFull(Ref>[] newChildren, int shift) { - long count=computeCount(newChildren); - return createFull(newChildren, shift, count); - } - - /** - * Creates a Map with the specified child map Refs. Removes empty maps passed as - * children. - * - * Returns a ListMap for small maps. - * - * @param children Array of Refs to child maps for each bit in mask - * @param shift Shift position (hex digit of key hashes for this map) - * @param mask Mask specifying the hex digits included in the child array at - * this shift position - * @return A new map as specified @ - */ - @SuppressWarnings("unchecked") - private static AHashSet create(Ref>[] children, int shift, short mask, long count) { - int cLen = children.length; - if (Integer.bitCount(mask & 0xFFFF) != cLen) { - throw new IllegalArgumentException( - "Invalid child array length " + cLen + " for bit mask " + Utils.toHexString(mask)); - } - - // compress small counts to SetLeaf - if (count <= SetLeaf.MAX_ENTRIES) { - Ref[] entries = new Ref[Utils.checkedInt(count)]; - int ix = 0; - for (Ref> childRef : children) { - AHashSet child = childRef.getValue(); - long cc = child.count(); - for (long i = 0; i < cc; i++) { - entries[ix++] = child.getElementRef(i); - } - } - assert (ix == count); - return SetLeaf.create(entries); - } - int sel = (1 << cLen) - 1; - short newMask = mask; - for (int i = 0; i < cLen; i++) { - AHashSet child = children[i].getValue(); - if (child.isEmpty()) { - newMask = (short) (newMask & ~(1 << digitForIndex(i, mask))); // remove from mask - sel = sel & ~(1 << i); // remove from selection - } - } - if (mask != newMask) { - return new SetTree(Utils.filterSmallArray(children, sel), shift, newMask, count); - } - return new SetTree(children, shift, mask, count); - } - - @Override - public Ref getElementRef(long i) { - long pos = i; - for (Ref> c : children) { - AHashSet child = c.getValue(); - long cc = child.count(); - if (pos < cc) return child.getElementRef(pos); - pos -= cc; - } - throw new IndexOutOfBoundsException("Entry index: " + i); - } - - @Override - protected Ref getRefByHash(Hash hash) { - int digit = Utils.extractDigit(hash, shift); - int i = Bits.indexForDigit(digit, mask); - if (i < 0) return null; // not present - return children[i].getValue().getRefByHash(hash); - } - - @SuppressWarnings("unchecked") - @Override - public AHashSet exclude(ACell key) { - return excludeRef((Ref) Ref.get(key)); - } - - @Override - public AHashSet excludeRef(Ref keyRef) { - int digit = Utils.extractDigit(keyRef.getHash(), shift); - int i = Bits.indexForDigit(digit, mask); - if (i < 0) return this; // not present - - // dissoc entry from child - AHashSet child = children[i].getValue(); - AHashSet newChild = child.excludeRef(keyRef); - if (child == newChild) return this; // no removal, no change - - AHashSet result=(newChild.isEmpty())?dissocChild(i):replaceChild(i, newChild.getRef()); - return result.toCanonical(); - } - - public AHashSet toCanonical() { - if (count>SetLeaf.MAX_ENTRIES) return this; - int n=Utils.checkedInt(count); - @SuppressWarnings("unchecked") - Ref[] newEntries=new Ref[n]; - for (int i=0; i(newEntries); - } - - @SuppressWarnings("unchecked") - private AHashSet dissocChild(int i) { - int bsize = children.length; - AHashSet child = children[i].getValue(); - Ref>[] newBlocks = (Ref>[]) new Ref[bsize - 1]; - System.arraycopy(children, 0, newBlocks, 0, i); - System.arraycopy(children, i + 1, newBlocks, i, bsize - i - 1); - short newMask = (short) (mask & (~(1 << digitForIndex(i, mask)))); - long newCount = count - child.count(); - return create(newBlocks, shift, newMask, newCount); - } - - @SuppressWarnings("unchecked") - private SetTree insertChild(int digit, Ref> newChild) { - int bsize = children.length; - int i = Bits.positionForDigit(digit, mask); - short newMask = (short) (mask | (1 << digit)); - if (mask == newMask) throw new Error("Digit already present!"); - - Ref>[] newChildren = (Ref>[]) new Ref[bsize + 1]; - System.arraycopy(children, 0, newChildren, 0, i); - System.arraycopy(children, i, newChildren, i + 1, bsize - i); - newChildren[i] = newChild; - long newCount = count + newChild.getValue().count(); - return (SetTree) create(newChildren, shift, newMask, newCount); - } - - /** - * Replaces the child ref at a given index position. Will return this if no change - * - * @param i - * @param newChild - * @return @ - */ - private AHashSet replaceChild(int i, Ref> newChild) { - if (children[i] == newChild) return this; - AHashSet oldChild = children[i].getValue(); - Ref>[] newChildren = children.clone(); - newChildren[i] = newChild; - long newCount = count + newChild.getValue().count() - oldChild.count(); - return create(newChildren, shift, mask, newCount); - } - - public static int digitForIndex(int index, short mask) { - // scan mask for specified index - int found = 0; - for (int i = 0; i < 16; i++) { - if ((mask & (1 << i)) != 0) { - if (found++ == index) return i; - } - } - throw new IllegalArgumentException("Index " + index + " not available in mask map: " + Utils.toHexString(mask)); - } - - @SuppressWarnings("unchecked") - @Override - public SetTree include(ACell value) { - Ref keyRef = (Ref) Ref.get(value); - return includeRef(keyRef, shift); - } - - @Override - protected SetTree includeRef(Ref e, int shift) { - if (this.shift != shift) { - throw new Error("Invalid shift!"); - } - Ref keyRef = e; - int digit = Utils.extractDigit(keyRef.getHash(), shift); - int i = Bits.indexForDigit(digit, mask); - if (i < 0) { - // location not present - AHashSet newChild = SetLeaf.create(e); - return insertChild(digit, newChild.getRef()); - } else { - // location needs update - AHashSet child = children[i].getValue(); - AHashSet newChild = child.includeRef(e, shift + 1); - if (child == newChild) return this; - return (SetTree) replaceChild(i, newChild.getRef()); - } - } - - @Override - public AHashSet includeRef(Ref ref) { - return includeRef(ref,shift); - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.SET; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = Format.writeVLCLong(bs,pos, count); - - bs[pos++] = (byte) shift; - pos = Utils.writeShort(bs, pos,mask); - - int ilength = children.length; - for (int i = 0; i < ilength; i++) { - pos = children[i].encode(bs,pos); - } - return pos; - } - - @Override - public int estimatedEncodingSize() { - // allow space for tag, shift byte byte, 2 byte mask, embedded child refs - return 4 + Format.MAX_EMBEDDED_LENGTH * children.length; - } - - public static int MAX_ENCODING_LENGTH = 4 + Format.MAX_EMBEDDED_LENGTH * 16; - - /** - * Reads a SetTree from the provided ByteBuffer Assumes the header byte and count is - * already read. - * - * @param bb ByteBuffer to read from - * @param count Number of elements - * @return TreeMap instance as read from ByteBuffer - * @throws BadFormatException If encoding is invalid - */ - @SuppressWarnings("unchecked") - public static SetTree read(ByteBuffer bb, long count) throws BadFormatException { - int shift = bb.get(); - short mask = bb.getShort(); - - int ilength = Integer.bitCount(mask & 0xFFFF); - Ref>[] blocks = (Ref>[]) new Ref[ilength]; - - for (int i = 0; i < ilength; i++) { - // need to read as a Ref - Ref> ref = Format.readRef(bb); - blocks[i] = ref; - } - // create directly, we have all values - SetTree result = new SetTree(blocks, shift, mask, count); - if (!result.isValidStructure()) throw new BadFormatException("Problem with TreeMap invariants"); - return result; - } - - @Override - public boolean isCanonical() { - if (count <= MapLeaf.MAX_ENTRIES) return false; - return true; - } - - @Override public final boolean isCVMValue() { - return shift==0; - } - - @Override - public int getRefCount() { - return children.length; - } - - @SuppressWarnings("unchecked") - @Override - public Ref getRef(int i) { - return (Ref) children[i]; - } - - @SuppressWarnings("unchecked") - @Override - public SetTree updateRefs(IRefFunction func) { - int n = children.length; - if (n == 0) return this; - Ref>[] newChildren = children; - for (int i = 0; i < n; i++) { - Ref> child = children[i]; - Ref> newChild = (Ref>) func.apply(child); - if (child != newChild) { - if (children == newChildren) { - newChildren = children.clone(); - } - newChildren[i] = newChild; - } - } - if (newChildren == children) return this; - // Note: we assume no key hashes have changed, so structure is the same - return new SetTree<>(newChildren, shift, mask, count); - } - - @Override - public AHashSet mergeWith(AHashSet b, int setOp) { - return mergeWith(b, setOp, this.shift); - } - - @Override - protected AHashSet mergeWith(AHashSet b, int setOp, int shift) { - if ((b instanceof SetTree)) { - SetTree bt = (SetTree) b; - if (this.shift != bt.shift) throw new Error("Misaligned shifts!"); - return mergeWith(bt, setOp, shift); - } - if ((b instanceof SetLeaf)) return mergeWith((SetLeaf) b, setOp, shift); - throw new Error("Unrecognised map type: " + b.getClass()); - } - - @SuppressWarnings("unchecked") - private AHashSet mergeWith(SetTree b, int setOp, int shift) { - // assume two TreeMaps with identical prefix and shift - assert (b.shift == shift); - int fullMask = mask | b.mask; - // We are going to build full child list only if needed - Ref>[] newChildren = null; - for (int digit = 0; digit < 16; digit++) { - int bitMask = 1 << digit; - if ((fullMask & bitMask) == 0) continue; // nothing to merge at this index - AHashSet ac = childForDigit(digit).getValue(); - AHashSet bc = b.childForDigit(digit).getValue(); - AHashSet rc = ac.mergeWith(bc, setOp, shift + 1); - if (ac != rc) { - if (newChildren == null) { - newChildren = (Ref>[]) new Ref[16]; - for (int ii = 0; ii < digit; ii++) { // copy existing children up to this point - int chi = Bits.indexForDigit(ii, mask); - if (chi >= 0) newChildren[ii] = children[chi]; - } - } - } - if (newChildren != null) newChildren[digit] = rc.getRef(); - } - if (newChildren == null) return this; - return createFull(newChildren, shift); - } - - @SuppressWarnings("unchecked") - private AHashSet mergeWith(SetLeaf b, int setOp, int shift) { - Ref>[] newChildren = null; - int ix = 0; - for (int i = 0; i < 16; i++) { - int imask = (1 << i); // mask for this digit - if ((mask & imask) == 0) continue; - Ref> cref = children[ix++]; - AHashSet child = cref.getValue(); - SetLeaf bSubset = b.filterHexDigits(shift, imask); // filter only relevant elements in b - AHashSet newChild = child.mergeWith(bSubset, setOp, shift + 1); - if (child != newChild) { - if (newChildren == null) { - newChildren = (Ref>[]) new Ref[16]; - for (int ii = 0; ii < children.length; ii++) { // copy existing children - int chi = digitForIndex(ii, mask); - newChildren[chi] = children[ii]; - } - } - } - if (newChildren != null) { - newChildren[i] = newChild.getRef(); - } - } - assert (ix == children.length); - // if any new children created, create a new Map, else use this - AHashSet result = (newChildren == null) ? this : createFull(newChildren, shift); - - SetLeaf extras = b.filterHexDigits(shift, ~mask); - int en = extras.size(); - for (int i = 0; i < en; i++) { - Ref e = extras.getRef(i); - Ref newE = applyOp(setOp,null,e); - if (newE != null) { - // include only new keys where function result is not null. Re-use existing - // entry if possible. - result = result.includeRef(newE, shift); - } - } - return result; - } - - - - /** - * Gets the Ref for the child at the given digit, or an empty map if not found - * - * @param digit The hex digit to query at this TreeMap's shift position - * @return The child map for this digit, or an empty map if the child does not - * exist - */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - private Ref> childForDigit(int digit) { - int ix = Bits.indexForDigit(digit, mask); - if (ix < 0) return (Ref)Sets.emptyRef(); - return children[ix]; - } - - @Override - public boolean equals(ASet a) { - if (!(a instanceof SetTree)) return false; - return equals((SetTree) a); - } - - boolean equals(SetTree b) { - if (this == b) return true; - long n = count; - if (n != b.count) return false; - if (mask != b.mask) return false; - if (shift != b.shift) return false; - - // Fall back to comparing hashes. Probably most efficient in general. - if (getHash().equals(b.getHash())) return true; - return false; - } - - @Override - public void validate() throws InvalidDataException { - super.validate(); - - if (mask == 0) throw new InvalidDataException("TreeMap must have children!", this); - if ((shift <0)||(shift>MAX_SHIFT)) { - throw new InvalidDataException("Invalid shift for SetTree", this); - } - - if (count<=SetLeaf.MAX_ENTRIES) { - throw new InvalidDataException("Count too small [" + count + "] for SetTree", this); - } - - Hash firstHash; - try { - firstHash=getElementRef(0).getHash(); - } catch (ClassCastException e) { - throw new InvalidDataException("Bad child type:" +e.getMessage(), this); - } - - int bsize = children.length; - - long childCount=0;; - for (int i = 0; i < bsize; i++) { - if (children[i] == null) { - throw new InvalidDataException("Null child ref at index " + i,this); - } - - ACell o = children[i].getValue(); - if (!(o instanceof AHashMap)) { - throw new InvalidDataException( - "Expected AHashSet child at index " + i +" but got "+Utils.getClassName(o), this); - } - @SuppressWarnings("unchecked") - AHashSet child = (AHashSet) o; - if (child.isEmpty()) - throw new InvalidDataException("Empty child at index " + i,this); - - if (child instanceof SetTree) { - SetTree childTree=(SetTree) child; - int expectedShift=shift+1; - if (childTree.shift!=expectedShift) { - throw new InvalidDataException("Wrong child shift ["+childTree.shift+"], expected ["+expectedShift+"]",this); - } - } - - Hash childHash=child.getElementRef(0).getHash(); - long pmatch=firstHash.commonHexPrefixLength(childHash); - if (pmatch b) { - if (b instanceof SetTree) { - return containsAll((SetTree)b); - } - // must be a SetLeaf - long n=b.count; - for (long i=0; i me=b.getElementRef(i); - if (!this.containsHash(me.getHash())) return false; - } - return true; - } - - protected boolean containsAll(SetTree map) { - // fist check this mask contains all of target mask - if ((this.mask|map.mask)!=this.mask) return false; - - for (int i=0; i<16; i++) { - Ref> child=this.childForDigit(i); - if (child==null) continue; - - Ref> mchild=map.childForDigit(i); - if (mchild==null) continue; - - if (!(child.getValue().containsAll(mchild.getValue()))) return false; - } - return true; - } - - @Override - public Ref getValueRef(ACell k) { - return getRefByHash(Hash.compute(k)); - } - - @Override - protected void copyToArray(R[] arr, int offset) { - for (int i=0; i child=children[i].getValue(); - child.copyToArray(arr,offset); - offset=Utils.checkedInt(offset+child.count()); - } - } - - @Override - public boolean containsHash(Hash hash) { - return getRefByHash(hash)!=null; - } -} diff --git a/convex-core/src/main/java/convex/core/data/Sets.java b/convex-core/src/main/java/convex/core/data/Sets.java deleted file mode 100644 index f032713cc..000000000 --- a/convex-core/src/main/java/convex/core/data/Sets.java +++ /dev/null @@ -1,106 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collection; - -import convex.core.exceptions.BadFormatException; -import convex.core.lang.RT; -import convex.core.util.Utils; - -public class Sets { - - static final Ref[] EMPTY_ENTRIES = new Ref[0]; - - @SuppressWarnings({ "rawtypes", "unchecked" }) - static final SetLeaf EMPTY = new SetLeaf(EMPTY_ENTRIES); - - static { - // Set empty Ref flags as internal embedded constant - EMPTY.getRef().setFlags(Ref.INTERNAL_FLAGS); - } - - @SuppressWarnings("rawtypes") - public static final Ref EMPTY_REF = EMPTY.getRef(); - - @SuppressWarnings("unchecked") - public static SetLeaf empty() { - return (SetLeaf) EMPTY; - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static Ref> emptyRef() { - return (Ref)EMPTY_REF; - } - - @SuppressWarnings("unchecked") - @SafeVarargs - public static ASet of(Object... elements) { - int n=elements.length; - ASet result=empty(); - for (int i=0; i) result.conj(RT.cvm(elements[i])); - } - return result; - } - - @SuppressWarnings("unchecked") - @SafeVarargs - public static ASet of(ACell... elements) { - int n=elements.length; - ASet result=empty(); - for (int i=0; i) result.conj(elements[i]); - } - return result; - } - - /** - * Creates a set of all the elements in the given data structure - * - * @param Type of elements - * @param source Source for elements - * @return A Set - */ - @SuppressWarnings("unchecked") - public static ASet create(ADataStructure source) { - if (source instanceof ASet) return (ASet) source; - - if (source instanceof AMap) { - ASequence seq = RT.sequence(source); // should always be non-null - return Sets.create(seq); - } - if (source instanceof ACollection) return Sets.fromCollection((Collection) source); - throw new IllegalArgumentException("Unexpected type!" + Utils.getClass(source)); - } - - /** - * Creates a set of all the elements in the given data structure - * - * @param Type of elements - * @param source Source for elements - * @return A Set - */ - public static ASet fromCollection(Collection source) { - return Sets.of(source.toArray()); - } - - public static ASet read(ByteBuffer bb) throws BadFormatException { - long count = Format.readVLCLong(bb); - if (count <= SetLeaf.MAX_ENTRIES) { - return SetLeaf.read(bb, count); - } else { - return SetTree.read(bb, count); - } - } - - public static AHashSet createWithShift(int shift, ArrayList> values) { - AHashSet result=Sets.empty(); - for (Ref v: values) { - result=result.includeRef(v, shift); - } - return result; - } - - -} diff --git a/convex-core/src/main/java/convex/core/data/SignedData.java b/convex-core/src/main/java/convex/core/data/SignedData.java deleted file mode 100644 index a5b5f4025..000000000 --- a/convex-core/src/main/java/convex/core/data/SignedData.java +++ /dev/null @@ -1,306 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; - -import convex.core.crypto.AKeyPair; -import convex.core.crypto.ASignature; -import convex.core.crypto.Ed25519Signature; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.BadSignatureException; -import convex.core.exceptions.InvalidDataException; -import convex.core.transactions.ATransaction; - -/** - * Node representing a signed data object. - * - * A signed data object encapsulates: - *
    - *
  • An Address that identifies the signer
  • - *
  • A digital signature
  • - *
  • An underlying Cell that has been signed.
  • - *
- * - * The SignedData instance is considered valid if the signature can be successfully validated for - * the given Address and data value, and if so can be taken as a cryptographic proof that the signature - * was created by someone in possession of the corresponding private key. - * - * Note we currently go via a Ref here for a few reasons: - It guarantees we - * have a hash for signing - It makes the SignedData object - * implementation/representation independent of the value type - It creates a - * possibility of structural sharing for transaction values excluding signatures - * - * Binary representation: - *
    - *
  1. 1 byte - Tag.SIGNED_DATA tag
  2. - *
  3. 32 bytes - Public Key of signer
  4. - *
  5. 64 bytes - raw Signature data
  6. - *
  7. 1+ bytes - Data Value Ref (may be embedded)
  8. - *
- * - * SECURITY: signing requires presence of a local keypair TODO: SECURITY: any - * persistence forces validation of Signature?? - * - * @param The type of the signed object - */ -public class SignedData extends ACell { - // Encoded fields - private final AccountKey publicKey; - private final ASignature signature; - private final Ref valueRef; - - private SignedData(Ref refToValue, AccountKey address, ASignature sig) { - this.valueRef = refToValue; - this.publicKey = address; - signature = sig; - } - - /** - * Signs a data value Ref with the given keypair. - * - * SECURITY: Marks as already validated, since we just signed it. - * - * @param keyPair The public/private key pair of the signer. - * @param ref Ref to the data to sign - * @return SignedData object signed with the given key-pair - */ - public static SignedData createWithRef(AKeyPair keyPair, Ref ref) { - ASignature sig = keyPair.sign(ref.getHash()); - SignedData sd = new SignedData(ref, keyPair.getAccountKey(), sig); - sd.markValidated(); - return sd; - } - - /** - * Mark this SignedData as already verified as good - cache in Ref - */ - private void markValidated() { - Ref ref=getRef(); - int flags=ref.getFlags(); - if ((flags&Ref.VERIFIED_MASK)!=0) return; // already done - cachedRef=ref.withFlags(flags|Ref.VERIFIED_MASK); - } - - /** - * Mark this SignedData as a bad signature - cache in Ref - */ - private void markBadSignature() { - Ref ref=getRef(); - int flags=ref.getFlags(); - if ((flags&Ref.BAD_MASK)!=0) return; // already done - cachedRef=ref.withFlags(flags|Ref.BAD_MASK); - } - - - public static SignedData create(AKeyPair keyPair, T value2) { - return createWithRef(keyPair, Ref.get(value2)); - } - - /** - * Creates a SignedData object with the given parameters. - * - * SECURITY: Not assumed to be valid. - * - * @param address Public Address of the signer - * @param sig Signature of the supplied data - * @param ref Ref to the data that has been signed - * @return A new SignedData object - */ - public static SignedData create(AccountKey address, ASignature sig, Ref ref) { - // boolean check=Sign.verify(ref.getHash(), sig, address); - // if (!check) throw new ValidationException("Invalid signature: "+sig); - return new SignedData(ref, address, sig); - } - - - public static SignedData create(AKeyPair kp, ASignature sig, Ref ref) { - - return create(kp.getAccountKey(),sig,ref); - } - - - /** - * Gets the signed value object encapsulated by this SignedData object. - * - * Does not check Signature. - * - * @return Data value that has been signed - */ - public T getValue() { - return valueRef.getValue(); - } - - - /** - * Gets the public key of the signer. If the signature is valid, this - * represents a cryptographic proof that the signer was in possession of the - * private key of this address. - * - * @return Public Key of signer. - */ - public AccountKey getAccountKey() { - return publicKey; - } - - /** - * Gets the Signature that formed part of this SignedData object - * - * @return Signature instance - */ - public ASignature getSignature() { - return signature; - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.SIGNED_DATA; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = publicKey.encodeRaw(bs,pos); - pos = signature.encodeRaw(bs,pos); - pos = valueRef.encode(bs,pos); - return pos; - } - - @Override - public int estimatedEncodingSize() { - return 1+AccountKey.LENGTH+Ed25519Signature.SIGNATURE_LENGTH+Format.MAX_EMBEDDED_LENGTH; - } - - /** - * Reads a SignedData instance from the given ByteBuffer - * - * @param data A ByteBuffer containing - * @return A SignedData object - * @throws BadFormatException If encoding is invalid - */ - public static SignedData read(ByteBuffer data) throws BadFormatException { - // header already assumed to be consumed - AccountKey address = AccountKey.readRaw(data); - ASignature sig = ASignature.read(data); - Ref value = Format.readRef(data); - return create(address, sig, value); - } - - /** - * Validates the signature in this SignedData instance. Caches result - * - * @return true if valid, false otherwise - */ - public boolean checkSignature() { - Ref> sigRef=getRef(); - int flags=sigRef.getFlags(); - if ((flags&Ref.BAD_MASK)!=0) return false; - if ((flags&Ref.VERIFIED_MASK)!=0) return true; - - Hash hash=valueRef.getHash(); - boolean check = signature.verify(hash, publicKey); - - if (check) { - markValidated(); - } else { - markBadSignature(); - } - return check; - } - - /** - * Checks if the signature has already gone through verification. MAy or may - * not be a valid signature. - * - * @return true if valid, false otherwise - */ - public boolean isSignatureChecked() { - Ref> sigRef=getRef(); - if (sigRef==null) return false; - int flags=sigRef.getFlags(); - return (flags&(Ref.BAD_MASK|Ref.VERIFIED_MASK))!=0; - } - - public void validateSignature() throws BadSignatureException { - if (!checkSignature()) throw new BadSignatureException("Signature not valid!", this); - } - - @Override - public boolean isCanonical() { - return true; - } - - @Override public final boolean isCVMValue() { - return false; - } - - @Override - public int getRefCount() { - // Value Ref only - return 1; - } - - @SuppressWarnings("unchecked") - @Override - public Ref getRef(int i) { - if (i != 0) throw new IndexOutOfBoundsException("Illegal SignedData ref index: " + i); - return (Ref) valueRef; - } - - @Override - public SignedData updateRefs(IRefFunction func) { - @SuppressWarnings("unchecked") - Ref newValueRef = (Ref) func.apply(valueRef); - if (valueRef == newValueRef) return this; - - // SECURITY: preserve verification flags - SignedData newSD= new SignedData(newValueRef, publicKey, signature); - newSD.cachedRef=newSD.getRef().withFlags(getRef().getFlags()); - return newSD; - } - - @Override - public void print(StringBuilder sb) { - sb.append("{"); - sb.append(":signed "+valueRef.getHash().toString()); - sb.append("}"); - } - - @Override - public void validate() throws InvalidDataException { - super.validate(); - } - - @Override - public void validateCell() throws InvalidDataException { - publicKey.validate(); - signature.validate(); - valueRef.validate(); - } - - public Ref getDataRef() { - return valueRef; - } - - /** - * SignedData is not embedded. We want to persist in store always to cache verification status - * - * @return Always false - */ - public boolean isEmbedded() { - return false; - } - - @Override - public String toString() { - return "{:signed "+getValue()+"}"; - } - - @Override - public byte getTag() { - return Tag.SIGNED_DATA; - } - - @Override - public ACell toCanonical() { - return this; - } -} diff --git a/convex-core/src/main/java/convex/core/data/StringShort.java b/convex-core/src/main/java/convex/core/data/StringShort.java deleted file mode 100644 index 3623655f3..000000000 --- a/convex-core/src/main/java/convex/core/data/StringShort.java +++ /dev/null @@ -1,163 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.nio.CharBuffer; - -import convex.core.exceptions.InvalidDataException; -import convex.core.util.Utils; - -/** - * Class representing a short CVM string. - */ -public class StringShort extends AString { - - /** - * Length of longest StringShort value that is embedded in chars - * - * Just long enough for a 64-char hex string with 0x and 2 delimiters. If that helps. - */ - public static final int MAX_EMBEDDED_STRING_LENGTH=68; - - /** - * Length of longest StringShort value in chars - */ - public static final int MAX_LENGTH=1024; - - - private String data; - - protected StringShort(String data) { - super(data.length()); - this.data=data; - } - - /** - * Creates a StringShort instance from a regular Java String - * - * @param string String to wrap as StringShort - * @return StringShort instance, or null if String is of invalid size - */ - public static StringShort create(String string) { - int len=string.length(); - if ((len<0)||(len>MAX_LENGTH)) return null; - return new StringShort(string); - } - - - @Override - public char charAt(int index) { - return data.charAt(index); - } - - @Override - public StringShort subSequence(int start, int end) { - if ((start<0)||(end>length)) throw new IndexOutOfBoundsException("Out of range subSerqnce "+start+","+end); - if (endMAX_LENGTH) throw new InvalidDataException("StringShort too long: " +length,this); - if (length!=data.length()) throw new InvalidDataException("Wrong String length!",this); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos=Format.writeVLCLong(bs,pos, length); - - int n=data.length(); - for (int i=0; islen)) throw new IllegalArgumentException("Out of range"); - return new StringSlice(source,start,len); - } - - @Override - public char charAt(int index) { - return source.charAt(index-start); - } - - @Override - public AString subSequence(int start, int end) { - int len=end-start; - if (len==0) return Strings.EMPTY; - if (len<0) throw new IllegalArgumentException("Negative length"); - if ((start<0)||(start+len>=length)) throw new IllegalArgumentException("Out of range"); - if ((start==0)&&(len==length)) return this; - return source.subSequence(this.start+start, this.start+end); - } - - @Override - public void validateCell() throws InvalidDataException { - // Nothing? - - } - - @Override - public int encode(byte[] bs, int pos) { - throw new UnsupportedOperationException(""); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - throw new UnsupportedOperationException(""); - } - - @Override - public int estimatedEncodingSize() { - return 100; - } - - @Override - public boolean isCanonical() { - return false; - } - - @Override public final boolean isCVMValue() { - return false; - } - - @Override - public int getRefCount() { - return 0; - } - - - @Override - protected void appendToStringBuffer(StringBuilder sb, int start,int length) { - int sourceStart=this.start+start; - source.appendToStringBuffer(sb, sourceStart, length); - } - - @Override - protected AString append(char charValue) { - StringBuilder sb=new StringBuilder(); - appendToStringBuffer(sb, 0, length); - sb.append(charValue); - return Strings.create(sb.toString()); - } - - @Override - public AString toCanonical() { - return Strings.create(toString()); - } - - -} diff --git a/convex-core/src/main/java/convex/core/data/StringTree.java b/convex-core/src/main/java/convex/core/data/StringTree.java deleted file mode 100644 index a69413c91..000000000 --- a/convex-core/src/main/java/convex/core/data/StringTree.java +++ /dev/null @@ -1,190 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; - -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.util.Bits; - -public class StringTree extends AString { - - public static final int MINIMUM_LENGTH=StringShort.MAX_LENGTH+1; - - public static final int BASE_SHIFT = 10; - public static final int BIT_SHIFT_PER_LEVEL = 4; - public static final int FANOUT = 1 << BIT_SHIFT_PER_LEVEL; - - private final Ref[] children; - private final int shift; - - protected StringTree(int length,Ref[] children) { - super(length); - this.children=children; - this.shift=calcShift(length); - } - - protected static int calcShift(int length) { - if (length<=0) throw new IllegalArgumentException("Illegal length: "+length); - - int bitCount=32-Bits.leadingZeros(length-1)-1; - - int shift=BASE_SHIFT+(Math.floorDiv(bitCount-BASE_SHIFT, BIT_SHIFT_PER_LEVEL)*BIT_SHIFT_PER_LEVEL); - if (shift[] children = (Ref[]) new Ref[n]; - for (int i = 0; i < n; i++) { - int start=i*childSize; - AString child=Strings.create(s.substring(start, Math.min(len, start+childSize))); - Ref ref = child.getRef(); - children[i] = ref; - } - return new StringTree(len,children); - } - - private int childIndexAt(int index) { - int ci=index>>shift; - return ci; - } - - @Override - public char charAt(int index) { - int ci=index>>shift; - int cix=index-ci*childSize(); - return children[ci].getValue().charAt(cix); - } - - @Override - public AString subSequence(int start, int end) { - return StringSlice.create(this,start,end-start); - } - - @Override - public void validateCell() throws InvalidDataException { - // TODO Auto-generated method stub - - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos=Format.writeVLCLong(bs,pos, length); - int n=children.length; - for (int i=0; i[] children = (Ref[]) new Ref[n]; - for (int i = 0; i < n; i++) { - Ref ref = Format.readRef(bb); - children[i] = ref; - } - - return new StringTree(length,children); - } - - protected static int calcChildCount(int length, int shift) { - return ((length-1)>>shift)+1; - } - - @Override - public int estimatedEncodingSize() { - // Usually all children will be non-embedded Refs - return 10+Ref.INDIRECT_ENCODING_LENGTH*children.length; - } - - @Override - public boolean isCanonical() { - return true; - } - - @Override public final boolean isCVMValue() { - return true; - } - - @Override - public int getRefCount() { - return children.length; - } - - @SuppressWarnings("unchecked") - @Override - public Ref getRef(int i) { - int ic = children.length; - if (i < 0) throw new IndexOutOfBoundsException("Negative Ref index: " + i); - if (i < ic) return (Ref) children[i]; - throw new IndexOutOfBoundsException("Ref index out of range: " + i); - } - - @SuppressWarnings("unchecked") - @Override - public StringTree updateRefs(IRefFunction func) { - int ic = children.length; - Ref[] newChildren = children; - for (int i = 0; i < ic; i++) { - Ref current = children[i]; - Ref newChild = (Ref) func.apply(current); - - if (newChild!=current) { - if (children==newChildren) newChildren=children.clone(); - newChildren[i] = newChild; - } - } - if (newChildren==children) return this; // no change, safe to return this - return new StringTree(length,newChildren); - } - - @Override - protected void appendToStringBuffer(StringBuilder sb, int start, int length) { - int cstart=childIndexAt(start); - int cend=childIndexAt(start+length-1); - int csize=childSize(); - for (int i=cstart; i<=cend; i++) { - AString child=children[i].getValue(); - - // compute indexes indo child - int c0=Math.max(0, start-i*csize); - int c1=Math.min(child.length, start+length-i*csize); - child.appendToStringBuffer(sb, c0, c1-c0); - } - } - - @Override - protected AString append(char charValue) { - // TODO: SECURITY: needs to be O(1) - StringBuilder sb=new StringBuilder(); - appendToStringBuffer(sb, 0, length); - sb.append(charValue); - return Strings.create(sb.toString()); - } - - @Override - public StringTree toCanonical() { - return this; - } - - -} diff --git a/convex-core/src/main/java/convex/core/data/Strings.java b/convex-core/src/main/java/convex/core/data/Strings.java deleted file mode 100644 index 0b1f053be..000000000 --- a/convex-core/src/main/java/convex/core/data/Strings.java +++ /dev/null @@ -1,42 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; - -import convex.core.exceptions.BadFormatException; - -public class Strings { - - public static final StringShort EMPTY = StringShort.create(""); - public static final StringShort NIL = StringShort.create("nil"); - - /** - * Reads a CVM String value from a bytebuffer. Assumes tag already read. - * - * @param bb ByteBuffer to read from - * @return String instance - * @throws BadFormatException If format has problems - */ - public static AString read(ByteBuffer bb) throws BadFormatException { - long length=Format.readVLCLong(bb); - if (length==0) return EMPTY; - if (length<0) throw new BadFormatException("Negative string length!"); - if (length>Integer.MAX_VALUE) throw new BadFormatException("String length too long! "+length); - if (length<=StringShort.MAX_LENGTH) { - return StringShort.read((int)length,bb); - } - return StringTree.read((int)length,bb); - } - - public static AString create(String s) { - int len=s.length(); - if (len==0) return EMPTY; - if (len<=StringShort.MAX_LENGTH) { - return StringShort.create(s); - } - return StringTree.create(s); - } - - public static AString empty() { - return EMPTY; - } -} diff --git a/convex-core/src/main/java/convex/core/data/Symbol.java b/convex-core/src/main/java/convex/core/data/Symbol.java deleted file mode 100644 index b46a2c514..000000000 --- a/convex-core/src/main/java/convex/core/data/Symbol.java +++ /dev/null @@ -1,147 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.util.WeakHashMap; - -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; - -/** - *

Class representing a Symbol. Symbols are more commonly used in CVM code to refer to functions and values in the - * execution environment.

- * - *

Symbols are simply small immutable data Objects, and can be used freely in data structures. They can be used as map - * keys, however for most normal circumstances Strings or Keywords are more appropriate as keys. - *

- * - *

- * A Symbol comprises: - * - A name - *

- * - *

- * "Becoming sufficiently familiar with something is a substitute for - * understanding it." - John Conway - *

- */ -public class Symbol extends ASymbolic { - - private Symbol(String name) { - super(name); - } - - public AType getType() { - return Types.SYMBOL; - } - - protected static final WeakHashMap cache=new WeakHashMap<>(100); - - /** - * Creates a Symbol with the given name - * @param name Symbol name - * @return Symbol instance, or null if the Symbol is invalid - */ - public static Symbol create(String name) { - if (!validateName(name)) return null; - Symbol sym= new Symbol(name); - - synchronized (cache) { - // TODO: figure out if caching Symbols is a net win or not - Symbol cached=cache.get(name); - if (cached!=null) return cached; - cache.put(name,sym); - } - - return sym; - } - - /** - * Creates a Symbol with the given name. Must be an unqualified name. - * - * @param name Name for Symbol - * @return Symbol instance, or null if the name is invalid for a Symbol. - */ - public static Symbol create(AString name) { - if (name==null) return null; - return create(name.toString()); - } - - @Override - public boolean equals(ACell o) { - if (o instanceof Symbol) return equals((Symbol) o); - return false; - } - - /** - * Tests if this Symbol is equal to another Symbol. Equality is defined by both namespace and name being equal. - * @param sym Symbol to compare with - * @return true if Symbols are equal, false otherwise - */ - public boolean equals(Symbol sym) { - return sym.name.equals(name); - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.SYMBOL; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = Format.writeRawUTF8String(bs, pos, name.toString()); - return pos; - } - - /** - * Reads a Symbol from the given ByteBuffer, assuming tag already consumed - * - * @param bb ByteBuffer source - * @return The Symbol read - * @throws BadFormatException If a Symbol could not be read correctly. - */ - public static Symbol read(ByteBuffer bb) throws BadFormatException { - String name=Format.readUTF8String(bb); - Symbol sym = Symbol.create(name); - if (sym == null) throw new BadFormatException("Can't read symbol"); - return sym; - } - - @Override - public boolean isCanonical() { - // Always canonical - return true; - } - - @Override - public void print(StringBuilder sb) { - sb.append(getName()); - } - - @Override - public int estimatedEncodingSize() { - return 50; - } - - @Override - public void validateCell() throws InvalidDataException { - super.validateCell(); - } - - @Override - public int getRefCount() { - return 0; - } - - @Override - public byte getTag() { - return Tag.SYMBOL; - } - - @Override - public ACell toCanonical() { - return this; - } -} diff --git a/convex-core/src/main/java/convex/core/data/Syntax.java b/convex-core/src/main/java/convex/core/data/Syntax.java deleted file mode 100644 index 4e354628d..000000000 --- a/convex-core/src/main/java/convex/core/data/Syntax.java +++ /dev/null @@ -1,336 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; - -import convex.core.data.prim.CVMLong; -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.RT; -import convex.core.util.Utils; - -/** - * Class representing a Syntax Object. - * - * A Syntax Object wraps: - *
    - *
  • A Form (which may contain nested Syntax Objects)
  • - *
  • Metadata for the Syntax Object, which may be any arbitrary hashmap
  • - *
- * - * Syntax Objects may not wrap another Syntax Object directly, but may contain nested - * Syntax Objects within data structures. - * - * Inspired by Racket. - * - */ -public class Syntax extends ACell { - public static final Syntax EMPTY = create(null, null); - - /** - * Ref to the unwrapped datum value. Cannot refer to another Syntax object - */ - private final Ref datumRef; - - /** - * Metadata map - * If empty, gets encoded as null in byte encoding - */ - private final AHashMap meta; - - private Syntax(Ref datumRef, AHashMap props) { - this.datumRef = datumRef; - this.meta = props; - } - - public AType getType() { - return Types.SYNTAX; - } - - public static Syntax createUnchecked(ACell value, AHashMap meta) { - return new Syntax(Ref.get(value),meta); - } - - /** - * Wraps a value as a Syntax Object, adding the given new metadata - * - * @param value Value to wrap in Syntax Object - * @param meta Metadata to merge, may be null - * @return Syntax instance - */ - public static Syntax create(ACell value, AHashMap meta) { - if (value instanceof Syntax) { - Syntax stx=((Syntax) value); - if (meta==null) return stx; - return stx.mergeMeta(meta); - } - if (meta==null) meta=Maps.empty(); - - return new Syntax(Ref.get(value), meta); - } - - /** - * Wraps a value as a Syntax Object with empty metadata. Does not change existing Syntax objects. - * - * @param value Any CVM value - * @return Syntax instance - */ - public static Syntax create(ACell value) { - if (value instanceof Syntax) return (Syntax) value; - return create(value, Maps.empty()); - } - - /** - * Wraps a value as a Syntax Object with empty metadata. Does not change existing Syntax objects. - * - * @param value Any value, will be converted to valid CVM type - * @return Syntax instance - */ - public static Syntax of(ACell value) { - return create(value); - } - - /** - * Create a Syntax Object with the given value. Converts to appropriate CVM type as a convenience - * - * @param value Value to wrap - * @return Syntax instance - */ - public static Syntax of(Object value) { - return create(RT.cvm(value)); - } - - /** - * Gets the value datum from this Syntax Object - * - * @param Expected datum type from Syntax object - * @return Value datum - */ - @SuppressWarnings("unchecked") - public R getValue() { - return (R) datumRef.getValue(); - } - - /** - * Gets the metadata for this syntax object. May be empty, but never null. - * - * @return Metadata for this Syntax Object as a hashmap - */ - public AHashMap getMeta() { - return meta; - } - - public Long getStart() { - Object v= meta.get(Keywords.START); - if (v instanceof CVMLong) return ((CVMLong)v).longValue(); - return null; - } - - public Long getEnd() { - Object v= meta.get(Keywords.END); - if (v instanceof CVMLong) return ((CVMLong)v).longValue(); - return null; - } - - public String getSource() { - Object v= meta.get(Keywords.SOURCE); - if (v instanceof AString) return v.toString(); - return null; - } - - @Override - public boolean isCanonical() { - return true; - } - - @Override public final boolean isCVMValue() { - return true; - } - - public static Syntax read(ByteBuffer bb) throws BadFormatException { - Ref datum = Format.readRef(bb); - AHashMap props = Format.read(bb); - if (props == null) { - props = Maps.empty(); // we encode empty props as null for efficiency - } else { - if (props.isEmpty()) { - throw new BadFormatException("Empty Syntax metadata should be encoded as nil"); - } - } - return new Syntax(datum, props); - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.SYNTAX; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos=datumRef.encode(bs,pos); - // encode empty props as null for efficiency - if (meta.isEmpty()) { - bs[pos++]=Tag.NULL; - } else { - pos=meta.encode(bs,pos); - } - return pos; - } - - @Override - public void print(StringBuilder sb) { - if (meta==null) { - sb.append("^{} "); - } else { - sb.append('^'); - meta.print(sb); - sb.append(' '); - } - Utils.print(sb, datumRef.getValue()); - } - - @Override - public String toString() { - return print(); - } - - @Override - public void validateCell() throws InvalidDataException { - if (datumRef == null) throw new InvalidDataException("null datum ref", this); - if (meta == null) throw new InvalidDataException("null metadata", this); - meta.validateCell(); - } - - @Override - public void validate() throws InvalidDataException { - super.validate(); - if (datumRef.getValue() instanceof Syntax) { - throw new InvalidDataException("Cannot double-wrap a Syntax value",this); - } - } - - @Override - public int estimatedEncodingSize() { - return 1+2*Format.MAX_EMBEDDED_LENGTH; - } - - @Override - public int getRefCount() { - return 1 + meta.getRefCount(); - } - - @SuppressWarnings("unchecked") - @Override - public Ref getRef(int i) { - if (i == 0) return (Ref) datumRef; - return meta.getRef(i - 1); - } - - - @Override - public Syntax updateRefs(IRefFunction func) { - @SuppressWarnings("unchecked") - Ref newDatum = (Ref)func.apply(datumRef); - AHashMap newMeta = meta.updateRefs(func); - if ((datumRef == newDatum) && (meta == newMeta)) return this; - return new Syntax(newDatum, newMeta); - } - - /** - * Merges metadata into this syntax object, overriding existing metadata - * - * @param additionalMetadata Extra metadata to merge - * @return Syntax Object with updated metadata - */ - public Syntax mergeMeta(AHashMap additionalMetadata) { - AHashMap mm = meta; - mm = mm.merge(additionalMetadata); - return this.withMeta(mm); - } - - /** - * Merge metadata into a Cell, after wrapping as a Syntax Object - * - * @param original Cell to enhance with merged metadata - * @param additional Syntax Object containing additional metadata. Any value will be ignored. - * @return Syntax object with merged metadata - */ - public static Syntax mergeMeta(ACell original, Syntax additional) { - Syntax x=Syntax.create(original); - if (additional!=null) { - x=x.mergeMeta(additional.getMeta()); - } - return x; - } - - /** - * Replaces metadata on this Syntax Object. Old metadata is discarded. - * - * @param newMetadata New metadata map - * @return Syntax Object with updated metadata - */ - public Syntax withMeta(AHashMap newMetadata) { - if (meta == newMetadata) return this; - return new Syntax(datumRef, newMetadata); - } - - /** - * Removes all metadata from this Syntax Object - * - * @return Syntax Object with empty metadata - */ - public Syntax withoutMeta() { - return withMeta(Maps.empty()); - } - - /** - * Unwraps a Syntax Object to get the underlying value. - * - * If the argument is not a Syntax object, return it unchanged (already unwrapped) - * @param Expected type of value - * @param x Any Object, which may be a Syntax Object - * @return The unwrapped value - */ - @SuppressWarnings("unchecked") - public static R unwrap(ACell x) { - return (x instanceof Syntax) ? ((Syntax) x).getValue() : (R) x; - } - - /** - * Recursively unwraps a Syntax object - * - * @param maybeSyntax Syntax Object to unwrap - * @return Unwrapped object - */ - @SuppressWarnings("unchecked") - public static R unwrapAll(ACell maybeSyntax) { - ACell a = unwrap(maybeSyntax); - - if (a instanceof ACollection) { - return (R) ((ACollection) a).map(e -> unwrapAll(e)); - } else if (a instanceof AMap) { - AMap m = (AMap) a; - return (R) m.reduceEntries((acc, e) -> { - return acc.assoc(unwrapAll(e.getKey()), unwrapAll(e.getValue())); - }, (AMap) Maps.empty()); - } else { - // nothing else can contain Syntax objects, so just return normally - return (R) a; - } - } - - @Override - public byte getTag() { - return Tag.SYNTAX; - } - - @Override - public ACell toCanonical() { - return this; - } - - - -} diff --git a/convex-core/src/main/java/convex/core/data/Tag.java b/convex-core/src/main/java/convex/core/data/Tag.java deleted file mode 100644 index fc6a402c2..000000000 --- a/convex-core/src/main/java/convex/core/data/Tag.java +++ /dev/null @@ -1,87 +0,0 @@ -package convex.core.data; - -/** - * Class containing constant Tag values. - * - * All of this is critical to the wire format and hash calculation. - * - * This is the gospel. The whole truth, and nothing but the truth. - * - * Hack here at your peril. Changes will break every single database, most immutable Value IDs, and probably your heart. - */ -public class Tag { - // Basic Types: Primitive values and numerics - // we might add unsigned primitives at some point? - public static final byte NULL = (byte) 0x00; - public static final byte BYTE = (byte) 0x01; - //public static final byte SHORT = (byte) 0x05; - //public static final byte INT = (byte) 0x07; - public static final byte LONG = (byte) 0x09; - - //public static final byte BIG_INTEGER = (byte) 0x0a; // Arbitrary length integer - public static final byte CHAR = (byte) 0x0c; - public static final byte DOUBLE = (byte) 0x0d; - //public static final byte FLOAT = (byte) 0x0f; - //public static final byte BIG_DECIMAL = (byte) 0x0e; // E notation precise decimal - - // Amounts of tokens - // Note: Amounts use the low 4 bits of the tag for decimal scale factor - //public static final byte AMOUNT = (byte) 0x10; // Financial amount - - // crypto and security primitives - public static final byte REF = (byte) 0x20; - public static final byte ADDRESS = (byte) 0x21; - public static final byte SIGNATURE = (byte) 0x22; - - // Standard supported object data types - public static final byte STRING = (byte) 0x30; - public static final byte BLOB = (byte) 0x31; - public static final byte SYMBOL = (byte) 0x32; - public static final byte KEYWORD = (byte) 0x33; - - // data type tags beyond this point - - // general purpose data structures - public static final byte VECTOR = (byte) 0x80; - public static final byte LIST = (byte) 0x81; - public static final byte MAP = (byte) 0x82; - public static final byte SET = (byte) 0x83; - - public static final byte BLOBMAP = (byte) 0x84; - - public static final byte SYNTAX = (byte) 0x88; - - // special data structure - public static final byte SIGNED_DATA = (byte) 0x90; - - // Record data structures - public static final byte STATE = (byte) 0xA0; - public static final byte BELIEF = (byte) 0xAA; - public static final byte BLOCK = (byte) 0xAB; - public static final byte ORDER = (byte) 0xAC; - public static final byte RESULT = (byte)0xAD; // transaction result - public static final byte BLOCK_RESULT = (byte) 0xAE; - - public static final byte FALSE = (byte) 0xB0; - public static final byte TRUE = (byte) 0xB1; - - // Control structures - public static final byte COMMAND = (byte) 0xC0; - public static final byte ACCOUNT_STATUS = (byte) 0xC1; - public static final byte PEER_STATUS = (byte) 0xC2; - - // Code - public static final byte OP = (byte) 0xCC; - public static final byte CORE_DEF = (byte) 0xCD; - public static final byte FN = (byte) 0xCF; - public static final byte FN_MULTI = (byte) 0xCB; - - // transaction types - public static final byte INVOKE = (byte) 0xD0; - public static final byte TRANSFER = (byte) 0xD1; - public static final byte CALL = (byte) 0xD2; - - // F? Illegal / reserved - public static final byte ILLEGAL = (byte) 0xFF; - -} diff --git a/convex-core/src/main/java/convex/core/data/VectorArray.java b/convex-core/src/main/java/convex/core/data/VectorArray.java deleted file mode 100644 index 7ebd5ca6f..000000000 --- a/convex-core/src/main/java/convex/core/data/VectorArray.java +++ /dev/null @@ -1,216 +0,0 @@ -package convex.core.data; - -import java.util.ListIterator; -import java.util.Spliterator; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; - -import convex.core.exceptions.InvalidDataException; - -/** - * Experimental: implementation of AVector backed by a Java array for temporary usage purposes. - * - * @param Type of vector elements - */ -public class VectorArray extends AVector { - - private final T[] array; - private final int offset; - private final int stride; - - protected VectorArray(long count, T[] array, int offset, int stride) { - super(count); - this.array=array; - this.offset=offset; - this.stride=stride; - } - - @Override - public ListIterator listIterator() { - throw new UnsupportedOperationException(); - } - - @Override - public int estimatedEncodingSize() { - return 100; - } - - @Override - public T get(long i) { - if ((i < 0) || (i >= count)) throw new IndexOutOfBoundsException("Index: " + i); - return array[offset+(int)(i*stride)]; - } - - @Override - public AVector appendChunk(VectorLeaf chunkVector) { - return toVector().appendChunk(chunkVector); - } - - @Override - public VectorLeaf getChunk(long offset) { - return toVector().getChunk(offset); - } - - @Override - public AVector append(T value) { - return toVector().append(value); - } - - @Override - public boolean isPacked() { - // TODO Auto-generated method stub - return false; - } - - - @Override - public boolean anyMatch(Predicate pred) { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean allMatch(Predicate pred) { - // TODO Auto-generated method stub - return false; - } - - @Override - public AVector map(Function mapper) { - // TODO Auto-generated method stub - return null; - } - - @Override - public AVector concat(ASequence b) { - return toVector().concat(b); - } - - @Override - public R reduce(BiFunction func, R value) { - throw new UnsupportedOperationException(); - } - - @Override - public Spliterator spliterator(long position) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ListIterator listIterator(long index) { - // TODO Auto-generated method stub - return null; - } - - @Override - public boolean isCanonical() { - // Not a canonical vector! - return false; - } - - @Override public final boolean isCVMValue() { - return false; - } - - @Override - public AVector updateRefs(IRefFunction func) { - return toVector().updateRefs(func); - } - - @Override - public long commonPrefixLength(AVector b) { - return toVector().commonPrefixLength(b); - } - - @Override - public AVector next() { - if (count==0) return null; - return slice(1,count-1); - } - - @Override - public AVector assoc(long i, R value) { - return toVector().assoc(i,value); - } - - @Override - public long longIndexOf(Object value) { - // TODO Auto-generated method stub - return 0; - } - - @Override - public long longLastIndexOf(Object value) { - // TODO Auto-generated method stub - return 0; - } - - @Override - public void forEach(Consumer action) { - // TODO Auto-generated method stub - - } - - @Override - public void visitElementRefs(Consumer> f) { - // TODO Auto-generated method stub - - } - - @Override - public Ref getElementRef(long index) { - return Ref.get(get(index)); - } - - @SuppressWarnings("unchecked") - @Override - public VectorArray subVector(long start, long length) { - checkRange(start, length); - if (length == count) return (VectorArray) this; - - return new VectorArray(length,(R[])array,offset+(int)(start*stride),stride); - } - - @Override - public int encode(byte[] bs, int pos) { - return toVector().encode(bs, pos); - } - - @SuppressWarnings("unchecked") - @Override - public AVector toVector() { - if (stride==1) Vectors.create(array, offset, (int)count); - return (AVector) Vectors.create(toCellArray()); - } - - @Override - public void validateCell() throws InvalidDataException { - throw new UnsupportedOperationException(); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - return toVector().encodeRaw(bs, pos); - } - - @Override - public int getRefCount() { - throw new UnsupportedOperationException(); - } - - @Override - public AVector toCanonical() { - // Convert to vector - return toVector(); - } - - @Override - protected void copyToArray(R[] arr, int offset) { - // TODO Auto-generated method stub - - } - -} diff --git a/convex-core/src/main/java/convex/core/data/VectorLeaf.java b/convex-core/src/main/java/convex/core/data/VectorLeaf.java deleted file mode 100644 index 8a36a95e8..000000000 --- a/convex-core/src/main/java/convex/core/data/VectorLeaf.java +++ /dev/null @@ -1,763 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.util.ListIterator; -import java.util.NoSuchElementException; -import java.util.Spliterator; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; - -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * A Persistent Vector implementation representing 0-16 elements with a - * packed Vector prefix. - * - * Design goals: - *
    - *
  • Allows fast access to most recently appended items
  • - *
  • O(1) append, equals - O(log n) access, update
  • - *
  • O(log n) comparisons
  • - *
  • Fast computation of common prefix
  • - *
- * - * Representation in bytes: - * - *
    - *
  • 0x80 - ListVector tag byte
  • - *
  • VLC Long - Length of list. Greater than 16 implies prefix must be - * present. Low 4 bits specify N (0 means 16 in presence of prefix)
  • - *
  • [Ref]*N - N Elements with length
  • - *
  • Ref? - Tail Ref (excluded if not present)
  • - *
- * - * @param Type of vector elements - */ -public class VectorLeaf extends AVector { - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static final VectorLeaf EMPTY = new VectorLeaf(new Ref[0]); - - public static final Ref> EMPTY_REF = EMPTY.getRef(); - - static { - // Set empty Ref flags as internal embedded constant - EMPTY_REF.setFlags(Ref.INTERNAL_FLAGS); - } - - /** Maximum size of a single ListVector before a tail is required */ - public static final int MAX_SIZE = Vectors.CHUNK_SIZE; - - private final Ref[] items; - private Ref> prefix; - - VectorLeaf(Ref[] items, Ref> prefix, long count) { - super(count); - this.items = items; - this.prefix = prefix; - } - - VectorLeaf(Ref[] items) { - this(items, null, items.length); - } - - /** - * Creates a VectorLeaf with the given items - * - * @param elements Elements to add - * @param offset Offset into element array - * @param length Number of elements to include from array - * @return New ListVector - */ - @SuppressWarnings("unchecked") - public static VectorLeaf create(ACell[] elements, int offset, int length) { - if (length == 0) return (VectorLeaf) VectorLeaf.EMPTY; - if (length > Vectors.CHUNK_SIZE) - throw new IllegalArgumentException("Too many elements for ListVector: " + length); - Ref[] items = new Ref[length]; - for (int i = 0; i < length; i++) { - T value=(T) elements[i + offset]; - items[i] = Ref.get(value); - } - return new VectorLeaf(items); - } - - /** - * Creates a ListVector with the given items appended to the specified tail - * - * @param elements Elements to add - * @param offset Offset into element array - * @param length Number of elements to include from array - * @param prefix Prefix vector to append to - * @return The updated ListVector - */ - @SuppressWarnings("unchecked") - public static VectorLeaf create(ACell[] elements, int offset, int length, AVector prefix) { - if (length == 0) - throw new IllegalArgumentException("ListVector with tail cannot be created with zero head elements"); - if (length > Vectors.CHUNK_SIZE) - throw new IllegalArgumentException("Too many elements for ListVector: " + length); - Ref[] items = new Ref[length]; - for (int i = 0; i < length; i++) { - T value=(T) elements[i + offset]; - items[i] = Ref.get(value); - } - return new VectorLeaf(items, prefix.getRef(), prefix.count() + length); - } - - public static VectorLeaf create(T[] things) { - return create(things, 0, things.length); - } - - @SuppressWarnings("unchecked") - @Override - public final AVector toVector() { - return (AVector) this; - } - - @SuppressWarnings("unchecked") - @Override - public AVector append(T value) { - int localSize = items.length; - if (localSize < Vectors.CHUNK_SIZE) { - // extend storage array - Ref[] newItems = new Ref[localSize + 1]; - System.arraycopy(items, 0, newItems, 0, localSize); - newItems[localSize] = Ref.get(value); - - if (localSize + 1 == Vectors.CHUNK_SIZE) { - // need to extend to TreeVector - VectorLeaf chunk = new VectorLeaf(newItems); - if (!hasPrefix()) return chunk; // exactly one whole chunk - return prefix.getValue().appendChunk(chunk); - } else { - // just grow current ListVector head - return new VectorLeaf(newItems, prefix, count + 1); - } - } else { - // this must be a full single chunk already, so turn this into tail of new - // ListVector - AVector newTail = this; - return new VectorLeaf(new Ref[] { Ref.get(value) }, newTail.getRef(), count + 1); - } - } - - @SuppressWarnings("unchecked") - @Override - public AVector concat(ASequence b) { - // Maybe can optimise? - long aLen = count(); - long bLen = b.count(); - AVector result = (AVector) this; - long i = aLen; - long end = aLen + bLen; - while (i < end) { - if ((i & Vectors.BITMASK) == 0) { - int rn = Utils.checkedInt(Math.min(Vectors.CHUNK_SIZE, end - i)); - if (rn == Vectors.CHUNK_SIZE) { - // we can append a whole chunk - result = result.appendChunk((VectorLeaf) b.subVector(i - aLen, rn)); - i += Vectors.CHUNK_SIZE; - continue; - } - } - // otherwise just append one-by-one - result = result.append(b.get(i - aLen)); - i++; - } - return result; - } - - @Override - public AVector appendChunk(VectorLeaf chunk) { - if (chunk.count != Vectors.CHUNK_SIZE) - throw new IllegalArgumentException("Can't append a chunk of size: " + chunk.count()); - - if (this.count == 0) return chunk; - if (this.hasPrefix()) { - throw new IllegalArgumentException( - "Can't append chunk to a ListVector with a tail (length = " + count + ")"); - } - if (this.count != Vectors.CHUNK_SIZE) - throw new IllegalArgumentException("Can't append chunk to a ListVector of size: " + this.count); - return VectorTree.wrap2(chunk, this); - } - - @Override - public T get(long i) { - if ((i < 0) || (i >= count)) throw new IndexOutOfBoundsException("Index: " + i); - long ix = i - prefixLength(); - if (ix >= 0) { - return items[(int) ix].getValue(); - } else { - return prefix.getValue().get(i); - } - } - - @Override - public Ref getElementRef(long i) { - if ((i < 0) || (i >= count)) throw new IndexOutOfBoundsException("Index: " + i); - long ix = i - prefixLength(); - if (ix >= 0) { - return items[(int) ix]; - } else { - return prefix.getValue().getElementRef(i); - } - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public AVector assoc(long i, R value) { - if ((i < 0) || (i >= count)) return null; - - long ix = i - prefixLength(); - if (ix >= 0) { - R old = (R) items[(int) ix].getValue(); - if (old == value) return (AVector) this; - Ref[] newItems = (Ref[]) items.clone(); - newItems[(int) ix] = Ref.get(value); - return new VectorLeaf(newItems, (Ref)prefix, count); - } else { - AVector tl = prefix.getValue(); - AVector newTail = tl.assoc(i, value); - if (tl == newTail) return (AVector) this; - return new VectorLeaf((Ref[])items, newTail.getRef(), count); - } - } - - /** - * Reads a ListVector from the provided ByteBuffer - * - * Assumes the header byte and count is already read. - * - * @param bb ByteBuffer to read from - * @param count Number of elements - * @return VectorLeaf read from ByteBuffer - * @throws BadFormatException If encoding is invalid - */ - @SuppressWarnings("unchecked") - public static VectorLeaf read(ByteBuffer bb, long count) throws BadFormatException { - if (count < 0) throw new BadFormatException("Negative length"); - if (count == 0) return (VectorLeaf) EMPTY; - boolean prefixPresent = count > MAX_SIZE; - - int n = ((int) count) & 0xF; - if (n == 0) { - if (count > 16) throw new BadFormatException("Vector not valid for size 0 mod 16: " + count); - n = VectorLeaf.MAX_SIZE; // we know this must be true since zero already caught - } - - Ref[] items = (Ref[]) new Ref[n]; - for (int i = 0; i < n; i++) { - Ref ref = Format.readRef(bb); - items[i] = ref; - } - - Ref> tail = null; - if (prefixPresent) { - tail=Format.readRef(bb); - } - - return new VectorLeaf(items, tail, count); - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.VECTOR; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - int ilength = items.length; - boolean hasPrefix = hasPrefix(); - - // count field - pos = Format.writeVLCLong(bs,pos, count); - - for (int i = 0; i < ilength; i++) { - pos= items[i].encode(bs,pos); - } - - if (hasPrefix) { - pos = prefix.encode(bs,pos); - } - return pos; - } - - @Override - public int estimatedEncodingSize() { - // allow space for header of reasonable length - // Estimate 64 bytes per element ref (plus space for tail/ other overhead) - int ESTIMATED_REF_SIZE=70; - return 1 + 9 + ESTIMATED_REF_SIZE * (items.length + 1); - } - - @Override - public long getEncodingLength() { - if (encoding!=null) return encoding.count(); - - // tag and count - long length=1+Format.getVLCLength(count); - int n = items.length; - if (prefix!=null) length+=prefix.getEncodingLength(); - for (int i = 0; i < n; i++) { - length+=items[i].getEncodingLength(); - } - return length; - } - - public static int MAX_ENCODING_SIZE = 1 + Format.MAX_VLC_LONG_LENGTH + Format.MAX_EMBEDDED_LENGTH * (MAX_SIZE+1); - - /** - * Returns true if this ListVector has a prefix AVector. - * - * @return true if this ListVector has a prefix, false otherwise - */ - public boolean hasPrefix() { - return prefix != null; - } - - public VectorLeaf withPrefix(AVector newPrefix) { - if ((newPrefix == null) && !hasPrefix()) return this; - long tc = (newPrefix == null) ? 0L : newPrefix.count(); - return new VectorLeaf(items, (newPrefix == null) ? null : newPrefix.getRef(), tc + items.length); - } - - @Override - public boolean isPacked() { - return (!hasPrefix()) && (items.length == Vectors.CHUNK_SIZE); - } - - @Override - public ListIterator listIterator() { - return listIterator(0); - } - - @Override - public ListIterator listIterator(long index) { - return new ListVectorIterator(index); - } - - /** - * Custom ListIterator for ListVector - */ - private class ListVectorIterator implements ListIterator { - ListIterator prefixIterator; - int pos; - - public ListVectorIterator(long index) { - if (index < 0L) throw new IndexOutOfBoundsException((int)index); - - long tc = prefixLength(); - if (index >= tc) { - // in the list head - if (index > count) throw new IndexOutOfBoundsException((int)index); - pos = (int) (index - tc); - this.prefixIterator = (prefix == null) ? null : prefix.getValue().listIterator(tc); - } else { - // in the prefix - pos = 0; - this.prefixIterator = (prefix == null) ? null : prefix.getValue().listIterator(index); - } - } - - @Override - public boolean hasNext() { - if ((prefixIterator != null) && prefixIterator.hasNext()) return true; - return pos < items.length; - } - - @Override - public T next() { - if (prefixIterator != null) { - if (prefixIterator.hasNext()) return prefixIterator.next(); - } - return items[pos++].getValue(); - } - - @Override - public boolean hasPrevious() { - if (pos > 0) return true; - if (prefixIterator != null) return prefixIterator.hasPrevious(); - return false; - } - - @Override - public T previous() { - if (pos > 0) return items[--pos].getValue(); - - if (prefixIterator != null) return prefixIterator.previous(); - throw new NoSuchElementException(); - } - - @Override - public int nextIndex() { - if ((prefixIterator != null) && prefixIterator.hasNext()) return prefixIterator.nextIndex(); - return Utils.checkedInt(prefixLength() + pos); - } - - @Override - public int previousIndex() { - if (pos > 0) return Utils.checkedInt(prefixLength() + pos - 1); - if (prefixIterator != null) return prefixIterator.previousIndex(); - return -1; - } - - @Override - public void remove() { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public void set(T e) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public void add(T e) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - } - - public long prefixLength() { - return count - items.length; - } - - @SuppressWarnings("unchecked") - @Override - protected void copyToArray(K[] arr, int offset) { - int s = size(); - if (prefix != null) { - prefix.getValue().copyToArray(arr, offset); - } - int ilen = items.length; - for (int i = 0; i < ilen; i++) { - K value = (K) items[i].getValue(); - ; - arr[offset + s - ilen + i] = value; - } - } - - @Override - public long longIndexOf(Object o) { - if (prefix != null) { - long pi = prefix.getValue().longIndexOf(o); - if (pi >= 0L) return pi; - } - for (int i = 0; i < items.length; i++) { - if (Utils.equals(items[i].getValue(), o)) return (count - items.length + i); - } - return -1L; - } - - @Override - public long longLastIndexOf(Object o) { - for (int i = items.length - 1; i >= 0; i--) { - if (Utils.equals(items[i].getValue(), o)) return (count - items.length + i); - } - if (prefix != null) { - long ti = prefix.getValue().longLastIndexOf(o); - if (ti >= 0L) return ti; - } - return -1L; - } - - @Override - public void forEach(Consumer action) { - if (prefix != null) { - prefix.getValue().forEach(action); - - for (Ref r : items) { - action.accept(r.getValue()); - } - } - } - - @Override - public boolean anyMatch(Predicate pred) { - if ((prefix != null) && (prefix.getValue().anyMatch(pred))) return true; - for (Ref r : items) { - if (pred.test(r.getValue())) return true; - } - return false; - } - - @Override - public boolean allMatch(Predicate pred) { - if ((prefix != null) && !(prefix.getValue().allMatch(pred))) return false; - for (Ref r : items) { - if (!pred.test(r.getValue())) return false; - } - return true; - } - - @SuppressWarnings("unchecked") - @Override - public AVector map(Function mapper) { - Ref> newPrefix = (prefix == null) ? null : prefix.getValue().map(mapper).getRef(); - - int ilength = items.length; - Ref[] newItems = (Ref[]) new Ref[ilength]; - for (int i = 0; i < ilength; i++) { - Ref iref=items[i]; - R r = mapper.apply(iref.getValue()); - newItems[i] = Ref.get(r); - } - - return (prefix == null) ? new VectorLeaf(newItems) : new VectorLeaf(newItems, newPrefix, count); - } - - @Override - public void visitElementRefs(Consumer> f) { - if (prefix != null) prefix.getValue().visitElementRefs(f); - for (Ref item : items) { - f.accept(item); - } - } - - @Override - public R reduce(BiFunction func, R value) { - if (prefix != null) value = prefix.getValue().reduce(func, value); - int ilength = items.length; - for (int i = 0; i < ilength; i++) { - value = func.apply(value, items[i].getValue()); - } - return value; - } - - @Override - public Spliterator spliterator(long position) { - return new ListVectorSpliterator(position); - } - - private class ListVectorSpliterator implements Spliterator { - long pos = 0; - - public ListVectorSpliterator(long position) { - if ((position < 0) || (position > count)) - throw new IllegalArgumentException(Errors.illegalPosition(position)); - this.pos = position; - } - - @Override - public boolean tryAdvance(Consumer action) { - if (pos >= count) return false; - action.accept((T) get(pos++)); - return true; - } - - @Override - public Spliterator trySplit() { - long tlength = prefixLength(); - if (pos < tlength) { - pos = tlength; - return prefix.getValue().spliterator(pos); - } - return null; - } - - @Override - public long estimateSize() { - return count; - } - - @Override - public int characteristics() { - return Spliterator.IMMUTABLE | Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED; - } - } - - @Override - public boolean isCanonical() { - return true; - } - - @Override public final boolean isCVMValue() { - return true; - } - - @Override - public int getRefCount() { - return items.length + (hasPrefix() ? 1 : 0); - } - - @SuppressWarnings("unchecked") - @Override - public Ref getRef(int i) { - if (prefix != null) { - if (i==0) return (Ref) prefix; - i--; // Decrement so that i indexes into child array after skipping prefix ref - } - int itemsCount = items.length; - if (i < 0) throw new IndexOutOfBoundsException("Negative Ref index: " + i); - if (i < itemsCount) return (Ref) items[i]; - throw new IndexOutOfBoundsException("Ref index out of range: " + i); - } - - @SuppressWarnings("unchecked") - @Override - public VectorLeaf updateRefs(IRefFunction func) { - Ref newPrefix = (prefix == null) ? null : func.apply(prefix); // do this first for in-order traversal - int ic = items.length; - Ref[] newItems = items; - for (int i = 0; i < ic; i++) { - Ref current = items[i]; - Ref newItem = func.apply(current); - if (newItem!=current) { - if (items==newItems) newItems=items.clone(); - newItems[i] = newItem; - } - } - if ((items==newItems) && (prefix == newPrefix)) return this; // if no change, safe to return this - return new VectorLeaf((Ref[]) newItems, (Ref>) newPrefix, count); - } - - @SuppressWarnings("unchecked") - @Override - public boolean equals(ACell a) { - if (a instanceof VectorLeaf) return equals((VectorLeaf)a); - if (!(a instanceof AVector)) return false; - - // Its a vector, but not canonical? - AVector v=(AVector) a; - if (v.count()!=count) return false; - return a.getEncoding().equals(this.getEncoding()); - } - - public boolean equals(VectorLeaf v) { - if (this == v) return true; - if (this.count != v.count()) return false; - if (!Utils.equals(this.prefix, v.prefix)) return false; - for (int i = 0; i < items.length; i++) { - if (!items[i].equalsValue(v.items[i])) return false; - } - return true; - } - - @Override - public long commonPrefixLength(AVector b) { - long n = count(); - if (this==b) return n; - int il = items.length; - long prefixLength = n - il; - if (prefixLength > 0) { - long prefixMatchLength = prefix.getValue().commonPrefixLength(b); - if (prefixMatchLength < prefixLength) return prefixMatchLength; // matched segment entirely within prefix - } - // must have matched prefixLength at least - long nn = Math.min(n, b.count()) - prefixLength; // number of extra elements to check - if (nn==0) return prefixLength; - VectorLeaf bChunk=b.getChunk(prefixLength); - for (int i = 0; i < nn; i++) { - if (!items[i].equalsValue(bChunk.items[i])) { - return prefixLength + i; - } - } - return prefixLength + nn; - } - - @Override - public VectorLeaf getChunk(long offset) { - if (prefix == null) { - if (offset == 0) return this; - } else { - AVector pre=prefix.getValue(); - long prefixLength=pre.count(); - if (offset AVector subVector(long start, long length) { - checkRange(start, length); - if (length == count) return (AVector) this; - - if (prefix == null) { - int len = Utils.checkedInt(length); - Ref[] newItems; - //if (start==0) { - // can share items if starting from zero index - // newItems=(Ref[]) items; - //} else { - newItems= new Ref[len]; - System.arraycopy(items, Utils.checkedInt(start), newItems, 0, len); - //} - - return new VectorLeaf(newItems, null, length); - } else { - long tc = prefixLength(); - if (start >= tc) { - // range is in tail of vector - return this.withPrefix(null).subVector(start - tc, length); - } - - AVector tv = prefix.getValue(); - if ((start + length) <= tc) { - // Range is entirely in prefix - return tv.subVector(start, length); - } else { - long split = tc - start; - return tv.subVector(start, split).concat(this.withPrefix(null).subVector(0, length - split)); - } - } - } - - @Override - public AVector next() { - if (count <= 1) return null; - return slice(1, count - 1); - } - - @SuppressWarnings("unchecked") - @Override - public void validate() throws InvalidDataException { - // TODO: Needs to ensure children are validated? - super.validate(); - if (prefix != null) { - // if we have a prefix, should be 1..15 elements only - if (count == Vectors.CHUNK_SIZE) { - throw new InvalidDataException("Full ListVector with prefix? This is not right...", this); - } - - if (count == 0) { - throw new InvalidDataException("Empty ListVector with prefix? This is not right...", this); - } - - ACell ccell=prefix.getValue(); - if (!(ccell instanceof AVector)) { - throw new InvalidDataException("Prefix is not a vector", this); - } - - AVector tv = (AVector)ccell; - if (prefixLength() != tv.count()) { - throw new InvalidDataException("Expected prefix length: " + prefixLength() + " but found " + tv.count(), - this); - } - tv.validate(); - } - } - - @Override - public void validateCell() throws InvalidDataException { - if ((count > 0) && (items.length == 0)) throw new InvalidDataException("Should be items present!", this); - if (!isCanonical()) throw new InvalidDataException("Not a canonical ListVector!", this); - } - - @Override - public ACell toCanonical() { - return this; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/VectorTree.java b/convex-core/src/main/java/convex/core/data/VectorTree.java deleted file mode 100644 index b49d8f95a..000000000 --- a/convex-core/src/main/java/convex/core/data/VectorTree.java +++ /dev/null @@ -1,719 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.util.ListIterator; -import java.util.NoSuchElementException; -import java.util.Spliterator; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; - -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * Persistent Vector implemented as a merkle tree of chunks - * - * shift indicates the level of the tree: 4 = 1st level, 8 = second etc. - * - * Invariants: - *
    - *
  • All children except the last must be fully packed
  • - *
  • Each non-terminal leaf chunk must be a tailless VectorLeaf of size 16
  • - *
- * - * This implies that the entire tree must be a multiple of 16 in size. This is a - * desirable property as we want dense trees in our canonical representation. - * Any extra elements must be stored in a ListVector. - * - * This structure facilitates fast ~O(log(n)) operations for lookup and vector - * element update, and usually O(1) element additions/lookup at end. - * - * "Software gets slower faster than hardware gets faster" - * - * - Niklaus Wirth - * - * @param Type of Vector elements - */ -public class VectorTree extends AVector { - - public static final int MINIMUM_SIZE = 2 * Vectors.CHUNK_SIZE; - private final int shift; // bits in each child block - - private final Ref>[] children; - - private VectorTree(Ref>[] children, long count) { - super(count); - this.shift = computeShift(count); - this.children = children; - } - - /** - * Computes the shift value for a BlockVector of the given count Note: if - * returns zero, count cannot be supported by a valid BlockVector - * - * @param count Number of elements - * @return Shift value - */ - public static int computeShift(long count) { - int shift = 0; - if (count >= (1L << 60)) return 60; - while ((1L << (shift + Vectors.BITS_PER_LEVEL)) < count) { - shift += Vectors.BITS_PER_LEVEL; - } - return shift; - } - - /** - * Gets the index of the start of a given child - * - * @param bpos - * @return - */ - private long childIndex(int childNumber) { - return childNumber * childSize(); - } - - /** - * Compute the size of the child array for a given count - * - * @param count - * @param shift - * @return - */ - static int computeArraySize(long count) { - int shift = computeShift(count); - long bsize = 1L << shift; - return (int) ((count + (bsize - 1)) / bsize); - } - - /** - * Create a TreeVector with the specified elements - things must have at least - * 32 elements (the minimum TreeVector size) - must be a whole multiple of 16 - * elements (complete chunks only) - * - * @param things Elements to include - * @param offset Offset into element array - * @param length Number of elements to include - * @return New TreeVector instance - */ - public static VectorTree create(ACell[] things, int offset, int length) { - if (length < MINIMUM_SIZE) - throw new IllegalArgumentException("Can't create BlockVector with insufficient size: " + length); - if ((length & Vectors.BITMASK) != 0) - throw new IllegalArgumentException("Can't create BlockVector with odd elements: " + length); - int shift = computeShift(length); - - int bSize = 1 << shift; - int bNum = (length + (bSize - 1)) / bSize; // enough blocks - @SuppressWarnings("unchecked") - Ref>[] bs = (Ref>[]) new Ref[bNum]; - for (int i = 0; i < bNum; i++) { - int bLen = Math.min(bSize, length - bSize * i); - bs[i] = Vectors.createChunked(things, offset + i * bSize, bLen).getRef(); - } - VectorTree tv = new VectorTree(bs, length); - return tv; - } - - @Override - public T get(long i) { - if ((i < 0) || (i >= count)) throw new IndexOutOfBoundsException("Index: " + i); - long bSize = 1L << shift; // size of a fully packed block - int b = (int) (i >> shift); - return children[b].getValue().get(i - b * bSize); - } - - @Override - public Ref getElementRef(long i) { - if ((i < 0) || (i >= count)) throw new IndexOutOfBoundsException("Index: " + i); - long bSize = 1L << shift; // size of a fully packed block - int b = (int) (i >> shift); - return children[b].getValue().getElementRef(i - b * bSize); - } - - @SuppressWarnings("unchecked") - @Override - public AVector assoc(long i, R value) { - if ((i < 0) || (i >= count)) return null; - - Ref>[] rchildren=(Ref[])children; - - long bSize = 1L << shift; // size of a fully packed block - int b = (int) (i >> shift); - AVector oc = rchildren[b].getValue(); - AVector nc = oc.assoc(i - (b * bSize), value); - if (oc == nc) return (AVector) this; - - Ref>[] newChildren = rchildren.clone(); - newChildren[b] = nc.getRef(); - return new VectorTree(newChildren, count); - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.VECTOR; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos= Format.writeVLCLong(bs,pos, count); - - int n = children.length; - for (int i = 0; i < n; i++) { - pos = children[i].encode(bs,pos); - } - return pos; - } - - @Override - public long getEncodingLength() { - if (encoding!=null) return encoding.count(); - - // tag and count - long length=1+Format.getVLCLength(count); - int n = children.length; - for (int i = 0; i < n; i++) { - length+=children[i].getEncodingLength(); - } - return length; - } - - @Override - public int estimatedEncodingSize() { - // Allow tag, long count, 80 bytes per child average plus some headroom - return 12 + (64 * (children.length+3)); - } - - public static int MAX_ENCODING_SIZE= 1 + Format.MAX_VLC_LONG_LENGTH + (Format.MAX_EMBEDDED_LENGTH * Vectors.CHUNK_SIZE); - - /** - * Reads a VectorTree from the provided ByteBuffer - * - * Assumes the header byte and count is already read. - * - * @param bb ByteBuffer to read from - * @param count Number of elements - * @return TreeVector instance as read from ByteBuffer - * @throws BadFormatException If encoding is invalid - */ - @SuppressWarnings("unchecked") - public static VectorTree read(ByteBuffer bb, long count) - throws BadFormatException { - if (count < 0) throw new BadFormatException("Negative count?"); - int n = computeArraySize(count); - Ref>[] items = (Ref>[]) new Ref[n]; - for (int i = 0; i < n; i++) { - Ref> ref = Format.readRef(bb); - items[i] = ref; - } - - return new VectorTree(items, count); - } - - @SuppressWarnings("unchecked") - @Override - public VectorTree appendChunk(VectorLeaf b) { - if (b.hasPrefix()) throw new IllegalArgumentException("Can't append a block with a tail"); - if (b.count() != VectorLeaf.MAX_SIZE) - throw new IllegalArgumentException("Invalid block size for append: " + b.count()); - if (isPacked()) { - // full blockvector, so need to elevate to the next level - Ref>[] newBlocks = new Ref[2]; - newBlocks[0] = this.getRef(); - newBlocks[1] = b.getRef(); - return new VectorTree(newBlocks, this.count() + b.count()); - } - - int blength = children.length; - AVector lastBlock = children[blength - 1].getValue(); - if (lastBlock.count() == childSize()) { - // need to extend block array - Ref>[] newBlocks = new Ref[blength + 1]; - System.arraycopy(children, 0, newBlocks, 0, blength); - newBlocks[blength] = b.getRef(); - return new VectorTree(newBlocks, count + Vectors.CHUNK_SIZE); - } else { - // add b into current last block - AVector newLast = lastBlock.appendChunk(b); - Ref>[] newBlocks = new Ref[blength]; - System.arraycopy(children, 0, newBlocks, 0, blength - 1); - newBlocks[blength - 1] = newLast.getRef(); - return new VectorTree(newBlocks, count + Vectors.CHUNK_SIZE); - } - } - - /** - * Get the child size in number of chunks (for all except the last child) - */ - private final long childSize() { - return 1L << (shift); - } - - /** - * Get the child size in number of chunks for a specific child - */ - private static long childSize(long count, int i) { - long n=computeArraySize(count); - if ((i<0)||(i>=n)) throw new IndexOutOfBoundsException("Bad child: "+i); - int shift=computeShift(count); - long cs= 1L< append(T value) { - return new VectorLeaf(new Ref[] { Ref.get(value) }, this.getRef(), count + 1); - } - - @SuppressWarnings("unchecked") - @Override - public AVector concat(ASequence b) { - long bLen = b.count(); - VectorTree result = (VectorTree) this; - long bi = 0; - while (bi < bLen) { - if ((bi + Vectors.CHUNK_SIZE) <= bLen) { - // can append a whole chunk - VectorLeaf chunk = (VectorLeaf) b.subVector(bi, Vectors.CHUNK_SIZE); - result = result.appendChunk(chunk); - bi += Vectors.CHUNK_SIZE; - } else { - // we have less than a chunk left, so final result must be a ListVector with the - // current result as tail - VectorLeaf head = (VectorLeaf) b.subVector(bi, bLen - bi); - return ((VectorLeaf)head).withPrefix(result); - } - } - return result; - } - - /** - * Creates a TreeVector with exactly two chunks - * - * @param - * @param head - * @param tail - * @return - */ - @SuppressWarnings("unchecked") - static VectorTree wrap2(VectorLeaf head, VectorLeaf tail) { - Ref>[] newBlocks = new Ref[2]; - newBlocks[0] = tail.getRef(); - newBlocks[1] = head.getRef(); - return new VectorTree(newBlocks, 2 * Vectors.CHUNK_SIZE); - } - - @Override - protected void copyToArray(K[] arr, int offset) { - for (int i = 0; i < children.length; i++) { - AVector b = children[i].getValue(); - b.copyToArray(arr, offset); - offset += b.count(); - } - } - - @Override - public long longIndexOf(Object o) { - long offset = 0; - for (int i = 0; i < children.length; i++) { - AVector b = children[i].getValue(); - long bpos = b.longIndexOf(o); - if (bpos >= 0) return bpos + offset; - offset += b.count(); - } - return -1; - } - - @Override - public long longLastIndexOf(Object o) { - long offset = count; - for (int i = children.length - 1; i >= 0; i--) { - AVector b = children[i].getValue(); - offset -= b.count(); - long bpos = b.longLastIndexOf(o); - if (bpos >= 0) return bpos + offset; - } - return -1; - } - - @Override - public ListIterator listIterator() { - return new VectorTreeIterator(); - } - - @Override - public ListIterator listIterator(long index) { - return new VectorTreeIterator(index); - } - - private class VectorTreeIterator implements ListIterator { - int bpos; - ListIterator sub; - - public VectorTreeIterator() { - this(0); - } - - public VectorTreeIterator(final long index) { - long ix=index; - if (index < 0L) throw new IndexOutOfBoundsException((int)index); - - bpos = 0; - for (int i = 0; i < children.length; i++) { - AVector b = children[bpos].getValue(); - long bc = b.count(); - if (ix <= bc) { - sub = b.listIterator(ix); - return; - } - ix -= bc; - bpos++; - } - // appears to be beyond the end? - throw new IndexOutOfBoundsException((int)index); - } - - @Override - public boolean hasNext() { - if (sub.hasNext()) return true; - return bpos < children.length - 1; - } - - @Override - public T next() { - if (sub.hasNext()) return sub.next(); - if (bpos < children.length - 1) { - bpos++; - sub = children[bpos].getValue().listIterator(); - return sub.next(); - } - ; - throw new NoSuchElementException(); - } - - @Override - public boolean hasPrevious() { - if (sub.hasPrevious()) return true; - return bpos > 0; - } - - @Override - public T previous() { - if (sub.hasPrevious()) return sub.previous(); - if (bpos > 0) { - bpos = bpos - 1; - AVector b = children[bpos].getValue(); - sub = b.listIterator(b.count()); - return sub.previous(); - } - throw new NoSuchElementException(); - } - - @Override - public int nextIndex() { - return Utils.checkedInt(childIndex(bpos) + sub.nextIndex()); - } - - @Override - public int previousIndex() { - return Utils.checkedInt(childIndex(bpos) + sub.previousIndex()); - } - - @Override - public void remove() { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public void set(T e) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - @Override - public void add(T e) { - throw new UnsupportedOperationException(Errors.immutable(this)); - } - - } - - @Override - public void forEach(Consumer action) { - for (Ref> r : children) { - r.getValue().forEach(action); - } - } - - @Override - public boolean anyMatch(Predicate pred) { - for (Ref> r : children) { - if (r.getValue().anyMatch(pred)) return true; - } - return false; - } - - @Override - public boolean allMatch(Predicate pred) { - for (Ref> r : children) { - if (!r.getValue().allMatch(pred)) return false; - } - return true; - } - - @SuppressWarnings("unchecked") - @Override - public AVector map(Function mapper) { - int blength = children.length; - Ref>[] newBlocks = (Ref>[]) new Ref[blength]; - for (int i = 0; i < blength; i++) { - AVector r = children[i].getValue().map(mapper); - newBlocks[i] = r.getRef(); - } - return new VectorTree(newBlocks, count); - } - - @Override - public void visitElementRefs(Consumer> f) { - for (Ref> item : children) { - item.getValue().visitElementRefs(f); - } - } - - @Override - public R reduce(BiFunction func, R value) { - int blength = children.length; - for (int i = 0; i < blength; i++) { - value = children[i].getValue().reduce(func, value); - } - return value; - } - - @Override - public Spliterator spliterator(long position) { - return new TreeVectorSpliterator(position); - } - - private class TreeVectorSpliterator implements Spliterator { - long pos = 0; - - public TreeVectorSpliterator(long position) { - if ((position < 0) || (position > count)) - throw new IllegalArgumentException(Errors.illegalPosition(position)); - this.pos = position; - } - - @Override - public boolean tryAdvance(Consumer action) { - if (pos >= count) return false; - action.accept((T) get(pos++)); - return true; - } - - @Override - public Spliterator trySplit() { - for (int i = 0; i < children.length; i++) { - long bpos = childIndex(i); - AVector b = children[i].getValue(); - - long bcount = b.count(); - long blockEnd = childIndex(i) + bcount; - if (pos < blockEnd) { - Spliterator ss = b.spliterator(pos - bpos); - pos = blockEnd; - return ss; - } - } - return null; - } - - @Override - public long estimateSize() { - return count; - } - - @Override - public int characteristics() { - return Spliterator.IMMUTABLE | Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED; - } - } - - @Override - public boolean isCanonical() { - if (count < MINIMUM_SIZE) return false; - return true; - } - - @Override public final boolean isCVMValue() { - return true; - } - - @SuppressWarnings("unchecked") - @Override - public final AVector toVector() { - assert (isCanonical()); - return (AVector) this; - } - - @Override - public int getRefCount() { - return children.length; - } - - @SuppressWarnings("unchecked") - @Override - public Ref getRef(int i) { - int ic = children.length; - if (i < 0) throw new IndexOutOfBoundsException("Negative Ref index: " + i); - if (i < ic) return (Ref) children[i]; - throw new IndexOutOfBoundsException("Ref index out of range: " + i); - } - - @SuppressWarnings("unchecked") - @Override - public VectorTree updateRefs(IRefFunction func) { - int ic = children.length; - Ref>[] newChildren = children; - for (int i = 0; i < ic; i++) { - Ref> current = children[i]; - Ref> newChild = (Ref>) func.apply(current); - - if (newChild!=current) { - if (children==newChildren) newChildren=children.clone(); - newChildren[i] = newChild; - } - } - if (newChildren==children) return this; // no change, safe to return this - return new VectorTree<>(newChildren, count); - } - - @Override - public long commonPrefixLength(AVector b) { - if (b instanceof VectorTree) return commonPrefixLength((VectorTree) b); - return b.commonPrefixLength(this); // Handle MapEntry and ListVectors - } - - private long commonPrefixLength(VectorTree b) { - if (this.equals(b)) return count; - - long cs = childSize(); - long bcs = b.childSize(); - if (cs == bcs) { - return commonPrefixLengthAligned(b); - } else if (cs < bcs) { - // b is longer - AVector bChild = b.children[0].getValue(); - return commonPrefixLength(bChild); - } else { - // this is longer - AVector child = children[0].getValue(); - return child.commonPrefixLength(b); - } - } - - // compute common prefix length assuming TreeVectors are aligned (same child - // size) - private long commonPrefixLengthAligned(VectorTree b) { - // check if we have the same stored hash. If so, quick exit! - Hash thisHash = cachedHash(); - if (thisHash != null) { - Hash bHash = b.cachedHash(); - if ((bHash != null) && (thisHash.equals(bHash))) return count; - } - - int n = Math.min(children.length, b.children.length); - long cs = childSize(); - long result = 0; - for (int i = 0; i < n; i++) { - long cpl = children[i].getValue().commonPrefixLength(b.children[i].getValue()); - if (cpl < cs) return result + cpl; - result += cs; // we have validated cs elements as equal - } - return result; - } - - @Override - public VectorLeaf getChunk(long offset) { - long cs = childSize(); - int ix = (int) (offset / cs); - AVector child = children[ix].getValue(); - long cOffset = offset - (ix * cs); - if (cs == VectorLeaf.MAX_SIZE) { - if (cOffset != 0) throw new IndexOutOfBoundsException("Index: " + offset); - return (VectorLeaf) child; - } - return child.getChunk(cOffset); - } - - @SuppressWarnings("unchecked") - @Override - public AVector subVector(long start, long length) { - checkRange(start, length); - if ((start & Vectors.BITMASK) == 0) { - // TODO: this can be fast! - } - ACell[] arr = new ACell[Utils.checkedInt(length)]; - for (int i = 0; i < length; i++) { - arr[i] = get(start + i); - } - return (AVector) Vectors.create(arr); - } - - @Override - public AVector next() { - return slice(1L, count - 1); - } - - @Override - public void validate() throws InvalidDataException { - super.validate(); - long c = 0; - int blen = children.length; - if (blen < 2) throw new InvalidDataException("Insufficient children: " + blen, this); - long bsize = childSize(); - for (int i = 0; i < blen; i++) { - ACell ch = children[i].getValue(); - if (!(ch instanceof AVector)) throw new InvalidDataException("Child "+i+" is not a vector!",this); - @SuppressWarnings("unchecked") - AVector b=(AVector)ch; - - b.validate(); - long expectedChildSize=childSize(count,i); - if (expectedChildSize != b.count()) { - throw new InvalidDataException("Expected block size: " + bsize + " for blocks[" + i + "] but was: " - + b.count() + " in BlockVector of size: " + count, this); - } - - c += b.count(); - } - if (c != count) { - throw new InvalidDataException("Expected count: " + count + " but sum of child sizes was: " + c, this); - } - } - - @Override - public void validateCell() throws InvalidDataException { - int blen = children.length; - if (count < blen) throw new InvalidDataException("Implausible low count: " + count, this); - - if (blen < 2) throw new InvalidDataException("Insufficient children: " + blen, this); - - } - - @Override - public ACell toCanonical() { - // TODO Should be always true? - return this; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/Vectors.java b/convex-core/src/main/java/convex/core/data/Vectors.java deleted file mode 100644 index d2abe8056..000000000 --- a/convex-core/src/main/java/convex/core/data/Vectors.java +++ /dev/null @@ -1,134 +0,0 @@ -package convex.core.data; - -import java.nio.ByteBuffer; -import java.util.Collection; - -import org.bouncycastle.util.Arrays; - -import convex.core.exceptions.BadFormatException; -import convex.core.lang.RT; -import convex.core.util.Utils; - -public class Vectors { - - protected static final int BITS_PER_LEVEL = 4; - protected static final int CHUNK_SIZE = 1 << BITS_PER_LEVEL; // 16 - protected static final int BITMASK = CHUNK_SIZE - 1; // 15 - - /** - * Creates a canonical AVector with the given elements - * - * @param elements Elements to include - * @param offset Offset into element array - * @param length Number of elements to take - * @return New vector with the specified elements - */ - public static AVector create(ACell[] elements, int offset, int length) { - if (length < 0) throw new IllegalArgumentException("Cannot create vector of negative length!"); - if (length <= CHUNK_SIZE) return VectorLeaf.create(elements, offset, length); - int tailLength = Utils.checkedInt((length >> BITS_PER_LEVEL) << BITS_PER_LEVEL); - AVector tail = Vectors.createChunked(elements, offset, tailLength); - if (tail.count() == length) return tail; - return VectorLeaf.create(elements, offset + tailLength, length - tailLength, tail); - } - - /** - * Create a canonical vector using blocks. Suitable for a ListVector tail. - * - * @param elements - * @param offset - * @param length - * @return A vector, which must consist of a positive number of complete chunks. - */ - static AVector createChunked(ACell[] elements, int offset, int length) { - if ((length == 0) || (length & BITMASK) != 0) - throw new IllegalArgumentException("Invalid vector length: " + length); - if (length == CHUNK_SIZE) return VectorLeaf.create(elements, offset, length); - return VectorTree.create(elements, offset, length); - } - - /** - * Create a vector from an array of elements. - * - * @param Type of elements - * @param elements Elements to include - * @return New vector with the specified elements - */ - public static AVector create(ACell[] elements) { - return create(elements, 0, elements.length); - } - - /** - * Coerces a collection to a vector. Not necessarily the most efficient. - * Performs an unchecked cast. - * - * @param Type of Vector elements to produce - * @param Type of source collection elements - * @param elements Elements to include - * @return New vector with the specified collection of elements - */ - @SuppressWarnings("unchecked") - public static AVector create(Collection elements) { - if (elements instanceof ASequence) return create((ASequence) elements); - if (elements.size() == 0) return empty(); - ACell[] cells=Utils.toCellArray(elements.toArray()); - return (AVector) create(cells); - } - - public static AVector create(ASequence list) { - if (list instanceof AVector) return (AVector) list; - if (list.size() == 0) return empty(); - return create(list.toCellArray()); - } - - - @SuppressWarnings("unchecked") - public static AVector empty() { - return (AVector) VectorLeaf.EMPTY; - } - - /** - * Creates a vector with the given values. Performs conversion to CVM types. - * @param Type of elements (after CVM conversion) - * @param elements Elements to include - * @return New Vector - */ - @SuppressWarnings("unchecked") - @SafeVarargs - public static AVector of(Object... elements) { - int n=elements.length; - ACell[] es= new ACell[n]; - for (int i=0; i AVector repeat(T m, int count) { - ACell[] obs = new ACell[count]; - Arrays.fill(obs, m); - return (AVector) create(obs); - } - - /** - * Reads a Vector for the specified bytebuffer. Assumes Tag byte already consumed. - * - * Distinguishes between child types according to count. - * - * @param Type of elements - * @param bb ByteBuffer to read from - * @return Vector read from ByteBuffer - * @throws BadFormatException If encoding is invalid - */ - public static AVector read(ByteBuffer bb) throws BadFormatException { - long count = Format.readVLCLong(bb); - if ((count <= VectorLeaf.MAX_SIZE) || ((count & 0x0F) != 0)) { - return VectorLeaf.read(bb, count); - } else { - return VectorTree.read(bb, count); - } - } - -} diff --git a/convex-core/src/main/java/convex/core/data/package-info.java b/convex-core/src/main/java/convex/core/data/package-info.java deleted file mode 100644 index d5d3b05c5..000000000 --- a/convex-core/src/main/java/convex/core/data/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Data structures and algorithms, including a complete set of classes - * required to implement immutable, decentralised data objects. - */ -package convex.core.data; \ No newline at end of file diff --git a/convex-core/src/main/java/convex/core/data/prim/APrimitive.java b/convex-core/src/main/java/convex/core/data/prim/APrimitive.java deleted file mode 100644 index e29caa9de..000000000 --- a/convex-core/src/main/java/convex/core/data/prim/APrimitive.java +++ /dev/null @@ -1,66 +0,0 @@ -package convex.core.data.prim; - -import convex.core.data.ACell; -import convex.core.data.Ref; -import convex.core.data.RefDirect; - -/** - * Abstract base class for small CVM primitive values. - * - * Primitives never contain Refs, are always embedded, and are always canonical - */ -public abstract class APrimitive extends ACell { - @Override - public final boolean isCanonical() { - return true; - } - - @Override - @SuppressWarnings("unchecked") - protected Ref createRef() { - // Create Ref at maximum status to reflect internal embedded nature - Ref newRef= RefDirect.create(this,cachedHash(),Ref.INTERNAL_FLAGS); - cachedRef=newRef; - return (Ref) newRef; - } - - @Override - public final int getRefCount() { - return 0; - } - - @Override - public final boolean isEmbedded() { - return true; - } - - @Override public final boolean isCVMValue() { - return true; - } - - @Override - protected long calcMemorySize() { - // always embedded and no child Refs, so memory size == 0 - return 0; - } - - /** - * @return long value representing primitive - */ - public abstract long longValue(); - - - /** - * @return double value representing primitive - */ - public abstract double doubleValue(); - - @Override - public ACell toCanonical() { - // Always canonical, probably? - return this; - } - - - -} diff --git a/convex-core/src/main/java/convex/core/data/prim/CVMBool.java b/convex-core/src/main/java/convex/core/data/prim/CVMBool.java deleted file mode 100644 index df41c26ff..000000000 --- a/convex-core/src/main/java/convex/core/data/prim/CVMBool.java +++ /dev/null @@ -1,101 +0,0 @@ -package convex.core.data.prim; - -import convex.core.data.ACell; -import convex.core.data.Tag; -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.InvalidDataException; - -/** - * Class for CVM Boolean types. - * - * Two canonical values are provided, TRUE and FALSE. No other instances should exist. - */ -public final class CVMBool extends APrimitive { - - private final boolean value; - - public static final CVMBool TRUE=new CVMBool(true); - public static final CVMBool FALSE=new CVMBool(false); - - private CVMBool(boolean value) { - this.value=value; - } - - @Override - public AType getType() { - return Types.BOOLEAN; - } - - - public static CVMBool create(boolean value) { - return value?TRUE:FALSE; - } - - /** - * Get the canonical CVMBool value for true or false - * - * @param b Boolean specifying - * @return CVMBool value representing false or true - */ - public static CVMBool of(boolean b) { - return b?TRUE:FALSE; - } - - - @Override - public long longValue() { - return value?1:0; - } - - @Override - public int estimatedEncodingSize() { - return 1; - } - - @Override - public void validateCell() throws InvalidDataException { - // Nothing to check. Always valid - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=value?Tag.TRUE:Tag.FALSE; - return pos; - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - throw new UnsupportedOperationException("Not meaningful to encode raw data for CVMBool"); - } - - @Override - public void print(StringBuilder sb) { - sb.append(value?"true":"false"); - } - - @Override - public double doubleValue() { - return value?1:0; - } - - public boolean booleanValue() { - return value; - } - - @Override - public byte getTag() { - return (value)?Tag.TRUE:Tag.FALSE; - } - - public static ACell parse(String text) { - if ("true".equals(text)) return TRUE; - if ("false".equals(text)) return FALSE; - return null; - } - - - - - -} diff --git a/convex-core/src/main/java/convex/core/data/prim/CVMByte.java b/convex-core/src/main/java/convex/core/data/prim/CVMByte.java deleted file mode 100644 index d7ed8397a..000000000 --- a/convex-core/src/main/java/convex/core/data/prim/CVMByte.java +++ /dev/null @@ -1,117 +0,0 @@ -package convex.core.data.prim; - -import convex.core.data.INumeric; -import convex.core.data.Tag; -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.InvalidDataException; - -/** - * Class for CVM Byte instances. - * - * Bytes are unsigned 8-bit integers which upcast to long for numerical operations. - * - */ -public final class CVMByte extends APrimitive implements INumeric { - - private final byte value; - - private static final CVMByte[] CACHE= new CVMByte[256]; - - public static final CVMByte ZERO; - public static final CVMByte ONE; - - // Private constructor to enforce singleton instances - private CVMByte(byte value) { - this.value=value; - } - - public static CVMByte create(long value) { - return CACHE[((int)(value))&0xFF]; - } - - static { - for (int i=0; i<256; i++) { - CACHE[i]=new CVMByte((byte)i); - } - ZERO=CACHE[0]; - ONE=CACHE[1]; - } - - public AType getType() { - return Types.BYTE; - } - - @Override - public long longValue() { - return 0xFFL&value; - } - - @Override - public int estimatedEncodingSize() { - return 2; - } - - @Override - public void validateCell() throws InvalidDataException { - // Nothing to check. Always valid - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.BYTE; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - bs[pos++]=value; - return pos; - } - - @Override - public void print(StringBuilder sb) { - sb.append(longValue()); - } - - @Override - public Class numericType() { - return Long.class; - } - - @Override - public double doubleValue() { - return (double)longValue(); - } - - @Override - public byte getTag() { - return Tag.BYTE; - } - - @Override - public CVMLong toLong() { - return CVMLong.create(longValue()); - } - - @Override - public CVMDouble toDouble() { - return CVMDouble.create(doubleValue()); - } - - @Override - public CVMLong signum() { - if (value==0) return CVMLong.ZERO; - return CVMLong.ONE; - } - - public byte byteValue() { - return value; - } - - @Override - public INumeric toStandardNumber() { - return toLong(); - } - -} diff --git a/convex-core/src/main/java/convex/core/data/prim/CVMChar.java b/convex-core/src/main/java/convex/core/data/prim/CVMChar.java deleted file mode 100644 index 23f68a1d9..000000000 --- a/convex-core/src/main/java/convex/core/data/prim/CVMChar.java +++ /dev/null @@ -1,132 +0,0 @@ -package convex.core.data.prim; - -import convex.core.data.Tag; -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.reader.ReaderUtils; -import convex.core.util.Utils; - -/** - * Class for CVM character values. - * - * Chars are 16-bit UTF-16 unsigned integers, and are the elements of Strings CVM. - */ -public final class CVMChar extends APrimitive { - - public static final CVMChar A = CVMChar.create('a'); - - private final char value; - - public CVMChar(char value) { - this.value=value; - } - - @Override - public AType getType() { - return Types.CHARACTER; - } - - - public static CVMChar create(long value) { - return new CVMChar((char)value); - } - - @Override - public long longValue() { - return value; - } - - @Override - public int estimatedEncodingSize() { - return 1+2; - } - - @Override - public void validateCell() throws InvalidDataException { - // Nothing to check. Always valid - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.CHAR; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - return Utils.writeChar(bs,pos,((char)value)); - } - - @Override - public void print(StringBuilder sb) { - // Prints like EDN. - // Characters are preceded by a backslash: \c, \newline, \return, \space and - // \tab yield - // the corresponding characters. - // Unicode characters are represented as in Java. - // Backslash cannot be followed by whitespace. - // - String s; - switch(value) { - case '\n': s = "\\newline"; break; - case '\r': s = "\\return"; break; - case ' ': s = "\\space"; break; - case '\t': s = "\\tab"; break; - default: s = "\\" + Character.toString(value); - } - sb.append(s); - } - - /** - * Returns the String representation of this CVMChar. - * - * Different from {@link #print() print()} which returns a readable representation. - * - * For instance, on CVMChar \a, this methods returns "a" while {@link #print() print()} returns "\a". - */ - @Override - public String toString() { - // Usually, primitive types are stringified using `print`. This method - return Character.toString(value); - } - - @Override - public double doubleValue() { - return (double)value; - } - - /** - * Parses a Character from a String - * @param s String to parse - * @return CVMChar instance, or null if not valid - */ - public static CVMChar parse(String s) { - int n=s.length(); - - if (n<2) return null; - - if (n==2) { - return CVMChar.create(s.charAt(1)); - } - - if (s.charAt(1)=='u') { - if (n==6) { - char c = (char) Long.parseLong(s.substring(2),16); - return CVMChar.create(c); - } - } - - s=s.substring(1); - return ReaderUtils.specialCharacter(s); - } - - public char charValue() { - return value; - } - - @Override - public byte getTag() { - return Tag.CHAR; - } -} diff --git a/convex-core/src/main/java/convex/core/data/prim/CVMDouble.java b/convex-core/src/main/java/convex/core/data/prim/CVMDouble.java deleted file mode 100644 index 49323dea8..000000000 --- a/convex-core/src/main/java/convex/core/data/prim/CVMDouble.java +++ /dev/null @@ -1,130 +0,0 @@ -package convex.core.data.prim; - -import convex.core.data.INumeric; -import convex.core.data.Tag; -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.InvalidDataException; -import convex.core.util.Utils; - -/** - * Class for CVM double floating-point values. - * - * Follows the Java standard / IEEE 784 spec. - */ -public final class CVMDouble extends APrimitive implements INumeric { - - public static final CVMDouble ZERO = CVMDouble.create(0.0); - public static final CVMDouble NEGATIVE_ZERO = CVMDouble.create(-0.0); - public static final CVMDouble ONE = CVMDouble.create(1.0); - public static final CVMDouble MINUS_ONE = CVMDouble.create(-1.0); - - public static final CVMDouble NaN = CVMDouble.create(Double.NaN); - public static final CVMDouble POSITIVE_INFINITY = CVMDouble.create(Double.POSITIVE_INFINITY); - public static final CVMDouble NEGATIVE_INFINITY = CVMDouble.create(Double.NEGATIVE_INFINITY); - - private final double value; - - public CVMDouble(double value) { - this.value=value; - } - - public static CVMDouble create(double value) { - return new CVMDouble(value); - } - - @Override - public AType getType() { - return Types.DOUBLE; - } - - @Override - public long longValue() { - return (long)value; - } - - @Override - public CVMLong toLong() { - return CVMLong.create(longValue()); - } - - @Override - public CVMDouble toDouble() { - return this; - } - - @Override - public CVMDouble signum() { - if (value>0.0) return CVMDouble.ONE; - if (value<0.0) return CVMDouble.MINUS_ONE; - if (Double.isNaN(value)) return NaN; // NaN special case - return this; - } - - @Override - public int estimatedEncodingSize() { - return 1+8; - } - - @Override - public void validateCell() throws InvalidDataException { - // Nothing to check. Always valid - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.DOUBLE; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - long doubleBits=Double.doubleToRawLongBits(value); - return Utils.writeLong(bs,pos,doubleBits); - } - - @Override - public String toString() { - if (Double.isInfinite(value)) { - if (value>0.0) { - return "##Inf"; - } else { - return "##-Inf"; - } - } else if (Double.isNaN(value)) { - return "##NaN"; - } else { - return Double.toString(value); - } - } - - @Override - public void print(StringBuilder sb) { - sb.append(toString()); - } - - @Override - public Class numericType() { - return Double.class; - } - - @Override - public double doubleValue() { - return value; - } - - public static CVMDouble parse(String s) { - return create(Double.parseDouble(s)); - } - - @Override - public byte getTag() { - return Tag.DOUBLE; - } - - @Override - public INumeric toStandardNumber() { - return this; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/prim/CVMLong.java b/convex-core/src/main/java/convex/core/data/prim/CVMLong.java deleted file mode 100644 index 7d400dc5f..000000000 --- a/convex-core/src/main/java/convex/core/data/prim/CVMLong.java +++ /dev/null @@ -1,129 +0,0 @@ -package convex.core.data.prim; - -import convex.core.data.Format; -import convex.core.data.INumeric; -import convex.core.data.Tag; -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.InvalidDataException; - -/** - * Class for CVM long values. - * - * Longs are signed 64-bit integers, and are the primary fixed point integer type on the CVM. - */ -public final class CVMLong extends APrimitive implements INumeric { - - private static final int CACHE_SIZE = 256; - private static final CVMLong[] CACHE= new CVMLong[CACHE_SIZE]; - - static { - for (int i=0; i<256; i++) { - CACHE[i]=new CVMLong(i); - } - ZERO=CACHE[0]; - ONE=CACHE[1]; - } - - public static final CVMLong ZERO; - public static final CVMLong ONE; - public static final CVMLong MINUS_ONE = CVMLong.create(-1L); - public static final CVMLong MAX_VALUE = CVMLong.create(Long.MAX_VALUE); - public static final CVMLong MIN_VALUE = CVMLong.create(Long.MIN_VALUE); - - private final long value; - - public CVMLong(long value) { - this.value=value; - } - - public static CVMLong create(long value) { - if ((value=0)) { - return CACHE[(int)value]; - } - return new CVMLong(value); - } - - @Override - public AType getType() { - return Types.LONG; - } - - @Override - public long longValue() { - return value; - } - - @Override - public CVMLong toLong() { - return this; - } - - @Override - public CVMDouble toDouble() { - return CVMDouble.create(doubleValue()); - } - - @Override - public int estimatedEncodingSize() { - return 1+Format.MAX_VLC_LONG_LENGTH; - } - - @Override - public void validateCell() throws InvalidDataException { - // Nothing to check. Always valid - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.LONG; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - return Format.writeVLCLong(bs, pos, value); - } - - @Override - public void print(StringBuilder sb) { - sb.append(value); - } - - @Override - public Class numericType() { - return Long.class; - } - - @Override - public double doubleValue() { - return (double)value; - } - - /** - * Parse a String as a CVM Long. Throws an exception if the string is not valid - * @param s String to parse - * @return CVM Long value - */ - public static CVMLong parse(String s) { - return create(Long.parseLong(s)); - } - - @Override - public byte getTag() { - return Tag.LONG; - } - - @Override - public CVMLong signum() { - if (value>0) return CVMLong.ONE; - if (value<0) return CVMLong.MINUS_ONE; - return CVMLong.ZERO; - } - - @Override - public INumeric toStandardNumber() { - return this; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/type/ANumericType.java b/convex-core/src/main/java/convex/core/data/type/ANumericType.java deleted file mode 100644 index 64023937c..000000000 --- a/convex-core/src/main/java/convex/core/data/type/ANumericType.java +++ /dev/null @@ -1,11 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.prim.APrimitive; - -public abstract class ANumericType extends AStandardType { - - protected ANumericType(Class klass) { - super(klass); - } - -} diff --git a/convex-core/src/main/java/convex/core/data/type/AStandardType.java b/convex-core/src/main/java/convex/core/data/type/AStandardType.java deleted file mode 100644 index 1e6e679f2..000000000 --- a/convex-core/src/main/java/convex/core/data/type/AStandardType.java +++ /dev/null @@ -1,42 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; - -/** - * Base type for standard types mapped directly to a branch of ACell hierarchy - * @param Java Type (common superclass) - */ -public abstract class AStandardType extends AType { - - Class klass; - - protected AStandardType(Class klass) { - this.klass=klass; - } - - @Override - public boolean check(ACell value) { - return klass.isInstance(value); - } - - @Override - public final boolean allowsNull() { - return false; - } - - @Override - public abstract T defaultValue(); - - @SuppressWarnings("unchecked") - @Override - public T implicitCast(ACell a) { - if (check(a)) return (T)a; - return null; - } - - @Override - public final Class getJavaClass() { - return klass; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/type/AType.java b/convex-core/src/main/java/convex/core/data/type/AType.java deleted file mode 100644 index fab808398..000000000 --- a/convex-core/src/main/java/convex/core/data/type/AType.java +++ /dev/null @@ -1,46 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; - -/** - * Abstract base class for CVM value types - */ -public abstract class AType { - - /** - * Checks if a value is an instance of this Type. - * @param value Any CVM value - * @return true if value is an instance of this Type, false otherwise - */ - public abstract boolean check(ACell value); - - /** - * Checks if this type allows a null value. - * - * @return True if this type allows null values, false otherwise - */ - public abstract boolean allowsNull(); - - @Override - public abstract String toString(); - - /** - * Gets the default value for this type. May return null. - * @return Default value for the given Type - */ - public abstract ACell defaultValue(); - - /** - * Gets the default value for this type. Returns null if the cast fails. - * @param a Value to cast - * @return Value cast to this Type, or null if the cast fails - */ - public abstract ACell implicitCast(ACell a); - - /** - * Gets the Java common base class for all instances of this type. - * - * @return Java Class representing this Type - */ - public abstract Class getJavaClass(); -} diff --git a/convex-core/src/main/java/convex/core/data/type/AddressType.java b/convex-core/src/main/java/convex/core/data/type/AddressType.java deleted file mode 100644 index 727ece567..000000000 --- a/convex-core/src/main/java/convex/core/data/type/AddressType.java +++ /dev/null @@ -1,39 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.Address; - -/** - * Type that represents CVM Byte values - */ -public final class AddressType extends AStandardType
{ - /** - * Singleton runtime instance - */ - public static final AddressType INSTANCE = new AddressType(); - - private AddressType() { - super (Address.class); - } - - @Override - public boolean check(ACell value) { - return value instanceof Address; - } - - @Override - public String toString () { - return "Address"; - } - - @Override - public Address defaultValue() { - return Address.ZERO; - } - - @Override - public Address implicitCast(ACell a) { - if (a instanceof Address) return (Address)a; - return null; - } -} diff --git a/convex-core/src/main/java/convex/core/data/type/Any.java b/convex-core/src/main/java/convex/core/data/type/Any.java deleted file mode 100644 index f684f5e4b..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Any.java +++ /dev/null @@ -1,46 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; - -/** - * Type that represents any CVM value - */ -public class Any extends AType { - - public static final Any INSTANCE = new Any(); - - private Any() { - - } - - @Override - public boolean check(ACell value) { - return true; - } - - @Override - public boolean allowsNull() { - return true; - } - - @Override - public String toString() { - return "Any"; - } - - @Override - public ACell defaultValue() { - return null; - } - - @Override - public ACell implicitCast(ACell a) { - return a; - } - - @Override - public Class getJavaClass() { - return ACell.class; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/type/Blob.java b/convex-core/src/main/java/convex/core/data/type/Blob.java deleted file mode 100644 index 08713ce27..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Blob.java +++ /dev/null @@ -1,39 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ABlob; -import convex.core.data.ACell; - -/** - * Type that represents any Blob - */ -public class Blob extends AStandardType { - - public static final Blob INSTANCE = new Blob(); - - private Blob() { - super(ABlob.class); - } - - @Override - public boolean check(ACell value) { - return (value instanceof ABlob); - } - - @Override - public String toString() { - return "Blob"; - } - - @Override - public ABlob defaultValue() { - return convex.core.data.Blob.EMPTY; - } - - @Override - public ABlob implicitCast(ACell a) { - if (a instanceof ABlob) return (ABlob)a; - return null; - } - - -} diff --git a/convex-core/src/main/java/convex/core/data/type/BlobMapType.java b/convex-core/src/main/java/convex/core/data/type/BlobMapType.java deleted file mode 100644 index 49b70e921..000000000 --- a/convex-core/src/main/java/convex/core/data/type/BlobMapType.java +++ /dev/null @@ -1,41 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ABlobMap; -import convex.core.data.ACell; -import convex.core.data.BlobMap; -import convex.core.data.BlobMaps; - -/** - * Type that represents any CVM map - */ -@SuppressWarnings("rawtypes") -public class BlobMapType extends AStandardType { - - public static final BlobMapType INSTANCE = new BlobMapType(); - - private BlobMapType() { - super(ABlobMap.class); - } - - @Override - public boolean check(ACell value) { - return (value instanceof ABlobMap); - } - - @Override - public String toString() { - return "BlobMap"; - } - - @Override - public ABlobMap defaultValue() { - return BlobMaps.empty(); - } - - @Override - public ABlobMap implicitCast(ACell a) { - if (a instanceof BlobMap) return (BlobMap)a; - return null; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/type/Boolean.java b/convex-core/src/main/java/convex/core/data/type/Boolean.java deleted file mode 100644 index 56cfb9caf..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Boolean.java +++ /dev/null @@ -1,40 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.prim.CVMBool; -import convex.core.lang.RT; - -/** - * Type that represents CVM Long values - */ -public final class Boolean extends AStandardType { - /** - * Singleton runtime instance - */ - public static final Boolean INSTANCE = new Boolean(); - - private Boolean() { - super(CVMBool.class); - } - - @Override - public boolean check(ACell value) { - return value instanceof CVMBool; - } - - @Override - public String toString () { - return "Boolean"; - } - - @Override - public CVMBool defaultValue() { - return CVMBool.FALSE; - } - - @Override - public CVMBool implicitCast(ACell a) { - return RT.bool(a)?CVMBool.TRUE:CVMBool.FALSE; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/type/Byte.java b/convex-core/src/main/java/convex/core/data/type/Byte.java deleted file mode 100644 index 1b2267f78..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Byte.java +++ /dev/null @@ -1,40 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.prim.CVMByte; - -/** - * Type that represents CVM Byte values - */ -public final class Byte extends ANumericType { - - /** - * Singleton runtime instance - */ - public static final Byte INSTANCE = new Byte(); - - private Byte() { - super (CVMByte.class); - } - - @Override - public boolean check(ACell value) { - return value instanceof CVMByte; - } - - @Override - public String toString () { - return "Byte"; - } - - @Override - public CVMByte defaultValue() { - return CVMByte.ZERO; - } - - @Override - public CVMByte implicitCast(ACell a) { - if (a instanceof CVMByte) return (CVMByte)a; - return null; - } -} diff --git a/convex-core/src/main/java/convex/core/data/type/CharacterType.java b/convex-core/src/main/java/convex/core/data/type/CharacterType.java deleted file mode 100644 index cfd17f07c..000000000 --- a/convex-core/src/main/java/convex/core/data/type/CharacterType.java +++ /dev/null @@ -1,40 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.prim.CVMChar; - -/** - * Type that represents CVM Byte values - */ -public final class CharacterType extends AStandardType { - - /** - * Singleton runtime instance - */ - public static final CharacterType INSTANCE = new CharacterType(); - - private CharacterType() { - super(CVMChar.class); - } - - @Override - public boolean check(ACell value) { - return value instanceof CVMChar; - } - - @Override - public String toString () { - return "Character"; - } - - @Override - public CVMChar defaultValue() { - return CVMChar.A; - } - - @Override - public CVMChar implicitCast(ACell a) { - if (a instanceof CVMChar) return (CVMChar)a; - return null; - } -} diff --git a/convex-core/src/main/java/convex/core/data/type/Collection.java b/convex-core/src/main/java/convex/core/data/type/Collection.java deleted file mode 100644 index b547448e2..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Collection.java +++ /dev/null @@ -1,39 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.ACollection; -import convex.core.data.Vectors; - -/** - * Type that represents any CVM collection - */ -@SuppressWarnings("rawtypes") -public class Collection extends AStandardType { - - public static final Collection INSTANCE = new Collection(); - - private Collection() { - super (ACollection.class); - } - - @Override - public boolean check(ACell value) { - return (value instanceof ACollection); - } - - @Override - public String toString() { - return "Collection"; - } - - @Override - public ACollection defaultValue() { - return Vectors.empty(); - } - - @Override - public ACollection implicitCast(ACell a) { - if (a instanceof ACollection) return (ACollection)a; - return null; - } -} diff --git a/convex-core/src/main/java/convex/core/data/type/DataStructure.java b/convex-core/src/main/java/convex/core/data/type/DataStructure.java deleted file mode 100644 index 512b85541..000000000 --- a/convex-core/src/main/java/convex/core/data/type/DataStructure.java +++ /dev/null @@ -1,40 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.ADataStructure; -import convex.core.data.Vectors; - -/** - * Type that represents any CVM sequence - */ -@SuppressWarnings("rawtypes") -public class DataStructure extends AStandardType { - - public static final DataStructure INSTANCE = new DataStructure(); - - private DataStructure() { - super(ADataStructure.class); - } - - @Override - public boolean check(ACell value) { - return (value instanceof ADataStructure); - } - - @Override - public String toString() { - return "DataStructure"; - } - - @Override - public ADataStructure defaultValue() { - return Vectors.empty(); - } - - @Override - public ADataStructure implicitCast(ACell a) { - if (a instanceof ADataStructure) return (ADataStructure)a; - return null; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/type/Double.java b/convex-core/src/main/java/convex/core/data/type/Double.java deleted file mode 100644 index 1fe10ffac..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Double.java +++ /dev/null @@ -1,40 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.prim.CVMDouble; -import convex.core.lang.RT; - -/** - * Type that represents CVM Double values - */ -public final class Double extends ANumericType { - - /** - * Singleton runtime instance - */ - public static final Double INSTANCE = new Double(); - - private Double() { - super (CVMDouble.class); - } - - @Override - public boolean check(ACell value) { - return value instanceof CVMDouble; - } - - @Override - public String toString () { - return "Double"; - } - - @Override - public CVMDouble defaultValue() { - return CVMDouble.ZERO; - } - - @Override - public CVMDouble implicitCast(ACell a) { - return RT.ensureDouble(a); - } -} diff --git a/convex-core/src/main/java/convex/core/data/type/Function.java b/convex-core/src/main/java/convex/core/data/type/Function.java deleted file mode 100644 index 896b3ddc5..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Function.java +++ /dev/null @@ -1,40 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.lang.AFn; -import convex.core.lang.Core; - -/** - * Type that represents any CVM collection - */ -@SuppressWarnings("rawtypes") -public class Function extends AStandardType { - - public static final Function INSTANCE = new Function(); - - private Function() { - super(AFn.class); - } - - @Override - public boolean check(ACell value) { - return (value instanceof AFn); - } - - @Override - public String toString() { - return "Function"; - } - - @Override - public AFn defaultValue() { - return Core.VECTOR; - } - - @Override - public AFn implicitCast(ACell a) { - if (a instanceof AFn) return (AFn)a; - return null; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/type/KeywordType.java b/convex-core/src/main/java/convex/core/data/type/KeywordType.java deleted file mode 100644 index 16a3e8088..000000000 --- a/convex-core/src/main/java/convex/core/data/type/KeywordType.java +++ /dev/null @@ -1,41 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.Keyword; -import convex.core.data.Keywords; - -/** - * Type that represents CVM Byte values - */ -public final class KeywordType extends AStandardType { - - /** - * Singleton runtime instance - */ - public static final KeywordType INSTANCE = new KeywordType(); - - private KeywordType() { - super(Keyword.class); - } - - @Override - public boolean check(ACell value) { - return value instanceof Keyword; - } - - @Override - public String toString () { - return "Keyword"; - } - - @Override - public Keyword defaultValue() { - return Keywords.FOO; - } - - @Override - public Keyword implicitCast(ACell a) { - if (a instanceof Keyword) return (Keyword)a; - return null; - } -} diff --git a/convex-core/src/main/java/convex/core/data/type/List.java b/convex-core/src/main/java/convex/core/data/type/List.java deleted file mode 100644 index a1335a853..000000000 --- a/convex-core/src/main/java/convex/core/data/type/List.java +++ /dev/null @@ -1,39 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.AList; -import convex.core.data.Lists; - -/** - * Type that represents any CVM collection - */ -@SuppressWarnings("rawtypes") -public class List extends AStandardType { - - public static final List INSTANCE = new List(); - - private List() { - super(AList.class); - } - - @Override - public boolean check(ACell value) { - return (value instanceof AList); - } - - @Override - public String toString() { - return "List"; - } - - @Override - public AList defaultValue() { - return Lists.empty(); - } - - @Override - public AList implicitCast(ACell a) { - if (a instanceof AList) return (AList)a; - return null; - } -} diff --git a/convex-core/src/main/java/convex/core/data/type/Long.java b/convex-core/src/main/java/convex/core/data/type/Long.java deleted file mode 100644 index 7b333b18d..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Long.java +++ /dev/null @@ -1,40 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.prim.CVMLong; -import convex.core.lang.RT; - -/** - * Type that represents CVM Long values - */ -public final class Long extends ANumericType { - - /** - * Singleton runtime instance - */ - public static final Long INSTANCE = new Long(); - - private Long() { - super (CVMLong.class); - } - - @Override - public boolean check(ACell value) { - return value instanceof CVMLong; - } - - @Override - public String toString () { - return "Long"; - } - - @Override - public CVMLong defaultValue() { - return CVMLong.ZERO; - } - - @Override - public CVMLong implicitCast(ACell a) { - return RT.ensureLong(a); - } -} diff --git a/convex-core/src/main/java/convex/core/data/type/Map.java b/convex-core/src/main/java/convex/core/data/type/Map.java deleted file mode 100644 index 8cada737a..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Map.java +++ /dev/null @@ -1,40 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.AMap; -import convex.core.data.Maps; - -/** - * Type that represents any CVM map - */ -@SuppressWarnings("rawtypes") -public class Map extends AStandardType { - - public static final Map INSTANCE = new Map(); - - private Map() { - super(AMap.class); - } - - @Override - public boolean check(ACell value) { - return (value instanceof AMap); - } - - @Override - public String toString() { - return "Map"; - } - - @Override - public AMap defaultValue() { - return Maps.empty(); - } - - @Override - public AMap implicitCast(ACell a) { - if (a instanceof AMap) return (AMap)a; - return null; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/type/Nil.java b/convex-core/src/main/java/convex/core/data/type/Nil.java deleted file mode 100644 index 2ba1e5f20..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Nil.java +++ /dev/null @@ -1,47 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; - -/** - * The Type representing the single value 'nil' - */ -public class Nil extends AType { - - public static final Nil INSTANCE = new Nil(); - - private Nil() { - - } - - @Override - public boolean check(ACell value) { - return value==null; - } - - @Override - public boolean allowsNull() { - return true; - } - - @Override - public String toString() { - return "Nil"; - } - - @Override - public ACell defaultValue() { - return null; - } - - @Override - public ACell implicitCast(ACell a) { - // TODO: confirm anything can cast to null? - return null; - } - - @Override - public Class getJavaClass() { - return ACell.class; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/type/Number.java b/convex-core/src/main/java/convex/core/data/type/Number.java deleted file mode 100644 index 6dce5a37a..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Number.java +++ /dev/null @@ -1,48 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.INumeric; -import convex.core.data.prim.APrimitive; -import convex.core.data.prim.CVMLong; -import convex.core.lang.RT; - -public class Number extends AType { - - public static final Number INSTANCE = new Number(); - - private Number() { - - } - - @Override - public boolean check(ACell value) { - return RT.isNumber(value); - } - - @Override - public boolean allowsNull() { - return false; - } - - @Override - public String toString() { - return "Number"; - } - - @Override - public APrimitive defaultValue() { - return CVMLong.ZERO; - } - - @Override - public APrimitive implicitCast(ACell a) { - if (a instanceof INumeric) return (APrimitive)a; - return null; - } - - @Override - public Class getJavaClass() { - return APrimitive.class; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/type/OpCode.java b/convex-core/src/main/java/convex/core/data/type/OpCode.java deleted file mode 100644 index 8617b2bc7..000000000 --- a/convex-core/src/main/java/convex/core/data/type/OpCode.java +++ /dev/null @@ -1,42 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.lang.AOp; -import convex.core.lang.ops.Do; - -/** - * Type that represents CVM Long values - */ -@SuppressWarnings("rawtypes") -public final class OpCode extends AStandardType { - /** - * Singleton runtime instance - */ - public static final OpCode INSTANCE = new OpCode(); - - private OpCode() { - super(AOp.class); - } - - @Override - public boolean check(ACell value) { - return value instanceof AOp; - } - - @Override - public String toString () { - return "Op"; - } - - @Override - public AOp defaultValue() { - return Do.EMPTY; - } - - @Override - public AOp implicitCast(ACell a) { - if (a instanceof AOp) return (AOp)a; - return null; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/type/Record.java b/convex-core/src/main/java/convex/core/data/type/Record.java deleted file mode 100644 index 22e2cf86d..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Record.java +++ /dev/null @@ -1,38 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.ARecord; - -/** - * Type that represents any CVM collection - */ -public class Record extends AStandardType { - - public static final Record INSTANCE = new Record(); - - private Record() { - super(ARecord.class); - } - - @Override - public boolean check(ACell value) { - return (value instanceof ARecord); - } - - @Override - public String toString() { - return "Record"; - } - - @Override - public ARecord defaultValue() { - return ARecord.DEFAULT_VALUE; - } - - @Override - public ARecord implicitCast(ACell a) { - if (a instanceof ARecord) return (ARecord)a; - return null; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/type/Sequence.java b/convex-core/src/main/java/convex/core/data/type/Sequence.java deleted file mode 100644 index bd93884c4..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Sequence.java +++ /dev/null @@ -1,40 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.ASequence; -import convex.core.data.AVector; -import convex.core.data.Vectors; - -/** - * Type that represents any CVM sequence - */ -@SuppressWarnings("rawtypes") -public class Sequence extends AStandardType { - - public static final Sequence INSTANCE = new Sequence(); - - private Sequence() { - super (ASequence.class); - } - - @Override - public boolean check(ACell value) { - return (value instanceof ASequence); - } - - @Override - public String toString() { - return "Sequence"; - } - - @Override - public AVector defaultValue() { - return Vectors.empty(); - } - - @Override - public ASequence implicitCast(ACell a) { - if (a instanceof ASequence) return (ASequence)a; - return null; - } -} diff --git a/convex-core/src/main/java/convex/core/data/type/Set.java b/convex-core/src/main/java/convex/core/data/type/Set.java deleted file mode 100644 index 97035f399..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Set.java +++ /dev/null @@ -1,39 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.ASet; -import convex.core.data.Sets; - -/** - * Type that represents any CVM collection - */ -@SuppressWarnings("rawtypes") -public class Set extends AStandardType { - - public static final Set INSTANCE = new Set(); - - private Set() { - super(ASet.class); - } - - @Override - public boolean check(ACell value) { - return (value instanceof ASet); - } - - @Override - public String toString() { - return "Set"; - } - - @Override - public ASet defaultValue() { - return Sets.empty(); - } - - @Override - public ASet implicitCast(ACell a) { - if (a instanceof ASet) return (ASet)a; - return null; - } -} diff --git a/convex-core/src/main/java/convex/core/data/type/StringType.java b/convex-core/src/main/java/convex/core/data/type/StringType.java deleted file mode 100644 index db0fec637..000000000 --- a/convex-core/src/main/java/convex/core/data/type/StringType.java +++ /dev/null @@ -1,41 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.AString; -import convex.core.data.Strings; - -/** - * Type that represents CVM Byte values - */ -public final class StringType extends AStandardType { - /** - * Singleton runtime instance - */ - public static final StringType INSTANCE = new StringType(); - - private StringType() { - super (AString.class); - } - - @Override - public boolean check(ACell value) { - return value instanceof AString; - } - - @Override - public String toString () { - return "String"; - } - - - @Override - public AString defaultValue() { - return Strings.EMPTY; - } - - @Override - public AString implicitCast(ACell a) { - if (a instanceof AString) return (AString)a; - return null; - } -} diff --git a/convex-core/src/main/java/convex/core/data/type/SymbolType.java b/convex-core/src/main/java/convex/core/data/type/SymbolType.java deleted file mode 100644 index 2dd7e9d61..000000000 --- a/convex-core/src/main/java/convex/core/data/type/SymbolType.java +++ /dev/null @@ -1,41 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.Symbol; -import convex.core.lang.Symbols; - -/** - * Type that represents CVM Byte values - */ -public final class SymbolType extends AStandardType { - - /** - * Singleton runtime instance - */ - public static final SymbolType INSTANCE = new SymbolType(); - - private SymbolType() { - super(Symbol.class); - } - - @Override - public boolean check(ACell value) { - return value instanceof Symbol; - } - - @Override - public String toString () { - return "Symbol"; - } - - @Override - public Symbol defaultValue() { - return Symbols.FOO; - } - - @Override - public Symbol implicitCast(ACell a) { - if (a instanceof Symbol) return (Symbol)a; - return null; - } -} diff --git a/convex-core/src/main/java/convex/core/data/type/SyntaxType.java b/convex-core/src/main/java/convex/core/data/type/SyntaxType.java deleted file mode 100644 index 97a1985e7..000000000 --- a/convex-core/src/main/java/convex/core/data/type/SyntaxType.java +++ /dev/null @@ -1,40 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.Syntax; - -/** - * Type that represents CVM Syntax Object values - */ -public final class SyntaxType extends AStandardType { - - /** - * Singleton runtime instance - */ - public static final SyntaxType INSTANCE = new SyntaxType(); - - private SyntaxType() { - super(Syntax.class); - } - - @Override - public boolean check(ACell value) { - return value instanceof Syntax; - } - - @Override - public String toString () { - return "Syntax"; - } - - @Override - public Syntax defaultValue() { - return Syntax.EMPTY; - } - - @Override - public Syntax implicitCast(ACell a) { - if (a instanceof Syntax) return (Syntax)a; - return null; - } -} diff --git a/convex-core/src/main/java/convex/core/data/type/Transaction.java b/convex-core/src/main/java/convex/core/data/type/Transaction.java deleted file mode 100644 index 036d7af50..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Transaction.java +++ /dev/null @@ -1,29 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Invoke; - -public class Transaction extends AStandardType{ - - protected Transaction() { - super(ATransaction.class); - } - - public static final Transaction INSTANCE = new Transaction(); - - public static final ATransaction DEFAULT = Invoke.create(Address.create(0), 0, (ACell)null); - - - @Override - public ATransaction defaultValue() { - return DEFAULT; - } - - @Override - public String toString() { - return "Transaction"; - } - -} diff --git a/convex-core/src/main/java/convex/core/data/type/Types.java b/convex-core/src/main/java/convex/core/data/type/Types.java deleted file mode 100644 index 14d8be44c..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Types.java +++ /dev/null @@ -1,87 +0,0 @@ -package convex.core.data.type; - -/** - * Static base class for Type system functionality - * - * NOTE: Currently Types are not planned for support in 1.0 runtime, but included here to support testing - * - */ -public class Types { - // Fundamental types - public static final Nil NIL=Nil.INSTANCE; - public static final Any ANY = Any.INSTANCE; - - // Collection types - public static final Collection COLLECTION=Collection.INSTANCE; - public static final Vector VECTOR=Vector.INSTANCE; - public static final List LIST=List.INSTANCE; - public static final Set SET=Set.INSTANCE; - - // Numeric types - public static final Long LONG=Long.INSTANCE; - public static final Byte BYTE = Byte.INSTANCE; - public static final Double DOUBLE = Double.INSTANCE; - public static final Number NUMBER = Number.INSTANCE; - - // Atomic types - public static final Boolean BOOLEAN = Boolean.INSTANCE; - public static final CharacterType CHARACTER = CharacterType.INSTANCE; - - // Named types - public static final KeywordType KEYWORD = KeywordType.INSTANCE; - public static final SymbolType SYMBOL = SymbolType.INSTANCE; - public static final StringType STRING = StringType.INSTANCE; - - // Data Structures - public static final DataStructure DATA_STRUCTURE = DataStructure.INSTANCE; - public static final Record RECORD = Record.INSTANCE; - public static final Map MAP = Map.INSTANCE; - public static final Sequence SEQUENCE = Sequence.INSTANCE; - - public static final BlobMapType BLOBMAP = BlobMapType.INSTANCE; - - public static final Blob BLOB = Blob.INSTANCE; - public static final AddressType ADDRESS = AddressType.INSTANCE; - - - public static final Function FUNCTION = Function.INSTANCE; - public static final OpCode OP = OpCode.INSTANCE; - public static final SyntaxType SYNTAX=SyntaxType.INSTANCE; - - public static final Transaction TRANSACTION=Transaction.INSTANCE; - - - - public static AType[] ALL_TYPES=new AType[] { - NIL, - ANY, - - COLLECTION, - DATA_STRUCTURE, - RECORD, - SEQUENCE, - VECTOR, - MAP, - LIST, - SET, - BLOBMAP, - - NUMBER, - LONG, - BYTE, - DOUBLE, - - BOOLEAN, - CHARACTER, - STRING, - KEYWORD, - SYMBOL, - - BLOB, - ADDRESS, - - FUNCTION, - OP, - SYNTAX, - }; -} diff --git a/convex-core/src/main/java/convex/core/data/type/Vector.java b/convex-core/src/main/java/convex/core/data/type/Vector.java deleted file mode 100644 index ec859ca60..000000000 --- a/convex-core/src/main/java/convex/core/data/type/Vector.java +++ /dev/null @@ -1,40 +0,0 @@ -package convex.core.data.type; - -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.Vectors; - -/** - * Type that represents any CVM collection - */ -@SuppressWarnings("rawtypes") -public class Vector extends AStandardType { - - public static final Vector INSTANCE = new Vector(); - - private Vector() { - super(AVector.class); - } - - @Override - public boolean check(ACell value) { - return (value instanceof AVector); - } - - - @Override - public String toString() { - return "Vector"; - } - - @Override - public AVector defaultValue() { - return Vectors.empty(); - } - - @Override - public AVector implicitCast(ACell a) { - if (a instanceof AVector) return (AVector)a; - return null; - } -} diff --git a/convex-core/src/main/java/convex/core/exceptions/BadFormatException.java b/convex-core/src/main/java/convex/core/exceptions/BadFormatException.java deleted file mode 100644 index a1e1e7311..000000000 --- a/convex-core/src/main/java/convex/core/exceptions/BadFormatException.java +++ /dev/null @@ -1,21 +0,0 @@ -package convex.core.exceptions; - -/** - * Class representing errors in format encountered when trying to read data from - * a serialised form. - * - * - * - */ -@SuppressWarnings("serial") -public class BadFormatException extends ValidationException { - - public BadFormatException(String message) { - super(message); - } - - public BadFormatException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/convex-core/src/main/java/convex/core/exceptions/BadSignatureException.java b/convex-core/src/main/java/convex/core/exceptions/BadSignatureException.java deleted file mode 100644 index 500f32d43..000000000 --- a/convex-core/src/main/java/convex/core/exceptions/BadSignatureException.java +++ /dev/null @@ -1,21 +0,0 @@ -package convex.core.exceptions; - -import convex.core.data.ACell; -import convex.core.data.SignedData; - -@SuppressWarnings("serial") -public class BadSignatureException extends ValidationException { - - private SignedData sig; - - public BadSignatureException(String message, SignedData sig) { - super(message); - this.sig = sig; - } - - @SuppressWarnings("unchecked") - public SignedData getSignature() { - return (SignedData) sig; - } - -} diff --git a/convex-core/src/main/java/convex/core/exceptions/BaseException.java b/convex-core/src/main/java/convex/core/exceptions/BaseException.java deleted file mode 100644 index 1f7bc4278..000000000 --- a/convex-core/src/main/java/convex/core/exceptions/BaseException.java +++ /dev/null @@ -1,27 +0,0 @@ -package convex.core.exceptions; - -/** - * Abstract base class for exceptions that we expect to encounter and need to - * handle. - * - * "If you don’t handle [exceptions], we shut your application down. That - * dramatically increases the reliability of the system.” - Anders Hejlsberg - * - */ -@SuppressWarnings("serial") -public abstract class BaseException extends Exception { - - public BaseException(String message) { - super(message); - } - - public BaseException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public Throwable fillInStackTrace() { - return super.fillInStackTrace(); - // return this; // possible optimisation?? - } -} diff --git a/convex-core/src/main/java/convex/core/exceptions/InvalidDataException.java b/convex-core/src/main/java/convex/core/exceptions/InvalidDataException.java deleted file mode 100644 index 4f193b752..000000000 --- a/convex-core/src/main/java/convex/core/exceptions/InvalidDataException.java +++ /dev/null @@ -1,21 +0,0 @@ -package convex.core.exceptions; - -/** - * Class representing errors encountered during data validation. - * - * In general, InvalidDataException occurs if the data format is correct, but - * the data fails to satisfy a validation invariant. - */ -@SuppressWarnings("serial") -public class InvalidDataException extends ValidationException { - private final Object data; - - public InvalidDataException(String message, Object data) { - super(message); - this.data = data; - } - - public Object getData() { - return data; - } -} diff --git a/convex-core/src/main/java/convex/core/exceptions/MissingDataException.java b/convex-core/src/main/java/convex/core/exceptions/MissingDataException.java deleted file mode 100644 index c5ddfc72b..000000000 --- a/convex-core/src/main/java/convex/core/exceptions/MissingDataException.java +++ /dev/null @@ -1,37 +0,0 @@ -package convex.core.exceptions; - -import convex.core.data.Hash; -import convex.core.store.Stores; - -/** - * Exception thrown when an attempt is made to dereference a value that is not - * present in the current data store. - * - * Normally shouldn't be caught / referenced directly. Requires special handling - * by Peers. - * - */ -@SuppressWarnings("serial") -public class MissingDataException extends RuntimeException { - - private Hash hash; - - private MissingDataException(String message, Hash hash) { - super(message); - this.hash = hash; - } - - public MissingDataException(Hash hash) { - // TODO: remove inefficiency - this("Missing " + hash + " in store " + Stores.current().toString(), hash); - } - -// @Override -// public Throwable fillInStackTrace() { -// return this; -// } - - public Hash getMissingHash() { - return hash; - } -} diff --git a/convex-core/src/main/java/convex/core/exceptions/ParseException.java b/convex-core/src/main/java/convex/core/exceptions/ParseException.java deleted file mode 100644 index aed2352be..000000000 --- a/convex-core/src/main/java/convex/core/exceptions/ParseException.java +++ /dev/null @@ -1,17 +0,0 @@ -package convex.core.exceptions; - -/** - * Class for reader parse exceptions - * - */ -@SuppressWarnings("serial") -public class ParseException extends Error { - - public ParseException(String message) { - super(message); - } - - public ParseException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/convex-core/src/main/java/convex/core/exceptions/TODOException.java b/convex-core/src/main/java/convex/core/exceptions/TODOException.java deleted file mode 100644 index 9ce4317f4..000000000 --- a/convex-core/src/main/java/convex/core/exceptions/TODOException.java +++ /dev/null @@ -1,18 +0,0 @@ -package convex.core.exceptions; - -@SuppressWarnings("serial") -public class TODOException extends RuntimeException { - - public TODOException(String message) { - super("TODO: "+message); - } - - public TODOException() { - this("TODO"); - } - - public TODOException(Exception e) { - super("TODO: "+e.getMessage(),e); - } - -} diff --git a/convex-core/src/main/java/convex/core/exceptions/ValidationException.java b/convex-core/src/main/java/convex/core/exceptions/ValidationException.java deleted file mode 100644 index d789ff439..000000000 --- a/convex-core/src/main/java/convex/core/exceptions/ValidationException.java +++ /dev/null @@ -1,16 +0,0 @@ -package convex.core.exceptions; - -/** - * Class representing a validation failure - * - */ -@SuppressWarnings("serial") -public class ValidationException extends BaseException { - public ValidationException(String message) { - super(message); - } - - public ValidationException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/convex-core/src/main/java/convex/core/init/AInitConfig.java b/convex-core/src/main/java/convex/core/init/AInitConfig.java deleted file mode 100644 index 7079f0256..000000000 --- a/convex-core/src/main/java/convex/core/init/AInitConfig.java +++ /dev/null @@ -1,79 +0,0 @@ -package convex.core.init; - - -import convex.core.crypto.AKeyPair; -import convex.core.crypto.Ed25519KeyPair; -import convex.core.data.Address; - - -public class AInitConfig { - - protected AKeyPair userKeyPairs[]; - protected AKeyPair peerKeyPairs[]; - - - public int getUserCount() { - return userKeyPairs.length; - } - public int getPeerCount() { - return peerKeyPairs.length; - } - - public AKeyPair getUserKeyPair(int index) { - return userKeyPairs[index]; - } - public AKeyPair getPeerKeyPair(int index) { - return peerKeyPairs[index]; - } - - public AKeyPair[] getPeerKeyPairs() { - return peerKeyPairs; - } - - public Address getUserAddress(int index) { - return Init.calcUserAddress(index); - } - - public Address getPeerAddress(int index) { - return Init.calcPeerAddress(getUserCount(), index); - } - - public Address[] getPeerAddressList() { - Address result[] = new Address[getPeerCount()]; - for (int index = 0; index < getPeerCount(); index ++) { - result[index] = getPeerAddress(index); - } - return result; - } - - public static int DEFAULT_PEER_COUNT = 8; - public static int DEFAULT_USER_COUNT = 2; - - - protected AInitConfig(AKeyPair userKeyPairs[], AKeyPair peerKeyPairs[]) { - this.userKeyPairs = userKeyPairs; - this.peerKeyPairs = peerKeyPairs; - } - - public static AInitConfig create() { - return create(DEFAULT_USER_COUNT, DEFAULT_PEER_COUNT); - } - - public static AInitConfig create(int userCount, int peerCount) { - AKeyPair userKeyPairs[] = new Ed25519KeyPair[userCount]; - AKeyPair peerKeyPairs[] = new Ed25519KeyPair[peerCount]; - - for (int i = 0; i < userCount; i++) { - AKeyPair kp = Ed25519KeyPair.createSeeded(543212345 + i); - userKeyPairs[i] = kp; - } - - for (int i = 0; i < peerCount; i++) { - AKeyPair kp = Ed25519KeyPair.createSeeded(123454321 + i); - peerKeyPairs[i] = kp; - } - - return new AInitConfig(userKeyPairs, peerKeyPairs); - } - -} diff --git a/convex-core/src/main/java/convex/core/init/Init.java b/convex-core/src/main/java/convex/core/init/Init.java deleted file mode 100644 index dcb4a016d..000000000 --- a/convex-core/src/main/java/convex/core/init/Init.java +++ /dev/null @@ -1,350 +0,0 @@ -package convex.core.init; - -import java.io.IOException; -import java.util.List; - -import convex.core.Coin; -import convex.core.Constants; -import convex.core.State; -import convex.core.data.ACell; -import convex.core.data.AList; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.core.data.BlobMap; -import convex.core.data.BlobMaps; -import convex.core.data.PeerStatus; -import convex.core.data.Vectors; -import convex.core.lang.Context; -import convex.core.lang.Core; -import convex.core.lang.RT; -import convex.core.lang.Reader; -import convex.core.lang.Symbols; -import convex.core.util.Utils; - -/** - * Static class for generating the initial Convex State - * - * "The beginning is the most important part of the work." - Plato, The Republic - */ -public class Init { - - // Standard accounts numbers - public static final Address NULL_ADDRESS = Address.create(0); - public static final Address INIT_ADDRESS = Address.create(1); - - // Governance accounts and funding pools - public static final Address RESERVED_ADDRESS = Address.create(2); - public static final Address MAINBANK_ADDRESS = Address.create(3); - public static final Address ROOTFUND_ADDRESS = Address.create(4); - public static final Address MAINPOOL_ADDRESS = Address.create(5); - public static final Address LIVEPOOL_ADDRESS = Address.create(6); - - // Built-in special accounts - public static final Address MEMORY_EXCHANGE_ADDRESS = Address.create(7); - public static final Address CORE_ADDRESS = Address.create(8); - public static final Address REGISTRY_ADDRESS = Address.create(9); - public static final Address TRUST_ADDRESS = Address.create(10); - - // Base for user-specified addresses - public static final Address GENESIS_ADDRESS = Address.create(11); - - - public static State createBaseState(List genesisKeys) { - // accumulators for initial state maps - BlobMap peers = BlobMaps.empty(); - AVector accts = Vectors.empty(); - - long supply = Constants.MAX_SUPPLY; - - // Initial accounts - accts = addGovernanceAccount(accts, NULL_ADDRESS, 0L); // Null account - accts = addGovernanceAccount(accts, INIT_ADDRESS, 0L); // Initialisation Account - - // Reserved fund - long reserved = 100*Coin.EMERALD; - accts = addGovernanceAccount(accts, RESERVED_ADDRESS, reserved); // 75% for investors - supply-=reserved; - - // Foundation governance fund - long governance = 240*Coin.EMERALD; - accts = addGovernanceAccount(accts, MAINBANK_ADDRESS, governance); // 24% Foundation - supply -= governance; - - // Pools for network rewards - long rootFund = 8 * Coin.EMERALD; // 0.8% Long term net rewards - accts = addGovernanceAccount(accts, ROOTFUND_ADDRESS, rootFund); - supply -= rootFund; - - long mainPool = 1 * Coin.EMERALD; // 0.1% distribute 5% / year ~= 0.0003% /day - accts = addGovernanceAccount(accts, MAINPOOL_ADDRESS, mainPool); - supply -= mainPool; - - long livePool = 5 * Coin.DIAMOND; // 0.0005% = approx 2 days of mainpool feed - accts = addGovernanceAccount(accts, LIVEPOOL_ADDRESS, 5 * Coin.DIAMOND); - supply -= livePool; - - // Set up memory exchange. Initially 1GB available at 1000 per byte. (one diamond coin liquidity) - { - long memoryCoins = 1 * Coin.DIAMOND; - accts = addMemoryExchange(accts, MEMORY_EXCHANGE_ADDRESS, memoryCoins, 1000000000L); - supply -= memoryCoins; - } - - // Always have at least one user and one peer setup - int keyCount = genesisKeys.size(); - assert(keyCount > 0); - - // Core library at static address: CORE_ADDRESS - accts = addCoreLibrary(accts, CORE_ADDRESS); - // Core Account should now be fully initialised - // BASE_USER_ADDRESS = accts.size(); - - // Build globals - AVector globals = Constants.INITIAL_GLOBALS; - - // Create the inital state - State s = State.create(accts, peers, globals, BlobMaps.empty()); - - // Add the static defined libraries at addresses: TRUST_ADDRESS, REGISTRY_ADDRESS - s = createStaticLibraries(s, TRUST_ADDRESS, REGISTRY_ADDRESS); - - // Reload accounts with the libraries - accts = s.getAccounts(); - - // Set up initial user accounts - assert(accts.count() == GENESIS_ADDRESS.longValue()); - { - long userFunds = (long)(supply*0.8); // 80% to user accounts - supply -= userFunds; - - // Genesis user gets half of all user funds - long genFunds = userFunds/2; - accts = addAccount(accts, GENESIS_ADDRESS, genesisKeys.get(0), genFunds); - userFunds -= genFunds; - - for (int i = 0; i < keyCount; i++) { - // TODO: construct peer controller addresses - Address address = Address.create(accts.count()); - assert(address.longValue() == accts.count()); - AccountKey key = genesisKeys.get(i); - long userBalance = userFunds / (keyCount-i); - accts = addAccount(accts, address, key, userBalance); - userFunds -= userBalance; - } - assert(userFunds == 0L); - } - - // Finally add peers - // Set up initial peers - - // BASE_PEER_ADDRESS = accts.size(); - { - long peerFunds = supply; - supply -= peerFunds; - for (int i = 0; i < keyCount; i++) { - AccountKey peerKey = genesisKeys.get(i); - Address peerController = getGenesisPeerAddress(i); - - // set a staked fund such that the first peer starts with super-majority - long peerStake = peerFunds / (keyCount-i); - - // split peer funds between stake and account - peers = addPeer(peers, peerKey, peerController, peerStake); - peerFunds -= peerStake; - } - assert(peerFunds == 0L); - } - - - // Add the new accounts to the state - s = s.withAccounts(accts); - // Add peers to the state - s = s.withPeers(peers); - - { // Test total funds after creating user / peer accounts - long total = s.computeTotalFunds(); - if (total != Constants.MAX_SUPPLY) throw new Error("Bad total amount: " + total); - } - - return s; - } - - public static State createStaticLibraries(State s, Address trustAddress, Address registryAddress) { - - // At this point we have a raw initial state with no user or peer accounts - - s = doActorDeploy(s, "convex/registry.cvx"); - s = doActorDeploy(s, "convex/trust.cvx"); - - { // Register core libraries now that registry exists - Context ctx = Context.createFake(s, INIT_ADDRESS); - ctx = ctx.eval(Reader.read("(call *registry* (cns-update 'convex.core " + CORE_ADDRESS + "))")); - - s = ctx.getState(); - s = register(s, CORE_ADDRESS, "Convex Core Library", "Core utilities accessible by default in any account."); - s = register(s, MEMORY_EXCHANGE_ADDRESS, "Memory Exchange Pool", "Automated exchange following the Convex memory allowance model."); - } - - /* - * This test below does not correctly calculate the total funds of the state, since - * the peers have not yet been added. - * - { // Test total funds after creating core libraries - long total = s.computeTotalFunds(); - if (total != Constants.MAX_SUPPLY) throw new Error("Bad total amount: " + total + " should be " + Constants.MAX_SUPPLY); - } - */ - - return s; - } - - public static State createState(List genesisKeys) { - try { - State s=createBaseState(genesisKeys); - - // ============================================================ - // Standard library deployment - s = doActorDeploy(s, "convex/fungible.cvx"); - s = doActorDeploy(s, "convex/trusted-oracle/actor.cvx"); - s = doActorDeploy(s, "convex/trusted-oracle.cvx"); - s = doActorDeploy(s, "convex/asset.cvx"); - s = doActorDeploy(s, "torus/exchange.cvx"); - s = doActorDeploy(s, "asset/nft/simple.cvx"); - s = doActorDeploy(s, "asset/nft/tokens.cvx"); - s = doActorDeploy(s, "asset/box/actor.cvx"); - s = doActorDeploy(s, "asset/box.cvx"); - s = doActorDeploy(s, "convex/play.cvx"); - - { // Deploy Currencies - @SuppressWarnings("unchecked") - AVector> table = (AVector>) Reader - .readResourceAsData("torus/currencies.cvx"); - for (AVector row : table) { - s = doCurrencyDeploy(s, row); - } - } - - // Final funds check - long finalTotal = s.computeTotalFunds(); - if (finalTotal != Constants.MAX_SUPPLY) - throw new Error("Bad total funds in init state amount: " + finalTotal); - - return s; - - } catch (Throwable e) { - e.printStackTrace(); - throw Utils.sneakyThrow(e); - } - } - - public static Address calcPeerAddress(int userCount, int index) { - return Address.create(GENESIS_ADDRESS.longValue() + userCount + index); - } - - public static Address calcUserAddress(int index) { - return Address.create(GENESIS_ADDRESS.longValue() + index); - } - - // A CVX file contains forms which must be wrapped in a `(do ...)` and deployed as an actor. - // First form is the name that must be used when registering the actor. - // - private static State doActorDeploy(State s, String resource) { - Context
ctx = Context.createFake(s, INIT_ADDRESS); - - try { - AList forms = Reader.readAll(Utils.readResourceAsString(resource)); - - ctx = ctx.deployActor(forms.next().cons(Symbols.DO)); - if (ctx.isExceptional()) throw new Error("Error deploying actor:" + ctx.getValue()); - - ctx = ctx.eval(Reader.read("(call *registry* (cns-update " + forms.get(0) + " " + ctx.getResult() + "))")); - if (ctx.isExceptional()) throw new Error("Error while registering actor:" + ctx.getValue()); - - return ctx.getState(); - } catch (IOException e) { - throw Utils.sneakyThrow(e); - } - } - - private static State doCurrencyDeploy(State s, AVector row) { - String symbol = row.get(0).toString(); - double usdValue = RT.jvm(row.get(6)); - long decimals = RT.jvm(row.get(5)); - - // Currency liquidity in lowest currency division - double liquidity = (Long) RT.jvm(row.get(4)) * Math.pow(10, decimals); - - // CVX price for unit - double price = usdValue * 1000; - double cvx = price * liquidity / Math.pow(10, decimals); - - long supply = 1000000000000L; - Context
ctx = Context.createFake(s, MAINBANK_ADDRESS); - ctx = ctx.eval(Reader - .read("(do (import convex.fungible :as fun) (deploy (fun/build-token {:supply " + supply + "})))")); - Address addr = ctx.getResult(); - ctx = ctx.eval(Reader.read("(do (import torus.exchange :as torus) (torus/add-liquidity " + addr + " " - + liquidity + " " + cvx + "))")); - if (ctx.isExceptional()) throw new Error("Error adding market liquidity: " + ctx.getValue()); - ctx = ctx.eval(Reader.read("(call *registry* (cns-update 'currency." + symbol + " " + addr + "))")); - if (ctx.isExceptional()) throw new Error("Error registering currency in CNS: " + ctx.getValue()); - return ctx.getState(); - } - - private static State register(State state, Address origin, String name, String description) { - Context ctx = Context.createFake(state, origin); - ctx = ctx.eval(Reader.read("(call *registry* (register {:description \"" + description + "\" :name \"" + name + "\"}))")); - return ctx.getState(); - } - - public static Address getGenesisAddress() { - return GENESIS_ADDRESS; - } - - public static Address getGenesisPeerAddress(int index) { - return GENESIS_ADDRESS.offset(index+1); - } - - private static BlobMap addPeer(BlobMap peers, AccountKey peerKey, - Address owner, long initialStake) { - PeerStatus ps = PeerStatus.create(owner, initialStake, null); - return peers.assoc(peerKey, ps); - } - - private static AVector addGovernanceAccount(AVector accts, Address a, long balance) { - if (accts.count() != a.longValue()) throw new Error("Incorrect initialisation address: " + a); - AccountStatus as = AccountStatus.createGovernance(balance); - accts = accts.conj(as); - return accts; - } - - private static AVector addMemoryExchange(AVector accts, Address a, long balance, long allowance) { - if (accts.count() != a.longValue()) throw new Error("Incorrect memory exchange address: " + a); - AccountStatus as = AccountStatus.createGovernance(balance).withMemory(allowance); - accts = accts.conj(as); - return accts; - } - - private static AVector addCoreLibrary(AVector accts, Address a) { - if (accts.count() != a.longValue()) throw new Error("Incorrect core library address: " + a); - - AccountStatus as = AccountStatus.createActor(); - as=as.withEnvironment(Core.ENVIRONMENT); - as=as.withMetadata(Core.METADATA); - accts = accts.conj(as); - return accts; - } - - private static AVector addAccount(AVector accts, Address a, AccountKey key, - long balance) { - if (accts.count() != a.longValue()) throw new Error("Incorrect account address: " + a); - AccountStatus as = AccountStatus.create(0L, balance, key); - as = as.withMemory(Constants.INITIAL_ACCOUNT_ALLOWANCE); - accts = accts.conj(as); - return accts; - } - - -} diff --git a/convex-core/src/main/java/convex/core/lang/AFn.java b/convex-core/src/main/java/convex/core/lang/AFn.java deleted file mode 100644 index ec5301680..000000000 --- a/convex-core/src/main/java/convex/core/lang/AFn.java +++ /dev/null @@ -1,60 +0,0 @@ -package convex.core.lang; - -import convex.core.data.ACell; -import convex.core.data.IRefFunction; -import convex.core.data.Tag; -import convex.core.data.type.AType; -import convex.core.data.type.Types; - -/** - * Base class for functions expressed as values - * - * "You know what's web-scale? The Web. And you know what it is? Dynamically - * typed." - Stuart Halloway - * - * @param Return type of functions. - */ -public abstract class AFn extends ACell implements IFn { - - @Override - public abstract Context invoke(Context context, ACell[] args); - - @Override - public abstract AFn updateRefs(IRefFunction func); - - @Override - public final AType getType() { - return Types.FUNCTION; - } - - /** - * Tests if this function supports the given argument list - * - * By default, checks if the function supports the given arity only. - * - * TODO: intention is to override this to include dynamic type checks etc. - * @param args Array of arguments - * @return true if function supports the specified args array - */ - public boolean supportsArgs(ACell[] args) { - return hasArity(args.length); - } - - /** - * Tests if this function supports the given arity. - * @param arity Arity to check - * @return true if function supports the given arity, false otherwise - */ - public abstract boolean hasArity(int arity); - - @Override - public boolean isCVMValue() { - return true; - } - - @Override - public byte getTag() { - return Tag.FN; - } - -} diff --git a/convex-core/src/main/java/convex/core/lang/AOp.java b/convex-core/src/main/java/convex/core/lang/AOp.java deleted file mode 100644 index 19221d706..000000000 --- a/convex-core/src/main/java/convex/core/lang/AOp.java +++ /dev/null @@ -1,91 +0,0 @@ -package convex.core.lang; - -import convex.core.data.ACell; -import convex.core.data.Format; -import convex.core.data.IRefFunction; -import convex.core.data.Tag; -import convex.core.data.type.AType; -import convex.core.data.type.Types; - -/** - * Abstract base class for operations - * - * "...that was the big revelation to me when I was in graduate school—when I - * finally understood that the half page of code on the bottom of page 13 of the - * Lisp 1.5 manual was Lisp in itself. These were “Maxwell’s Equations of - * Software!” This is the whole world of programming in a few lines that I can - * put my hand over." - * - Alan Kay - * - * @param the type of the operation return value - */ -public abstract class AOp extends ACell { - - /** - * Executes this op with the given context. Must preserve depth unless an - * exceptional is returned. - * - * @param Type of Context - * @param context Initial Context - * @return The updated Context after executing this operation - * - */ - public abstract Context execute(Context context); - - @Override - public final AType getType() { - return Types.OP; - } - - @Override - public int estimatedEncodingSize() { - return 10+Format.MAX_EMBEDDED_LENGTH; - } - - @Override - public boolean isCanonical() { - return true; - } - - @Override - public ACell toCanonical() { - return this; - } - - @Override public final boolean isCVMValue() { - return true; - } - - /** - * Returns the opcode for this op - * - * @return Opcode as a byte - */ - public abstract byte opCode(); - - @Override - public final int encode(byte[] bs, int pos) { - bs[pos++]=Tag.OP; - bs[pos++]=opCode(); - return encodeRaw(bs,pos); - } - - /** - * Writes the raw data for this Op to the specified bytebuffer. Assumes Op tag - * and opcode already written. - * - * @param bs Byte array to write to - * @param pos Position to write in byte array - * @return The updated position - */ - @Override - public abstract int encodeRaw(byte[] bs, int pos); - - @Override - public abstract AOp updateRefs(IRefFunction func); - - @Override - public byte getTag() { - return Tag.OP; - } -} \ No newline at end of file diff --git a/convex-core/src/main/java/convex/core/lang/Compiler.java b/convex-core/src/main/java/convex/core/lang/Compiler.java deleted file mode 100644 index 3fad35d3e..000000000 --- a/convex-core/src/main/java/convex/core/lang/Compiler.java +++ /dev/null @@ -1,868 +0,0 @@ -package convex.core.lang; - -import java.util.Map; - -import convex.core.ErrorCodes; -import convex.core.data.ABlob; -import convex.core.data.ACell; -import convex.core.data.AList; -import convex.core.data.AMap; -import convex.core.data.ASequence; -import convex.core.data.ASet; -import convex.core.data.AVector; -import convex.core.data.Address; -import convex.core.data.Keyword; -import convex.core.data.List; -import convex.core.data.MapEntry; -import convex.core.data.Maps; -import convex.core.data.Sets; -import convex.core.data.Symbol; -import convex.core.data.Syntax; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMLong; -import convex.core.data.type.Types; -import convex.core.lang.Context.CompilerState; -import convex.core.lang.impl.AClosure; -import convex.core.lang.impl.CoreFn; -import convex.core.lang.impl.MultiFn; -import convex.core.lang.ops.Cond; -import convex.core.lang.ops.Constant; -import convex.core.lang.ops.Def; -import convex.core.lang.ops.Do; -import convex.core.lang.ops.Invoke; -import convex.core.lang.ops.Lambda; -import convex.core.lang.ops.Let; -import convex.core.lang.ops.Local; -import convex.core.lang.ops.Lookup; -import convex.core.lang.ops.Query; -import convex.core.lang.ops.Special; -import convex.core.util.Utils; - -/** - * Compiler class responsible for transforming forms (code as data) into an - * Op tree for execution. - * - * Phases in complete evaluation: - *
    - *
  1. Expansion (form -> AST)
  2. - *
  3. Compile (AST -> op)
  4. - *
  5. Execute (op -> result)
  6. - *
- * - * Expanded form follows certain rules: - No remaining macros / expanders in - * leading list positions - * - * TODO: consider including typechecking in expansion phase as per: - * http://www.ccs.neu.edu/home/stchang/pubs/ckg-popl2017.pdf - * - * "A language that doesn't affect the way you think about programming is not - * worth knowing." ― Alan Perlis - */ -public class Compiler { - - /** - * Expands and compiles a form. Equivalent to running expansion followed by - * compile. Should not be used directly, intended for use via - * Context.expandCompile(...) - * - * @param - * @param form A form, either raw or wrapped in a Syntax Object - * @param context Compilation context - * @return Context with compiled op as result - */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - static Context> expandCompile(ACell form, Context context) { - // expand phase starts with initial expander - AFn ex = INITIAL_EXPANDER; - - // Use initial expander both as current and continuation expander - // call expand via context to get correct depth and exception handling - final Context ctx = context.invoke(ex, form,ex); - - if (ctx.isExceptional()) return (Context>) (Object) ctx; - ACell c=ctx.getResult(); - - return ctx.compile(c); - } - - /** - * Compiles a single form. Should not be used directly, intended for use via - * Context.compile(...) - * - * Updates context with result, juice consumed - * - * @param Type of op result - * @param expandedForm A fully expanded form expressed as a Syntax Object - * @param context - * @return Context with compiled Op as result - */ - @SuppressWarnings("unchecked") - static Context> compile(ACell form, Context context) { - if (form==null) return compileConstant(context,null); - - if (form instanceof AList) return compileList((AList) form, context); - - if (form instanceof Syntax) return compileSyntax((Syntax) form, context); - if (form instanceof AVector) return compileVector((AVector) form, context); - if (form instanceof AMap) return compileMap((AMap) form, context); - if (form instanceof ASet) return compileSet((ASet) form, context); - - if ((form instanceof Keyword) || (form instanceof ABlob)) { - return compileConstant(context, form); - } - - if (form instanceof Symbol) { - return compileSymbol((Symbol) form, context); - } - - if (form instanceof AOp) { - // already compiled, just return as constant - return context.withResult(Juice.COMPILE_CONSTANT, (AOp)form); - } - - // return as a constant literal - return compileConstant(context,form); - } - - /** - * Compiles a sequence of forms, returning a vector of ops in the updated - * context. Equivalent to calling compile sequentially on each form. - * - * @param - * @param - * @param forms - * @param context - * @return Context with Vector of compiled ops as result - */ - @SuppressWarnings("unchecked") - static Context>> compileAll(ASequence forms, Context context) { - if (forms == null) return context.withResult(Vectors.empty()); // consider null as empty list - int n = forms.size(); - AVector> obs = Vectors.empty(); - for (int i = 0; i < n; i++) { - ACell form = forms.get(i); - context = context.compile(form); - if (context.isExceptional()) return (Context>>) context; - obs = obs.conj((AOp) context.getResult()); - } - return context.withResult(obs); - } - - @SuppressWarnings("unchecked") - private static > Context compileSyntax(Syntax s, Context context) { - context=compile(s.getValue(),context); - return (Context) context; - } - - @SuppressWarnings("unchecked") - private static > Context compileSymbol(Symbol sym, Context context) { - // First check for lexically defined Symbols - CompilerState cs=context.getCompilerState(); - if (cs!=null) { - CVMLong position=cs.getPosition(sym); - if (position!=null) { - Local op=Local.create(position.longValue()); - return (Context) context.withResult(Juice.COMPILE_LOOKUP,op); - } - } - - // Next check for special values - Special maybeSpecial=Special.forSymbol(sym); - if (maybeSpecial!=null) { - return context.withResult(maybeSpecial); - } - - // Get address of compilation environment to use for lookup resolution. - Address address=context.getAddress(); - - Lookup lookUp=Lookup.create(Constant.of(address),sym); - return (Context) context.withResult(Juice.COMPILE_LOOKUP, lookUp); - } - - @SuppressWarnings("unchecked") - private static > Context compileSetBang(AList list, Context context) { - if (list.count()!=3) return context.withArityError("set! requires two arguments, a symbol and an expression"); - - ACell a1=list.get(1); - if (!(a1 instanceof Symbol)) return context.withCompileError("set! requires a symbol as first argument"); - Symbol sym=(Symbol)a1; - - CompilerState cs=context.getCompilerState(); - CVMLong position=(cs==null)?null:context.getCompilerState().getPosition(sym); - if (position==null) return context.withCompileError("Trying to set! an undeclared symbol: "+sym); - - context=context.compile(list.get(2)); - if (context.isExceptional()) return (Context)context; - AOp exp=(AOp) context.getResult(); - - T op=(T) convex.core.lang.ops.Set.create(position.longValue(), exp); - return context.withResult(Juice.COMPILE_NODE,op); - } - - @SuppressWarnings("unchecked") - private static > Context compileLookup(AList list, Context context) { - long n=list.count(); - if ((n<2)||(n>3)) return context.withArityError("lookup requires one or two arguments, an optional expression and a Symbol"); - - AOp
exp=null; - if (n==3) { - context=context.compile(list.get(1)); - if (context.isExceptional()) return (Context)context; - exp=(AOp
) context.getResult(); - } - - ACell a1=list.get(n-1); - if (!(a1 instanceof Symbol)) return context.withCompileError("lookup requires a Symbol as last argument"); - Symbol sym=(Symbol)a1; - - T op=(T) Lookup.create(exp,sym); - return context.withResult(Juice.COMPILE_NODE,op); - } - - - private static > Context compileMap(AMap form, Context context) { - int n = form.size(); - ACell[] vs = new ACell[1 + n * 2]; - vs[0] = Symbols.HASH_MAP; - for (int i = 0; i < n; i++) { - MapEntry me = form.entryAt(i); - vs[1 + i * 2] = me.getKey(); - vs[1 + i * 2 + 1] = me.getValue(); - } - return compileList(List.create(vs), context); - } - - private static > Context compileSet(ASet form, Context context) { - AVector vs = Vectors.empty(); - for (ACell o : form) { - vs = vs.conj(o); - } - vs = vs.conj(Symbols.HASH_SET); - return compileList(List.reverse(vs), context); - } - - @SuppressWarnings("unchecked") - private static > Context compileVector(AVector vec, Context context) { - int n = vec.size(); - if (n == 0) return (Context) context.withResult(Juice.COMPILE_CONSTANT, Constant.EMPTY_VECTOR); - - context = context.compileAll(vec); - AVector> obs = (AVector>) context.getResult(); - - // return a 'vector' call - note function arg is a constant, we don't want to - // lookup on the 'vector' symbol - Constant fn = Constant.create(Core.VECTOR); - return (Context) context.withResult(Juice.COMPILE_NODE, Invoke.create(fn, obs)); - } - - @SuppressWarnings("unchecked") - private static Context compileConstant(Context context, ACell value) { - return (Context) context.withResult(Juice.COMPILE_CONSTANT, Constant.create(value)); - } - - /** - * Compiles a quoted form, returning an op that will produce a data structure - * after evaluation of all unquotes. - * - * @param - * @param context - * @param form Quoted form. May be a regular value or Syntax object. - * @return Context with complied op as result - */ - @SuppressWarnings("unchecked") - private static Context> compileQuasiQuoted(Context context, ACell aForm, int depth) { - ACell form; - - // Check if form is a Syntax Object and unwrap if necessary - boolean isSyntax; - if (aForm instanceof Syntax) { - form= Syntax.unwrap(aForm); - isSyntax=true; - } else { - form=aForm; - isSyntax=false; - } - - if (form instanceof ASequence) { - ASequence seq = (ASequence) form; - int n = seq.size(); - if (n == 0) { - return compileConstant(context, aForm); - } - - if (isListStarting(Symbols.UNQUOTE, form)) { - if (depth==1) { - if (n != 2) return context.withArityError("unquote requires 1 argument"); - ACell unquoted=seq.get(1); - //if (!(unquoted instanceof Syntax)) return context.withCompileError("unquote expects an expanded Syntax Object"); - Context> opContext = expandCompile(unquoted, context); - return opContext; - } else { - depth-=1; - } - } else if (isListStarting(Symbols.QUASIQUOTE, form)) { - depth+=1; - } - - // compile quoted elements - context = compileAllQuasiQuoted(context, seq, depth); - ASequence> rSeq = (ASequence>) context.getResult(); - - ACell fn = (seq instanceof AList) ? Core.LIST : Core.VECTOR; - AOp inv = Invoke.create( Constant.create(fn), rSeq); - if (isSyntax) { - inv=wrapSyntaxBuilder(inv,(Syntax)aForm); - } - return context.withResult(Juice.COMPILE_NODE, inv); - } else if (form instanceof AMap) { - AMap map = (AMap) form; - AVector rSeq = Vectors.empty(); - for (Map.Entry me : map.entrySet()) { - rSeq = rSeq.append(me.getKey()); - rSeq = rSeq.append(me.getValue()); - } - - // compile quoted elements - context = compileAllQuasiQuoted(context, rSeq,depth); - ASequence> cSeq = (ASequence>) context.getResult(); - - ACell fn = Core.HASHMAP; - AOp inv = Invoke.create(Constant.create(fn), cSeq); - if (isSyntax) { - inv=wrapSyntaxBuilder(inv,(Syntax)aForm); - } - - return context.withResult(Juice.COMPILE_NODE, inv); - } else if (form instanceof ASet) { - ASet set = (ASet) form; - AVector rSeq = set.toVector(); - - // compile quoted elements - context = compileAllQuasiQuoted(context, rSeq,depth); - ASequence> cSeq = (ASequence>) context.getResult(); - - ACell fn = Core.HASHSET; - AOp inv = Invoke.create(Constant.create(fn), cSeq); - if (isSyntax) { - inv=wrapSyntaxBuilder(inv,(Syntax)aForm); - } - - return context.withResult(Juice.COMPILE_NODE, inv); - }else { - return compileConstant(context, aForm); - } - } - - private static AOp wrapSyntaxBuilder(AOp op, Syntax source) { - return Invoke.create(Constant.create(Core.SYNTAX), op, Constant.create(source.getMeta())); - } - - /** - * Compiles a sequence of quoted forms - * - * @param - * @param context - * @param form - * @return Context with complied sequence of ops as result - */ - @SuppressWarnings("unchecked") - private static Context>> compileAllQuasiQuoted(Context context, ASequence forms, int depth) { - int n = forms.size(); - // create a list of ops producing each sub-element - ASequence> rSeq = Vectors.empty(); - for (int i = 0; i < n; i++) { - ACell subSyntax = forms.get(i); - ACell subForm = Syntax.unwrap(subSyntax); - if (isListStarting(Symbols.UNQUOTE_SPLICING, subForm)) { - AList subList = (AList) subForm; - int sn = subList.size(); - if (sn != 2) return context.withArityError("unquote-splicing requires 1 argument"); - // unquote-splicing looks like it needs flatmap - return context.withError(ErrorCodes.TODO,"unquote-splicing not yet supported"); - } else { - Context> rctx= compileQuasiQuoted(context, subSyntax,depth); - rSeq = (ASequence>) (rSeq.conj(rctx.getResult())); - } - } - return context.withResult(rSeq); - } - - /** - * Returns true if the form is a List starting with value equal to the - * the specified element - * - * @param element - * @param form - * @return True if form is a list starting with a Syntax Object wrapping the - * specified element, false otherwise. - */ - @SuppressWarnings("unchecked") - private static boolean isListStarting(Symbol element, ACell form) { - if (!(form instanceof AList)) return false; - AList list = (AList) form; - if (list.count() == 0) return false; - ACell firstElement=list.get(0); - return Utils.equals(element, Syntax.unwrap(firstElement)); - } - - @SuppressWarnings("unchecked") - private static > Context compileList(AList list, Context context) { - int n = list.size(); - if (n == 0) return (Context) context.withResult(Juice.COMPILE_CONSTANT, Constant.EMPTY_LIST); - - // first entry in list should be syntax - ACell first = list.get(0); - ACell head = Syntax.unwrap(first); - - if (head instanceof Symbol) { - Symbol sym = (Symbol) head; - - if (sym.equals(Symbols.DO)) { - context = context.compileAll(list.next()); - if (context.isExceptional()) return (Context) context; - Do op = Do.create((AVector>) context.getResult()); - return (Context) context.withResult(Juice.COMPILE_NODE, op); - } - - if (sym.equals(Symbols.LET)) return compileLet(list, context, false); - - if (sym.equals(Symbols.COND)) { - context = context.compileAll(list.next()); - if (context.isExceptional()) return (Context) context; - Cond op = Cond.create((AVector>) (context.getResult())); - return (Context) context.withResult(Juice.COMPILE_NODE, op); - } - - if (sym.equals(Symbols.DEF)) return compileDef(list, context); - if (sym.equals(Symbols.FN)) return compileFn(list, context); - - if (sym.equals(Symbols.QUOTE)) { - if (list.size() != 2) return context.withCompileError(sym + " expects one argument."); - return compileConstant(context,list.get(1)); - } - - if (sym.equals(Symbols.QUASIQUOTE)) { - if (list.size() != 2) return context.withCompileError(sym + " expects one argument."); - return (Context) compileQuasiQuoted(context, list.get(1),1); - } - - if (sym.equals(Symbols.UNQUOTE)) { - // execute the unquoted code directly to get a form to compile - if (list.size() != 2) return context.withCompileError(Symbols.UNQUOTE + " expects one argument."); - context = context.expandCompile(list.get(1)); - if (context.isExceptional()) return (Context) context; - AOp quotedOp = (AOp) context.getResult(); - - Context rctx = context.execute(quotedOp); - if (rctx.isExceptional()) return (Context) rctx; - - Syntax resultForm = Syntax.create(rctx.getResult()); - // need to expand and compile here, since we just created a raw form - return (Context) expandCompile(resultForm, context); - } - - if (sym.equals(Symbols.QUERY)) { - context = context.compileAll(list.next()); - if (context.isExceptional()) return (Context) context; - Query op = Query.create((AVector>) context.getResult()); - return (Context) context.withResult(Juice.COMPILE_NODE, op); - } - - if (sym.equals(Symbols.LOOP)) return compileLet(list, context, true); - if (sym.equals(Symbols.SET_BANG)) return compileSetBang(list, context); - - if (sym.equals(Symbols.LOOKUP)) return compileLookup(list, context); - - } - - // must be a regular function call - context = context.compileAll(list); - if (context.isExceptional()) return (Context) context; - Invoke op = Invoke.create((AVector>) context.getResult()); - - return (Context) context.withResult(Juice.COMPILE_NODE, op); - } - - - @SuppressWarnings("unchecked") - private static > Context compileLet(ASequence list, Context context, - boolean isLoop) { - // list = (let [...] a b c ...) - int n=list.size(); - if (n<2) return context.withCompileError(list.get(0) + " requires a binding form vector at minimum"); - - ACell bo = list.get(1); - - if (!(bo instanceof AVector)) - return context.withCompileError(list.get(0) + " requires a vector of binding forms but got: " + bo); - AVector bv = (AVector) bo; - int bn = bv.size(); - if ((bn & 1) != 0) return context.withCompileError( - list.get(0) + " requires a binding vector with an even number of forms but got: " + bn); - - AVector bindingForms = Vectors.empty(); - AVector> ops = Vectors.empty(); - - for (int i = 0; i < bn; i += 2) { - // Get corresponding op - context = context.expandCompile(bv.get(i + 1)); - if (context.isExceptional()) return (Context) context; - AOp op = (AOp) context.getResult(); - ops = ops.conj(op); - - // Get a binding form. Note binding happens *after* op - ACell bf = bv.get(i); - context=compileBinding(bf,context); - if (context.isExceptional()) return (Context) context; - bindingForms = bindingForms.conj(context.getResult()); - } - int exs = n - 2; // expressions in let after binding vector - for (int i = 2; i < 2 + exs; i++) { - context = context.expandCompile(list.get(i)); - if (context.isExceptional()) return (Context) context; - AOp op = (AOp) context.getResult(); - ops = ops.conj(op); - } - AOp op = Let.create(bindingForms, ops, isLoop); - - return (Context) context.withResult(Juice.COMPILE_NODE, op); - } - - /** - * Compiles a binding form. Updates the current CompilerState. Should save compiler state if used - * @param bindingForm - * @param context - * @return - */ - private static Context compileBinding(ACell bindingForm,Context context) { - CompilerState cs=context.getCompilerState(); - if (cs==null) cs=CompilerState.EMPTY; - - cs=updateBinding(bindingForm,cs); - if (cs==null) return context.withCompileError("Bad binding form"); - - context=context.withCompilerState(cs); - return context.withResult(bindingForm); - } - - @SuppressWarnings("unchecked") - private static CompilerState updateBinding(ACell bindingForm,CompilerState cs) { - if (bindingForm instanceof Symbol) { - Symbol sym=(Symbol)bindingForm; - if (!sym.equals(Symbols.UNDERSCORE)) { - cs=cs.define(sym, null); // TODO: metadata? - } - } else if (bindingForm instanceof AVector) { - AVector v=(AVector)bindingForm; - boolean foundAmpersand=false; - long vcount=v.count(); // count of binding form symbols (may include & etc.) - for (long i=0; i=(vcount-1)) return null; // trailing ampersand - foundAmpersand=true; - bf=v.get(i+1); - i++; - } - cs=updateBinding(bf,cs); - } - } else { - cs=null; - } - return cs; - } - - /** - * Compiles a lambda function form "(fn [...] ...)" to create a Lambda op. - * - * @param - * @param - * @param list - * @param context - * @return Context with compiled op as result. - */ - @SuppressWarnings("unchecked") - private static > Context compileFn(AList list, Context context) { - // list.get(0) is presumably fn - int n = list.size(); - if (n < 2) return context.withArityError("fn requires parameter vector and body in form: " + list); - - // check if we have a vector, in which case we have a single function definition - ACell firstObject = Syntax.unwrap(list.get(1)); - if (firstObject instanceof AVector) { - AVector paramsVector=(AVector) firstObject; - AList bodyList=list.drop(2); - return compileFnInstance(paramsVector,bodyList,context); - } - - return compileMultiFn(list.drop(1),context); - } - - @SuppressWarnings({ "unchecked"}) - private static > Context compileMultiFn(AList list, Context context) { - AVector> fns=Vectors.empty(); - - int num=list.size(); - for (int i=0; i) o,context); - if (context.isExceptional()) return (Context) context; - - AClosure compiledFn=((Lambda) context.getResult()).getFunction(); - fns=fns.conj(compiledFn); - } - - MultiFn mf=MultiFn.create(fns); - Lambda op = Lambda.create(mf); - return (Context) context.withResult(Juice.COMPILE_NODE, op); - } - - /** - * Compiles a function instance function form "([...] ...)" to create a Lambda op. - * - * @param - * @param - * @param list - * @param context - * @return Context with compiled op as result. - */ - @SuppressWarnings("unchecked") - private static > Context compileFnInstance(AList list, Context context) { - int n = list.size(); - if (n < 1) return context.withArityError("fn requires parameter vector and body in form: " + list); - - ACell firstObject = Syntax.unwrap(list.get(0)); - if (firstObject instanceof AVector) { - AVector paramsVector=(AVector) firstObject; - AList bodyList=list.drop(1); - return compileFnInstance(paramsVector,bodyList,context); - } - - return context.withError(ErrorCodes.COMPILE, - "fn instance requires a vector of parameters but got form: " + list); - } - - @SuppressWarnings("unchecked") - private static > Context compileFnInstance(AVector paramsVector, AList bodyList,Context context) { - // need to save compiler state, since we are compiling bindings - CompilerState savedCompilerState=context.getCompilerState(); - - context=compileBinding(paramsVector,context); - if (context.isExceptional()) return context.withCompilerState(savedCompilerState); // restore before return - paramsVector=(AVector) context.getResult(); - - context = context.compileAll(bodyList); - if (context.isExceptional()) return context.withCompilerState(savedCompilerState); // restore before return - - int n=bodyList.size(); - AOp body; - if (n == 0) { - // no body, so function just returns nil - body = Constant.nil(); - } else if (n == 1) { - // one body element, so just unwrap from list - body = ((ASequence>) context.getResult()).get(0); - } else { - // wrap multiple expressions in implicit do - body = Do.create(((ASequence>) context.getResult())); - } - - Lambda op = Lambda.create(paramsVector, body); - context=context.withCompilerState(savedCompilerState); - return (Context) context.withResult(Juice.COMPILE_NODE, op); - } - - @SuppressWarnings("unchecked") - private static > Context compileDef(AList list, Context context) { - int n = list.size(); - if (n != 3) return context.withCompileError("def requires a symbol and an expression, but got: " + list); - - ACell symArg=list.get(1); - - {// check we are actually defining a symbol - ACell sym = Syntax.unwrapAll(symArg); - if (!(sym instanceof Symbol)) return context.withCompileError("def requires a Symbol as first argument but got: " + RT.getType(sym)); - } - - ACell exp=list.get(2); - - // move metadata from expression. TODO: do we need to expand this first? - if (exp instanceof Syntax) { - symArg=Syntax.create(symArg).mergeMeta(((Syntax)exp).getMeta()); - exp=Syntax.unwrap(exp); - } - - context = context.compile(exp); - if (context.isExceptional()) return (Context) context; - - Def op = Def.create(symArg, (AOp) context.getResult()); - return (Context) context.withResult(Juice.COMPILE_NODE, op); - } - - - - - /** - * Initial expander used for expansion of forms prior to compilation. - * - * Should work on both raw forms and syntax objects. - * - * Follows the "Expansion-Passing Style" approach of Dybvig, Friedman, and Haynes - */ - public static final AFn INITIAL_EXPANDER =new CoreFn(Symbols.STAR_INITIAL_EXPANDER) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context,ACell[] args ) { - if (args.length!=2) return context.withArityError(exactArityMessage(2, args.length)); - ACell x = args[0]; - AFn cont=RT.ensureFunction(args[1]); - if (cont==null) return context.withCastError(1, args,Types.FUNCTION); - - // If x is a Syntax object, need to compile the datum - // TODO: check interactions with macros etc. - if (x instanceof Syntax) { - Syntax sx=(Syntax)x; - ACell[] nargs=args.clone(); - nargs[0]=sx.getValue(); - context=context.invoke(this, nargs); - if (context.isExceptional()) return context; - ACell expanded=context.getResult(); - Syntax result=Syntax.mergeMeta(expanded,sx); - return context.withResult(Juice.EXPAND_CONSTANT, result); - } - - ACell form = x; - - // First check for sequences. This covers most cases. - if (form instanceof ASequence) { - - // first check for List - if (form instanceof AList) { - AList listForm = (AList) form; - int n = listForm.size(); - // consider length 0 lists as constant - if (n == 0) return context.withResult(Juice.EXPAND_CONSTANT, x); - - // we need to check if the form itself starts with an expander - ACell first = Syntax.unwrap(listForm.get(0)); - - // check for macro / expander in initial position. - // Note that 'quote' is handled by this, via QUOTE_EXPANDER - AFn expander = context.lookupExpander(first); - if (expander!=null) { - return context.expand(expander,x, cont); // (exp x cont) - } - } - - // need to recursively expand collection elements - // OK for vectors and lists - ASequence seq = (ASequence) form; - if (seq.isEmpty()) return context.withResult(Juice.EXPAND_CONSTANT, x); - Context[] ct = new Context[] { context }; - - ASequence updated; - - updated = seq.map(elem -> { - Context ctx = ct[0]; - if (ctx.isExceptional()) return null; - - // Expand like: (cont x cont) - ctx = ctx.expand(cont,elem, cont); - - if (ctx.isExceptional()) { - ct[0] = ctx; - return null; - } - ACell newElement = ctx.getResult(); - ct[0] = ctx; - return newElement; - }); - Context rctx = ct[0]; - if (context.isExceptional()) return rctx; - return rctx.withResult(Juice.EXPAND_SEQUENCE, updated); - } - - if (form instanceof ASet) { - @SuppressWarnings("rawtypes") - Context ctx = (Context)context; - ASet updated = Sets.empty(); - for (ACell elem : ((ASet) form)) { - ctx = ctx.expand(cont, elem, cont); - if (ctx.isExceptional()) return ctx; - - ACell newElement = ctx.getResult(); - updated = updated.conj(newElement); - } - return ctx.withResult(Juice.EXPAND_SEQUENCE, updated); - } - - if (form instanceof AMap) { - @SuppressWarnings("rawtypes") - Context ctx = (Context)context; - AMap updated = Maps.empty(); - for (Map.Entry me : ((AMap) form).entrySet()) { - // get new key - ctx = ctx.expand(cont,me.getKey(), cont); - if (ctx.isExceptional()) return ctx; - - ACell newKey = ctx.getResult(); - - // get new value - ctx = ctx.expand(cont,me.getValue(), cont); - if (ctx.isExceptional()) return ctx; - ACell newValue = ctx.getResult(); - - updated = updated.assoc(newKey, newValue); - } - return ctx.withResult(Juice.EXPAND_SEQUENCE, updated); - } - - // Return the Syntax Object directly for anything else - // Remember to preserve metadata on symbols in particular! - return context.withResult(Juice.EXPAND_CONSTANT, x); - } - }; - - /** - * Expander used for expansion of `quote` forms. - * - * Should work on both raw forms and syntax objects. - * - * Follows the "Expansion-Passing Style" approach of Dybvig, Friedman, and Haynes - */ - public static final AFn QUOTE_EXPANDER =new CoreFn(Symbols.QUOTE) { - @Override - public Context invoke(Context context,ACell[] args ) { - if (args.length!=2) return context.withArityError(exactArityMessage(2, args.length)); - ACell x = args[0]; - - return context.withResult(Juice.EXPAND_CONSTANT,x); - } - }; - - /** - * Expander used for expansion of `quasiquote` forms. - * - * Should work on both raw forms and syntax objects. - * - * Follows the "Expansion-Passing Style" approach of Dybvig, Friedman, and Haynes - */ - public static final AFn QUASIQUOTE_EXPANDER =new CoreFn(Symbols.QUASIQUOTE) { - @Override - public Context invoke(Context context,ACell[] args ) { - if (args.length!=2) return context.withArityError(exactArityMessage(2, args.length)); - ACell x = args[0]; - - return context.withResult(Juice.EXPAND_CONSTANT,x); - } - }; - - -} diff --git a/convex-core/src/main/java/convex/core/lang/Context.java b/convex-core/src/main/java/convex/core/lang/Context.java deleted file mode 100644 index 4ad002ceb..000000000 --- a/convex-core/src/main/java/convex/core/lang/Context.java +++ /dev/null @@ -1,2262 +0,0 @@ -package convex.core.lang; - -import convex.core.Constants; -import convex.core.ErrorCodes; -import convex.core.State; -import convex.core.data.ABlobMap; -import convex.core.data.ACell; -import convex.core.data.AHashMap; -import convex.core.data.AList; -import convex.core.data.AMap; -import convex.core.data.AObject; -import convex.core.data.ASequence; -import convex.core.data.AString; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.core.data.Blob; -import convex.core.data.Hash; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.MapEntry; -import convex.core.data.Maps; -import convex.core.data.PeerStatus; -import convex.core.data.Strings; -import convex.core.data.Symbol; -import convex.core.data.Syntax; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMLong; -import convex.core.data.type.AType; -import convex.core.exceptions.TODOException; -import convex.core.init.Init; -import convex.core.lang.impl.AExceptional; -import convex.core.lang.impl.ATrampoline; -import convex.core.lang.impl.ErrorValue; -import convex.core.lang.impl.HaltValue; -import convex.core.lang.impl.RecurValue; -import convex.core.lang.impl.Reduced; -import convex.core.lang.impl.ReturnValue; -import convex.core.lang.impl.RollbackValue; -import convex.core.lang.impl.TailcallValue; -import convex.core.util.Economics; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * Representation of CVM execution context. - *

- * - * Execution context includes: - * - The current on-Chain state, including the defined execution environment for each Address - * - Local lexical bindings for the current execution position - * - The identity (as an Address) for the origin, caller and currently executing actor - * - Juice and execution depth current status for - * - Result of the last operation executed (which may be exceptional) - *

- * Interestingly, this behaves like Scala's ZIO[Context-Stuff, AExceptional, T] - *

- * Contexts maintain checks on execution depth and juice to control against arbitrary on-chain - * execution. Coupled with limits on total juice and limits on memory allocation - * per unit juice, this places an upper bound on execution time and space. - *

- * Contexts also support returning exceptional values. Exceptional results may come - * from arbitrary nested depth (which requires a bit of complexity to reset depth when - * catching exceptional values). We avoid using Java exceptions here, because exceptionals - * are "normal" in the context of on-chain execution, and we'd like to avoid the overhead - * of exception handling - may be especially important in DoS scenarios. - *

- * "If you have a procedure with 10 parameters, you probably missed some" - * - Alan Perlis - * - * @param Result type of Context - */ -public class Context extends AObject { - private static final long INITIAL_JUICE = 0; - - // Default values - private static final AVector> DEFAULT_LOG = null; - private static int DEFAULT_DEPTH = 0; - private static final AExceptional DEFAULT_EXCEPTION = null; - private static final long DEFAULT_OFFER = 0L; - public static final AVector EMPTY_BINDINGS=Vectors.empty(); - // private static final Logger log=Logger.getLogger(Context.class.getName()); - - /* - * Frequently changing fields during execution. - * - * While these are mutable, it is also very cheap to just fork() short-lived Contexts - * because the JVM generational GC will just sweep them up shortly afterwards. - */ - - private long juice; - private T result; - private AExceptional exception; - private int depth; - private AVector localBindings; - private ChainState chainState; - - /** - * Local log is a [vector of [address values] entries] - */ - private AVector> log; - private CompilerState compilerState; - - - /** - * Inner class compiler state. - * - * Maintains a mapping of Symbols to positions in a definition vector corresponding to lexical scope. - * - */ - public static final class CompilerState { - public static final CompilerState EMPTY = new CompilerState(Vectors.empty(),Maps.empty()); - - private AVector definitions; - private AHashMap mappings; - - private CompilerState(AVector definitions, AHashMap mappings) { - this.definitions=definitions; - this.mappings=mappings; - } - - public CompilerState define(Symbol sym, Syntax syn) { - long position=definitions.count(); - AVector newDefs=definitions.conj(syn); - AHashMap newMaps=mappings.assoc(sym, CVMLong.create(position)); - return new CompilerState(newDefs,newMaps); - } - - public CVMLong getPosition(Symbol sym) { - return mappings.get(sym); - } - } - - /** - * Inner class for less-frequently changing state related to Actor execution - * Should save some allocation / GC on average, since it will change less - * frequently than the surrounding Context and can be cheaply copied by reference. - * - * SECURITY: security critical, since it determines the current *address* and *caller* - * which in turn controls access to most account resources and rights. - */ - private static final class ChainState { - private final State state; - private final Address origin; - private final Address caller; - private final Address address; - private final long offer; - - /** - * Cached copy of the current environment. Avoid looking up via Address each time. - */ - private final AHashMap environment; - private final AHashMap> metadata; - - private ChainState(State state, Address origin,Address caller, Address address,AHashMap environment, AHashMap> metadata, long offer) { - this.state=state; - this.origin=origin; - this.caller=caller; - this.address=address; - this.environment=environment; - this.metadata=metadata; - this.offer=offer; - } - - public static ChainState create(State state, Address origin, Address caller, Address address, long offer) { - AHashMap environment=Core.ENVIRONMENT; - AHashMap> metadata=Core.METADATA; - if (address!=null) { - AccountStatus as=state.getAccount(address); - if (as!=null) { - environment=as.getEnvironment(); - metadata=as.getMetadata(); - } - } - return new ChainState(state,origin,caller,address,environment,metadata,offer); - } - - public ChainState withStateOffer(State newState,long newOffer) { - if ((state==newState)&&(offer==newOffer)) return this; - return create(newState,origin,caller,address,newOffer); - } - - private ChainState withState(State newState) { - if (state==newState) return this; - return create(newState,origin,caller,address,offer); - } - - private long getOffer() { - return offer; - } - - /** - * Gets the current defined environment - * @return - */ - private AHashMap getEnvironment() { - if (environment==null) return Maps.empty(); - return environment; - } - - private ChainState withEnvironment(AHashMap newEnvironment) { - if (environment==newEnvironment) return this; - AccountStatus as=state.getAccount(address); - AccountStatus nas=as.withEnvironment(newEnvironment); - State newState=state.putAccount(address,nas); - return withState(newState); - } - - public ChainState withEnvironment(AHashMap newEnvironment, - AHashMap> newMeta) { - if ((environment==newEnvironment)&&(metadata==newMeta)) return this; - AccountStatus as=state.getAccount(address); - AccountStatus nas=as.withEnvironment(newEnvironment).withMetadata(newMeta); - State newState=state.putAccount(address,nas); - return withState(newState); - } - - private ChainState withAccounts(AVector newAccounts) { - return withState(state.withAccounts(newAccounts)); - } - - public AHashMap> getMetadata() { - if (metadata==null) return Maps.empty(); - return metadata; - } - - - } - - private Context(ChainState chainState, long juice, AVector localBindings2, T result,int depth, AExceptional exception, AVector> log, CompilerState comp) { - this.chainState=chainState; - this.juice=juice; - this.localBindings=localBindings2; - this.result=result; - this.depth=depth; - this.exception=exception; - this.log=log; - this.compilerState=comp; - } - - @SuppressWarnings("unchecked") - private static Context create(ChainState cs, long juice, AVector localBindings, ACell result, int depth,AVector> log, CompilerState comp) { - if (juice<0) throw new IllegalArgumentException("Negative juice! "+juice); - return new Context(cs,juice,localBindings,(T)result,depth,DEFAULT_EXCEPTION,log,comp); - } - - private static Context create(State state, long juice,AVector localBindings, T result, int depth, Address origin,Address caller, Address address, long offer, AVector> log, CompilerState comp) { - ChainState chainState=ChainState.create(state,origin,caller,address,offer); - return create(chainState,juice,localBindings,result,depth,log,comp); - } - - /** - * Creates an execution context with a default actor address. - * - * Useful for Testing - * - * @param state State to use for this Context - * @return Fake context - */ - public static Context createFake(State state) { - return createFake(state,Init.CORE_ADDRESS); - } - - /** - * Creates a "fake" execution context for the given address. - * - * Not valid for use in real transactions, but can be used to - * compute stuff off-chain "as-if" the actor made the call. - * - * @param state State to use for this Context - * @param origin Origin address to use - * @return Fake context - */ - public static Context createFake(State state, Address origin) { - if (origin==null) throw new IllegalArgumentException("Null address!"); - return create(state,Constants.MAX_TRANSACTION_JUICE,EMPTY_BINDINGS,null,0,origin,null,origin, 0, DEFAULT_LOG,null); - } - - /** - * Creates an initial execution context with the specified actor as origin, and reserving the appropriate - * amount of juice. - * - * Juice reserve is extracted from the actor's current balance. - * - * @param state Initial State for Context - * @param origin Origin Address for Context - * @param juice Initial juice for Context - * @return Initial execution context with reserved juice. - */ - public static Context createInitial(State state, Address origin,long juice) { - AccountStatus as=state.getAccount(origin); - if (as==null) { - // no account - return Context.createFake(state).withError(ErrorCodes.NOBODY); - } - - long balance=as.getBalance(); - long juicePrice=state.getJuicePrice().longValue(); - - // reduce juice if insufficient balance - juice=Math.min(juice,balance/juicePrice); - long reserve=juicePrice*juice; - - assert (reserve<=balance) : "Reserve calculation failed!"; - - long newBalance=balance-reserve; - as=as.withBalance(newBalance); - state=state.putAccount(origin, as); - return create(state,juice,EMPTY_BINDINGS,null,DEFAULT_DEPTH,origin,null,origin,INITIAL_JUICE,DEFAULT_LOG,null); - } - - - - - /** - * Performs key actions at the end of a transaction: - *

    - *
  • Refunds juice
  • - *
  • Accumulates used juice fees in globals
  • - *
- * - * @param initialState State before transaction execution (after prepare) - * @param initialJuice total juice reserved at start of transaction - * @return Updated context - */ - public Context completeTransaction(State initialState, long initialJuice) { - // get state at end of transaction application - State state=getState(); - - long remainingJuice=Math.max(0L, juice); - long usedJuice=initialJuice-remainingJuice; - long juicePrice=initialState.getJuicePrice().longValue(); - assert(usedJuice>=0); - - long refund=0L; - - // maybe refund remaining juice - if (remainingJuice>0L) { - // Compute refund. Shouldn't be possible to overflow? - // But do a paranoid checked multiply just in case - refund+=Math.multiplyExact(remainingJuice,juicePrice); - } - - // compute memory delta - Address address=getAddress(); - AccountStatus account=state.getAccount(address); - long memUsed=state.getMemorySize()-initialState.getMemorySize(); - long allowance=account.getMemory(); - long balanceLeft=account.getBalance(); - boolean memoryFailure=false; - - long memorySpend=0L; // usually zero - if (memUsed>0) { - long allowanceUsed=Math.min(allowance, memUsed); - if (allowanceUsed>0) { - account=account.withMemory(allowance-allowanceUsed); - } - - // compute additional memory purchase requirement beyond allowance - long purchaseNeeded=memUsed-allowanceUsed; - if (purchaseNeeded>0) { - AccountStatus pool=state.getAccount(Init.MEMORY_EXCHANGE_ADDRESS); - // we do memory purchase if pool exists - if (pool!=null) { - long poolBalance=pool.getBalance(); - long poolAllowance=pool.getMemory(); - memorySpend=Economics.swapPrice(purchaseNeeded, poolAllowance, poolBalance); - - if ((refund+balanceLeft)>=memorySpend) { - // enough to cover memory price, so automatically buy from pool - // System.out.println("Buying "+purchaseNeeded+" memory for: "+price); - pool=pool.withBalances(poolBalance+memorySpend, poolAllowance-purchaseNeeded); - state=state.putAccount(Init.MEMORY_EXCHANGE_ADDRESS,pool); - } else { - // Insufficient memory, so need to roll back state to before transaction - // origin should still pay transaction fees, but no memory costs - memorySpend=0L; - state=initialState; - account=state.getAccount(address); - memoryFailure=true; - } - } - } - } else { - // credit any unused memory back to allowance (may be zero) - long allowanceCredit=-memUsed; - account=account.withMemory(allowance+allowanceCredit); - } - - // Make balance changes if needed for refund and memory purchase - account=account.addBalance(refund-memorySpend); - - // update Account - state=state.putAccount(address,account); - - // maybe add used juice to miner fees - if (usedJuice>0L) { - long transactionFees = usedJuice*juicePrice; - long oldFees=state.getGlobalFees().longValue(); - long newFees=oldFees+transactionFees; - state=state.withGlobalFees(CVMLong.create(newFees)); - } - - // final state update and result reporting - Context rctx=this.withState(state); - if (memoryFailure) { - rctx=rctx.withError(ErrorCodes.MEMORY, "Unable to allocate additional memory required for transaction ("+memUsed+" bytes)"); - } - return rctx; - } - - @SuppressWarnings("unchecked") - public Context withState(State newState) { - return (Context) this.withChainState(chainState.withState(newState)); - } - - /** - * Get the latest state from this Context - * @return State instance - */ - public State getState() { - return chainState.state; - } - - /** - * Get the juice available in this Context - * @return Juice available - */ - public long getJuice() { - return juice; - } - - /** - * Get the current offer from this Context - * @return Offered amount in Convex copper - */ - public long getOffer() { - return chainState.getOffer(); - } - - /** - * Gets the current Environment - * @return Environment map - */ - public AHashMap getEnvironment() { - return chainState.getEnvironment(); - } - - /** - * Gets the compiler state - * @return CompilerState instance - */ - public CompilerState getCompilerState() { - return compilerState; - } - - /** - * Gets the metadata for the current Account - * @return Metadata map - */ - public AHashMap> getMetadata() { - return chainState.getMetadata(); - } - - /** - * Consumes juice, returning an updated context if sufficient juice remains or an exceptional JUICE error. - * @param Result type - * @param gulp Amount of jjuice to consume - * @return Updated context with juice consumed - */ - @SuppressWarnings("unchecked") - public Context consumeJuice(long gulp) { - if (gulp<=0) throw new Error("Juice gulp must be positive!"); - if(!checkJuice(gulp)) return withJuiceError(); - juice=juice-gulp; - return (Context) this; - // return new Context(chainState,newJuice,localBindings,(R) result,depth,isExceptional); - } - - /** - * Checks if there is sufficient juice for a given gulp of consumption. Does not alter context in any way. - * - * @param gulp Amount of juice to be consumed. - * @return true if juice is sufficient, false otherwise. - */ - public boolean checkJuice(long gulp) { - return (juice>=gulp); - } - - /** - * Looks up a symbol's value in the current execution context, without any effect on the Context (no juice consumed etc.) - * - * @param Type of value associated with the given symbol - * @param symbol Symbol to look up. May be qualified - * @return Context with the result of the lookup (may be an undeclared exception) - */ - public Context lookup(Symbol symbol) { - // try lookup in dynamic environment - return lookupDynamic(symbol); - } - - /** - * Looks up a value in the dynamic environment. Consumes no juice. - * - * Returns an UNDECLARED exception if the symbol cannot be resolved. - * - * @param Result type - * @param symbol Symbol to look up - * @return Updated Context - */ - public Context lookupDynamic(Symbol symbol) { - Address address=getAddress(); - return lookupDynamic(address,symbol); - } - - /** - * Looks up a value in the dynamic environment. Consumes no juice. - * Returns an UNDECLARED exception if the symbol cannot be resolved. - * Returns a NOBODY exception if the specified Account does not exist - * - * @param Type of value result - * @param address Address of account in which to look up value - * @param symbol Symbol to look up - * @return Updated Context - */ - @SuppressWarnings("unchecked") - public Context lookupDynamic(Address address, Symbol symbol) { - AccountStatus as=getAccountStatus(address); - if (as==null) return withError(ErrorCodes.NOBODY,"No account found for: "+symbol.toString()); - MapEntry envEntry=lookupDynamicEntry(as,symbol); - - // if not found, return UNDECLARED error - if (envEntry==null) { - return withError(ErrorCodes.UNDECLARED,symbol.toString()); - } - - // Result is whatever is defined as the datum value in the environment entry - ACell result = envEntry.getValue(); - return (Context) withResult(result); - } - - /** - * Looks up Metadata for the given symbol in this context - * @param sym Symbol to look up - * @return Metadata for given symbol (may be empty) or null if undeclared - */ - public AHashMap lookupMeta(Symbol sym) { - AHashMap env=getEnvironment(); - if (env.containsKey(sym)) { - return getMetadata().get(sym,Maps.empty()); - } - AccountStatus as = getAliasedAccount(env); - if (as==null) return null; - - env=as.getEnvironment(); - if (env.containsKey(sym)) { - return as.getMetadata().get(sym,Maps.empty()); - } - return null; - } - - /** - * Looks up Metadata for the given symbol in this context - * @param address Address to use for lookup (may pass null for current environment) - * @param sym Symbol to look up - * @return Metadata for given symbol (may be empty) or null if undeclared - */ - public AHashMap lookupMeta(Address address,Symbol sym) { - if (address==null) return lookupMeta(sym); - AccountStatus as=getAccountStatus(address); - if (as==null) return null; - AHashMap env=as.getEnvironment(); - if (env.containsKey(sym)) { - return as.getMetadata().get(sym,Maps.empty()); - } - return null; - } - - /** - * Looks up the account the defines a given Symbol - * @param sym Symbol to look up - * @param address Address to look up in first instance (null for current address). - * @return AccountStatus for given symbol (may be empty) or null if undeclared - */ - public AccountStatus lookupDefiningAccount(Address address,Symbol sym) { - AccountStatus as=(address==null)?getAccountStatus():getAccountStatus(address); - if (as==null) return null; - AHashMap env=as.getEnvironment(); - if (env.containsKey(sym)) { - return as; - } - - as = getAliasedAccount(env); - if (as==null) return null; - - env=as.getEnvironment(); - if (env.containsKey(sym)) { - return as; - } - return null; - } - - /** - * Looks up value for the given symbol in this context - * @param sym Symbol to look up - * @return Value for the given symbol or null if undeclared - */ - public ACell lookupValue(Symbol sym) { - AHashMap env=getEnvironment(); - - // Lookup in current environment first - MapEntry me=env.getEntry(sym); - if (me!=null) { - return me.getValue(); - } - - AccountStatus as = getAliasedAccount(env); - if (as==null) return null; - return as.getEnvironment().get(sym); - } - - /** - * Looks up value for the given symbol in this context - * @param address Address to look up in (may be null for current environment) - * @param sym Symbol to look up - * @return Value for the given symbol or null if undeclared - */ - public ACell lookupValue(Address address,Symbol sym) { - if (address==null) return lookupValue(sym); - AccountStatus as=getAccountStatus(address); - if (as==null) return null; - AHashMap env=as.getEnvironment(); - return env.get(sym); - } - - /** - * Looks up an environment entry for a specific address without consuming juice. - * - * @param address Address of Account in which to look up entry - * @param sym Symbol to look up - * @return Environment entry - */ - public MapEntry lookupDynamicEntry(Address address,Symbol sym) { - AccountStatus as=getAccountStatus(address); - if (as==null) return null; - return lookupDynamicEntry(as,sym); - } - - - - private MapEntry lookupDynamicEntry(AccountStatus as,Symbol sym) { - // Get environment for Address, or default to initial environment - AHashMap env = (as==null)?Core.ENVIRONMENT:as.getEnvironment(); - - - MapEntry result=env.getEntry(sym); - - if (result==null) { - AccountStatus aliasAccount=getAliasedAccount(env); - result = lookupAliasedEntry(aliasAccount,sym); - } - return result; - } - - private MapEntry lookupAliasedEntry(AccountStatus as,Symbol sym) { - if (as==null) return null; - AHashMap env = as.getEnvironment(); - return env.getEntry(sym); - } - - /** - * Gets the account status for the current Address - * - * @return AccountStatus object, or null if not found - */ - public AccountStatus getAccountStatus() { - Address a=getAddress(); - - // Possible we don't have an Address (e.g. in a Query) - if (a==null) return null; - - return chainState.state.getAccount(a); - } - - /** - * Looks up the account for an Symbol alias in the given environment. - * @param env - * @param path An alias path - * @return AccountStatus for the alias, or null if not present - */ - private AccountStatus getAliasedAccount(AHashMap env) { - // TODO: alternative core accounts - return getCoreAccount(); - } - - private AccountStatus getCoreAccount() { - return getState().getAccount(Init.CORE_ADDRESS); - } - - /** - * Gets the holdings map for the current account. - * @return Map of holdings, or null if the current account does not exist. - */ - public ABlobMap getHoldings() { - AccountStatus as=getAccountStatus(getAddress()); - if (as==null) return null; - return as.getHoldings(); - } - - public long getBalance() { - return getBalance(getAddress()); - } - - public long getBalance(Address address) { - AccountStatus as=getAccountStatus(address); - if (as==null) return 0L; - return as.getBalance(); - } - - /** - * Gets the caller of the currently executing context. - * - * Will be null if this context was not called from elsewhere (e.g. is an origin context) - * @return Caller of the currently executing context - */ - public Address getCaller() { - return chainState.caller; - } - - /** - * Gets the address of the currently executing Account. May be the current actor, or the address of the - * account that executed this transaction if no Actors have been called. - * - * @return Address of the current account, cannot be null, must be a valid existing account - */ - public Address getAddress() { - return chainState.address; - } - - /** - * Gets the result from this context. Throws an Error if the context return value is exceptional. - * - * @return Result value from this Context. - */ - public T getResult() { - if (exception!=null) { - String msg = "Can't get result with exceptional value: "+exception; - if (exception instanceof ErrorValue) { - ErrorValue ev=(ErrorValue)exception; - msg=msg+"\n"+ev.getTrace(); - } - throw new Error(msg); - } - return (T) result; - } - - /** - * Gets the resulting value from this context. May be either exceptional or a normal result. - * @return Either the normal result, or an AExceptional instance - */ - public Object getValue() { - if (exception!=null) return exception; - return result; - } - - /** - * Gets the exceptional value from this context. Throws an Error is the context return value is normal. - * @return an AExceptional instance - */ - public AExceptional getExceptional() { - if (exception==null) throw new Error("Can't get exceptional value for context with result: "+exception); - return exception; - } - - /** - * Returns a context updated with the specified result. - * - * Context may become exceptional depending on the result type. - * - * @param Result type - * @param value Value - * @return Context updated with the specified result. - */ - @SuppressWarnings("unchecked") - public Context withResult(ACell value) { - result=(T)value; - exception=null; - return (Context) this; - } - - /** - * Updates this context with a given value, which may either be a normal result or exceptional value - * @param Result type - * @param value Value - * @return Context updated with the specified result value. - */ - @SuppressWarnings("unchecked") - public Context withValue(Object value) { - if (value instanceof AExceptional) { - exception=(AExceptional)value; - result=null; - } else { - result = (T)value; - exception=null; - } - return (Context) this; - } - - public Context withResult(long gulp,R value) { - if (!checkJuice(gulp)) return withJuiceError(); - juice=juice-gulp; - - return withResult(value); - } - - /** - * Returns this context with a JUICE error, consuming all juice. - * @param Result type - * @return Exceptional Context signalling JUICE error. - */ - public Context withJuiceError() { - // set juice to zero. Can't consume more that we have! - this.juice=0; - return withError(ErrorCodes.JUICE,"Out of juice!"); - } - - @SuppressWarnings("unchecked") - public Context withException(AExceptional exception) { - //return (Context) new Context(chainState,juice,localBindings,exception,depth,true); - this.exception=exception; - this.result=null; - return (Context) this; - } - - public Context withException(long gulp,AExceptional value) { - if (!checkJuice(gulp)) return withJuiceError(); - juice=juice-gulp; - return withException(value); - //if ((this.result==value)&&(this.juice==newJuice)) return (Context) this; - //return (Context) new Context(chainState,newJuice,localBindings,value,depth,true); - } - - /** - * Updates the environment of this execution context. This changes the environment stored in the - * state for the current Address. - * - * @param newEnvironment - * @return Updated Context with the given dynamic environment - */ - private Context withEnvironment(AHashMap newEnvironment) { - ChainState cs=chainState.withEnvironment(newEnvironment); - return withChainState(cs); - } - - /** - * Updates the environment of this execution context. This changes the environment stored in the - * state for the current Address. - * - * @param newEnvironment - * @return Updated Context with the given dynamic environment - */ - private Context withEnvironment(AHashMap newEnvironment, AHashMap> newMeta) { - ChainState cs=chainState.withEnvironment(newEnvironment,newMeta); - return withChainState(cs); - } - - private Context withChainState(ChainState newChainState) { - //if (chainState==newChainState) return this; - //return create(newChainState,juice,localBindings,result,depth); - chainState=newChainState; - return this; - } - - /** - * Executes an Op within this context, returning an updated context. - * - * @param Return type of the Op - * @param op Op to execute - * @return Updated Context - */ - @SuppressWarnings("unchecked") - public Context execute(AOp op) { - // execute op with adjusted depth - int savedDepth=getDepth(); - Context> ctx =this.withDepth(savedDepth+1); - if (ctx.isExceptional()) return (Context) ctx; // depth error, won't have modified depth - - Context rctx=op.execute(ctx); - - // reset depth after execution. - rctx=rctx.withDepth(savedDepth); - return rctx; - } - - /** - * Executes an Op at the top level in a new forked Context. Handles top level halt, recur and return. - * - * Returning an updated context containing the result or an exceptional error. - * - * @param Return type of the Op - * @param op Op to execute - * @return Updated Context - */ - public Context run(AOp op) { - // Security: run in fork - Context ctx=fork().execute(op); - - // must handle state results like halt, rollback etc. - return handleStateResults(ctx,false); - } - - /** - * Executes a form at the top level in a new forked Context. Handles top level halt, recur and return. - * - * Returning an updated context containing the result or an exceptional error. - * - * @param Return type of the Op - * @param code Code to execute - * @return Updated Context - */ - public Context run(ACell code) { - Context ctx=fork().eval(code); - - // must handle state results like halt, rollback etc. - return handleStateResults(ctx,false); - } - - - /** - * Invokes a function within this context, returning an updated context. - * - * Handles function recur and return values. - * - * Keeps depth constant upon return. - * - * @param Return type of the function - * @param fn Function to execute - * @param args Arguments for function - * @return Updated Context - */ - @SuppressWarnings("unchecked") - public Context invoke(AFn fn, ACell... args) { - // Note: we don't adjust depth here because execute(...) does it for us in the function body - Context ctx = fn.invoke((Context) this,args); - - if (ctx.isExceptional()) { - // Need an Object because maybe mutating later - Object v=ctx.getExceptional(); - - // recur as many times as needed - while (v instanceof ATrampoline) { - // don't recur if this is the recur function itself - - if (v instanceof RecurValue) { - if (fn==Core.RECUR) break; - RecurValue rv = (RecurValue) v; - ACell[] newArgs = rv.getValues(); - ctx = fn.invoke((Context) ctx,newArgs); - v = ctx.getValue(); - } else if (v instanceof TailcallValue) { - if (fn==Core.TAILCALL_STAR) break; - TailcallValue rv=(TailcallValue)v; - ACell[] newArgs = rv.getValues(); - - // redirect function and invoke - fn = (AFn) rv.getFunction(); - ctx = fn.invoke((Context) ctx,newArgs); - v = ctx.getValue(); - } - } - - // unwrap return value if necessary - if ((v instanceof ReturnValue)&&(!(fn==Core.RETURN))) { - v = ((ReturnValue) v).getValue(); - - // unwrap result - return ctx.withResult((R)v); - } - - if (v instanceof ErrorValue) { - ErrorValue ev=(ErrorValue)v; - ev.addTrace("In function: "+RT.str(fn)); - } - } - return ctx; - } - - /** - * Execute an op, and bind the result to the given binding form in the lexical environment - * - * Binding form may be a destructuring form - * @param bindingForm Binding form - * @param op Op to execute to get binding values - * - * @param Result type of Context - * @return Context with local bindings updated - */ - @SuppressWarnings("unchecked") - public Context executeLocalBinding(ACell bindingForm, AOp op) { - Context ctx=this.execute(op); - if (ctx.isExceptional()) return ctx; - return (Context) ctx.updateBindings(bindingForm, ctx.getResult()); - } - - /** - * Updates local bindings with a given binding form - * - * @param Result type of Context - * @param bindingForm Binding form - * @param args Arguments to bind - * @return Non-exceptional Context with local bindings updated, or an exceptional result if bindings fail - */ - @SuppressWarnings("unchecked") - public Context updateBindings(ACell bindingForm, Object args) { - // Clear any exceptional status - Context ctx=this.withValue(null); - - if (bindingForm instanceof Symbol) { - Symbol sym=(Symbol)bindingForm; - if (sym.equals(Symbols.UNDERSCORE)) return ctx; - // TODO: confirm must be an ACell at this point? - return withLocalBindings(localBindings.conj((ACell)args)); - } else if (bindingForm instanceof AVector) { - AVector v=(AVector)bindingForm; - long vcount=v.count(); // count of binding form symbols (may include & etc.) - - // Count the arguments, exit with a CAST error if args are not sequential - Long argCount=RT.count(args); - if (argCount==null) return ctx.withError(ErrorCodes.CAST, "Trying to destructure an argument that is not a sequential collection"); - - boolean foundAmpersand=false; - for (long i=0; i rest=RT.vec(args).slice(i,consumeCount); // TODO: cost of this? - ctx= ctx.updateBindings(v.get(i+1), rest); - if(ctx.isExceptional()) return ctx; - - // mark ampersand as found, and skip to next binding form (i.e. past the variadic symbol following &) - foundAmpersand=true; - i++; - } else { - // just a regular binding - long argIndex=foundAmpersand?(argCount-(vcount-i)):i; - if (argIndex>=argCount) return ctx.withArityError("Insufficient arguments ("+argCount+") for binding form: "+bindingForm); - ctx=ctx.updateBindings(bf,RT.nth(args, argIndex)); - if(ctx.isExceptional()) return ctx; - } - } - - // at this point, should have consumed all bindings - if (!foundAmpersand) { - if (vcount!=argCount) { - return ctx.withArityError("Expected "+vcount+" arguments but got "+argCount+" for binding form: "+bindingForm); - } - } - } else { - return ctx.withCompileError("Don't understand binding form of type: "+RT.getType(bindingForm)); - } - // return - return ctx; - } - - @Override - public void print(StringBuilder sb) { - sb.append("{"); - sb.append(":juice "+juice); - sb.append(','); - sb.append(":result "); - Utils.print(sb,result); - sb.append(','); - sb.append(":state "); - getState().print(sb); - sb.append("}"); - } - - public AVector getLocalBindings() { - return localBindings; - } - - /** - * Updates this Context with new local bindings. Doesn't affact result state (exceptional or otherwise) - * @param Return type of Context - * @param newBindings New local bindings map to use. - * @return Updated context - */ - @SuppressWarnings("unchecked") - public Context withLocalBindings(AVector newBindings) { - //if (localBindings==newBindings) return (Context) this; - //return create(chainState,juice,newBindings,(R)result,depth); - localBindings=newBindings; - return (Context) this; - } - - /** - * Gets the account status record, or null if not found - * - * @param address Address of account - * @return AccountStatus for the specified address, or null if the account does not exist - */ - public AccountStatus getAccountStatus(Address address) { - return getState().getAccount(address); - } - - public int getDepth() { - return depth; - } - - public Address getOrigin() { - return chainState.origin; - } - - /** - * Defines a value in the environment of the current address - * @param key Symbol of the mapping to create - * @param value Value to define - * @return Updated context with symbol defined in environment - */ - public Context define(Symbol key, ACell value) { - AHashMap env = getEnvironment(); - AHashMap newEnvironment = env.assoc(key, value); - - return withEnvironment(newEnvironment); - } - - /** - * Defines a value in the environment of the current address, updating the metadata - * - * @param syn Syntax Object to define, containing a Symbol value - * @param value Value to set of the given Symbol - * @return Updated context with symbol defined in environment - */ - public Context defineWithSyntax(Syntax syn, ACell value) { - Symbol key=syn.getValue(); - AHashMap env = getEnvironment(); - AHashMap newEnvironment = env.assoc(key, value); - AHashMap> newMeta = getMetadata().assoc(key, syn.getMeta()); - - return withEnvironment(newEnvironment,newMeta); - } - - - /** - * Removes a definition mapping in the environment of the current address - * @param key Symbol of the environment mapping to remove - * @return Updated context with symbol definition removed from the environment, or this context if unchanged - */ - public Context undefine(Symbol key) { - AHashMap m = getEnvironment(); - AHashMap newEnvironment = m.dissoc(key); - AHashMap> newMeta = getMetadata().dissoc(key); - - return withEnvironment(newEnvironment,newMeta); - } - - /** - * Expand and compile a form in this Context. - * - * @param Return type of compiled op - * @param form Form to expand and compile - * @return Updated Context with compiled Op as result - */ - public Context> expandCompile(ACell form) { - // run compiler with adjusted depth - int saveDepth=getDepth(); - Context> rctx =this.withDepth(saveDepth+1); - if (rctx.isExceptional()) return rctx; // depth error, won't have modified depth - - // EXPAND AND COMPILE - rctx = Compiler.expandCompile(form, rctx); - - // reset depth after expansion and compilation, unless there is an error - rctx=rctx.withDepth(saveDepth); - - return rctx; - } - - /** - * Compile a form in this Context. Form must already be fully expanded to a Syntax Object - * - * @param Return type of compiled op - * @param expandedForm Form to compile - * @return Updated Context with compiled Op as result - */ - public Context> compile(ACell expandedForm) { - // Save an adjust depth - int saveDepth=getDepth(); - Context> rctx =this.withDepth(saveDepth+1); - if (rctx.isExceptional()) return rctx; // depth error - - // Save Compiler state - CompilerState savedCompilerState=getCompilerState(); - - // COMPILE - rctx = Compiler.compile(expandedForm, rctx); - - if (rctx.isExceptional()) { - AExceptional ex=rctx.getExceptional(); - if (ex instanceof ErrorValue) { - ErrorValue ev=(ErrorValue)ex; - // TODO: SECURITY: DoS limits - //String msg = "Compiling: Syntax Object with datum of type "+Utils.getClassName(expandedForm); - String msg = "Compiling:"+ expandedForm; - //String msg = "Compiling: "+expandedForm; - ev.addTrace(msg); - } - } - - // restore depth and return - rctx=rctx.withDepth(saveDepth); - rctx=rctx.withCompilerState(savedCompilerState); - return rctx; - } - - /** - * Executes a form in the current context. - * - * Ops are executed directly. - * Other forms will be expanded and compiled before execution, unless *lang* is set, in which case they will - * be executed via the *lang* function. - * - * @param Return type of evaluation - * @param form Form to evaluate - * @return Context containing the result of evaluating the specified form - */ - @SuppressWarnings("unchecked") - public Context eval(ACell form) { - Context> ctx=(Context>) this; - AOp op; - - if (form instanceof AOp) { - op=(AOp)form; - } else { - AFn lang=RT.ensureFunction(lookupValue(Symbols.STAR_LANG)); - if (lang!=null) { - // Execute *lang* function, but increment depth just in case - int saveDepth=ctx.getDepth(); - ctx=ctx.withDepth(saveDepth+1); - if (ctx.isExceptional()) return (Context) ctx; - Context rctx = ctx.invoke(lang,form); - return rctx.withDepth(saveDepth); - } else { - ctx=expandCompile(form); - if (ctx.isExceptional()) return (Context) ctx; - op=ctx.getResult(); - ctx=ctx.withResult(null); // clear result for execution - } - } - return ctx.execute(op); - } - - /** - * Evaluates a form as another Address. - * - * Causes TRUST error if the Address is not controlled by the current address. - * @param Result type - * @param address Address of Account in which to evaluate - * @param form Form to evaluate - * @return Updated Context - */ - public Context evalAs(Address address, ACell form) { - Address caller=getAddress(); - if (caller.equals(address)) return eval(form); - AccountStatus as=this.getAccountStatus(address); - if (as==null) return withError(ErrorCodes.NOBODY,"Address does not exist: "+address); - - Address controller=as.getController(); - if (controller==null) return withError(ErrorCodes.TRUST,"Cannot control address with nil controller set: "+address); - - boolean canControl=false; - - // Run eval in a forked context - Context ctx=this.fork(); - if (controller.equals(getAddress())) { - canControl=true; - } else { - AccountStatus controlAccount=this.getAccountStatus(controller); - if (controlAccount==null) return ctx.withError(ErrorCodes.TRUST,"Cannot control address because controller does not exist: "+controller); - if (controlAccount.isActor()) { - // (call target amount (receive-coin source amount nil)) - ctx=ctx.actorCall(controller,DEFAULT_OFFER,Symbols.CHECK_TRUSTED_Q,caller,null,address); - if (ctx.isExceptional()) return ctx; - canControl=RT.bool(ctx.getResult()); - } - } - - if (!canControl) return ctx.withError(ErrorCodes.TRUST,"Cannot control address: "+address); - - // SECURITY: eval with a context switch - final Context exContext=Context.create(getState(), juice, EMPTY_BINDINGS, null, depth+1, getOrigin(),caller, address,0,log,null); - - final Context rContext=exContext.eval(form); - // SECURITY: must handle results as if returning from an actor call - return handleStateResults(rContext,false); - } - - /** - * Executes code as if run in the current account, but always discarding state changes. - * @param Result type - * @param form Code to execute. - * @return Context updated with only query result and juice consumed - */ - public Context query(ACell form) { - Context ctx=this.fork(); - - // adjust depth. May be exceptional if depth limit exceeded - ctx=ctx.withDepth(depth+1); - - // eval in current account if everything OK - if (!ctx.isExceptional()) { - ctx=ctx.eval(form); - } - - // handle results including state rollback. Will propagate any errors. - return handleQueryResult(ctx); - } - - /** - * Executes code as if run in the specified account, but always discarding state changes. - * @param Result type - * @param address Address of Account in which to execute the query - * @param form Code to execute. - * @return Context updated with only query result and juice consumed - */ - public Context queryAs(Address address, ACell form) { - // chainstate with the target address as origin. - ChainState cs=ChainState.create(getState(),address,null,address,DEFAULT_OFFER); - Context ctx=Context.create(cs, juice, EMPTY_BINDINGS, null, depth,log,null); - ctx=ctx.evalAs(address, form); - return handleQueryResult(ctx); - } - - /** - * Just take result and juice from query. Log and state not kept. - * @param - * @param ctx - * @return - */ - protected Context handleQueryResult(Context ctx) { - this.juice=ctx.getJuice(); - return this.withValue(ctx.result); - } - - /** - * Compiles a sequence of forms in the current context. - * Returns a vector of ops in the updated Context. - * - * Maintains depth. - * - * @param Return type of compiled op. - * @param forms A sequence of forms to compile - * @return Updated context with vector of compiled forms - */ - public Context>> compileAll(ASequence forms) { - Context>> rctx = Compiler.compileAll(forms, this); - return rctx; - } - -// public Context adjustDepth(int delta) { -// int newDepth=Math.addExact(depth,delta); -// return withDepth(newDepth); -// } - - /** - * Changes the depth of this context. Returns exceptional result if depth limit exceeded. - * @param Result type - * @param newDepth New depth value - * @return Updated context with new depth set - */ - @SuppressWarnings("unchecked") - Context withDepth(int newDepth) { - if (newDepth==depth) return (Context) this; - if ((newDepth<0)||(newDepth>Constants.MAX_DEPTH)) return withError(ErrorCodes.DEPTH,"Invalid depth: "+newDepth); - depth=newDepth; - return (Context)this; - } - - @SuppressWarnings("unchecked") - public Context withJuice(long newJuice) { - juice=newJuice; - return (Context) this; - } - - @SuppressWarnings("unchecked") - public Context withCompilerState(CompilerState comp) { - compilerState=comp; - return (Context) this; - } - - /** - * Tests if this Context holds an exceptional result. - * - * Ops should cancel and return exceptional results unchanged, unless they can handle them. - * @return true if context has an exceptional value, false otherwise - */ - public boolean isExceptional() { - return exception!=null; - } - - /** - * Tests if an Address is valid, i.e. refers to an existing Account - * - * @param address Address to check. May be null - * @return true if Account exists, false otherwise - */ - public boolean isValidAccount(Address address) { - if (address==null) return false; - return getAccountStatus(address)!=null; - } - - /** - * Tests if this Context's current status contains an Error. Errors are an uncatchable subset of Exceptions. - * - * @return true if context has an Error value, false otherwise - */ - public boolean isError() { - return (exception!=null)&&(exception instanceof ErrorValue); - } - - /** - * Transfers funds from the current address to the target. - * - * Uses no juice - * - * @param target Target Address, will be created if does not already exist. - * @param amount Amount to transfer, must be between 0 and Amount.MAX_VALUE inclusive - * @return Context with sent amount if the transaction succeeds, or an exceptional value if the transfer fails - */ - public Context transfer(Address target, long amount) { - if (amount<0) return withError(ErrorCodes.ARGUMENT,"Can't transfer a negative amount"); - if (amount>Constants.MAX_SUPPLY) return withError(ErrorCodes.ARGUMENT,"Can't transfer an amount beyond maximum limit"); - - AVector accounts=getState().getAccounts(); - - Address source=getAddress(); - long sourceIndex=source.longValue(); - AccountStatus sourceAccount=accounts.get(sourceIndex); - - long currentBalance=sourceAccount.getBalance(); - if (currentBalance=accounts.count()) { - return this.withError(ErrorCodes.NOBODY,"Target account for transfer "+target+" does not exist"); - } - AccountStatus targetAccount=accounts.get(targetIndex); - - if (targetAccount.isActor()) { - // (call target amount (receive-coin source amount nil)) - // SECURITY: actorCall must do fork to preserve this - Context actx=this.fork(); - actx=actorCall(target,amount,Symbols.RECEIVE_COIN,source,CVMLong.create(amount),null); - if (actx.isExceptional()) return actx; - - // TODO: Should return value be change in balance? or amount offered? - Long sent=currentBalance-actx.getBalance(source); - return actx.withResult(CVMLong.create(sent)); - } else { - // must be a user account - long oldTargetBalance=targetAccount.getBalance(); - long newTargetBalance=oldTargetBalance+amount; - AccountStatus newTargetAccount=targetAccount.withBalance(newTargetBalance); - accounts=accounts.assoc(targetIndex, newTargetAccount); - - // SECURITY: new context with updated accounts - Context result=withChainState(chainState.withAccounts(accounts)).withResult(CVMLong.create(amount)); - - return result; - } - - } - - /** - * Transfers memory allowance from the current address to the target. - * - * Uses no juice - * - * @param target Target Address, must already exist - * @param amountToSend Amount of memory to transfer, must be between 0 and Amount.MAX_VALUE inclusive - * @return Context with a null result if the transaction succeeds, or an exceptional value if the transfer fails - */ - public Context transferMemoryAllowance(Address target, CVMLong amountToSend) { - long amount=amountToSend.longValue(); - if (amount<0) return withError(ErrorCodes.ARGUMENT,"Can't transfer a negative aloowance amount"); - if (amount>Constants.MAX_SUPPLY) return withError(ErrorCodes.ARGUMENT,"Can't transfer an allowance amount beyond maximum limit"); - - AVector accounts=getState().getAccounts(); - - Address source=getAddress(); - long sourceIndex=source.longValue(); - AccountStatus sourceAccount=accounts.get(sourceIndex); - - long currentBalance=sourceAccount.getMemory(); - if (currentBalance=accounts.count()) { - return withError(ErrorCodes.NOBODY,"Cannot transfer memory allowance to non-existent account: "+target); - } - AccountStatus targetAccount=accounts.get(targetIndex); - - long newTargetBalance=targetAccount.getMemory()+amount; - AccountStatus newTargetAccount=targetAccount.withMemory(newTargetBalance); - accounts=accounts.assoc(targetIndex, newTargetAccount); - - // SECURITY: new context with updated accounts - Context result=withChainState(chainState.withAccounts(accounts)).withResult(amountToSend); - return result; - } - - /** - * Sets the memory allowance for the current account, buying / selling from the pool as necessary to - * ensure the correct final allowance - * @param allowance New memory allowance - * @return Context indicating the price paid for the allowance change (may be zero or negative for refund) - */ - public Context setMemory(long allowance) { - AVector accounts=getState().getAccounts(); - if (allowance<0) return withError(ErrorCodes.ARGUMENT,"Can't transfer a negative aloowance amount"); - if (allowance>Constants.MAX_SUPPLY) return withError(ErrorCodes.ARGUMENT,"Can't transfer an allowance amount beyond maximum limit"); - - Address source=getAddress(); - long sourceIndex=source.longValue(); - AccountStatus sourceAccount=accounts.get(sourceIndex); - - long current=sourceAccount.getMemory(); - long balance=sourceAccount.getBalance(); - long delta=allowance-current; - if (delta==0L) return this.withResult(CVMLong.ZERO); - - AccountStatus pool=getState().getAccount(Init.MEMORY_EXCHANGE_ADDRESS); - - try { - long poolAllowance=pool.getMemory(); - long poolBalance=pool.getBalance(); - long price = Economics.swapPrice(delta, poolAllowance,poolBalance); - if (price>balance) { - return withError(ErrorCodes.FUNDS,"Cannot afford allowance, would cost: "+price); - } - sourceAccount=sourceAccount.withBalances(balance-price, allowance); - pool=pool.withBalances(poolBalance+price, poolAllowance-delta); - - // Update accounts - AVector newAccounts=accounts.assoc(sourceIndex, sourceAccount); - newAccounts=newAccounts.assoc(Init.MEMORY_EXCHANGE_ADDRESS.longValue(),pool); - - return withChainState(chainState.withAccounts(newAccounts)).withResult(null); - } catch (IllegalArgumentException e) { - return withError(ErrorCodes.FUNDS,"Cannot trade allowance: "+e.getMessage()); - } - } - - /** - * Accepts offered funds for the given address. - * - * STATE error if offered amount is insufficient. ARGUMENT error if acceptance is negative. - * - * @param Type of result - * @param amount Amount to accept - * @return Updated context, with long amount accepted as result - */ - @SuppressWarnings("unchecked") - public Context acceptFunds(long amount) { - if (amount<0L) return this.withError(ErrorCodes.ARGUMENT,"Negative accept argument"); - if (amount==0L) return this.withResult(Juice.ACCEPT, (R)CVMLong.ZERO); - - long offer=getOffer(); - if (amount>offer) return this.withError(ErrorCodes.STATE,"Insufficient offered funds"); - - State state=getState(); - Address addr=getAddress(); - long balance=state.getBalance(addr); - state=state.withBalance(addr,balance+amount); - - // need to update both state and offer - ChainState cs=chainState.withStateOffer(state,offer-amount); - Context ctx=this.withChainState(cs); - - return (Context) ctx.withResult(Juice.ACCEPT, CVMLong.create(amount)); - } - - /** - * Executes a call to an Actor. Utility function which convert a java String function name - * - * @param Return type of Actor call - * @param target Target Actor address - * @param offer Amount of Convex Coins to offer in Actor call - * @param functionName Symbol of function name defined by Actor - * @param args Arguments to Actor function invocation - * @return Context with result of Actor call (may be exceptional) - */ - public Context actorCall(Address target, long offer, String functionName, ACell... args) { - return actorCall(target,offer,Symbol.create(functionName),args); - } - - /** - * Executes a call to an Actor. - * - * @param Return type of Actor call - * @param target Target Actor address - * @param offer Amount of Convex Coins to offer in Actor call - * @param functionName Symbol of function name defined by Actor - * @param args Arguments to Actor function invocation - * @return Context with result of Actor call (may be exceptional) - */ - public Context actorCall(Address target, long offer, ACell functionName, ACell... args) { - // SECURITY: set up state for actor call - State state=getState(); - Symbol sym=RT.ensureSymbol(functionName); - AccountStatus as=state.getAccount(target); - if (as==null) return this.withError(ErrorCodes.NOBODY,"Actor Account does not exist: "+target); - - // Handling for non-zero offers. - // SECURITY: Subtract offer from balance first so we don't have double-spend issues! - if (offer>0L) { - Address senderAddress=getAddress(); - AccountStatus cas=state.getAccount(senderAddress); - long balance=cas.getBalance(); - if (balance fn = as.getCallableFunction(sym); - - if (fn == null) { - return this.withError(ErrorCodes.STATE, "Value defined in account " + target + " is not a callable function: " + sym); - } - - // Ensure we create a forked Context for the Actor call - final Context exContext=forkActorCall(state, target, offer); - - // INVOKE ACTOR FUNCTION - final Context rctx=exContext.invoke(fn,args); - - ErrorValue ev=rctx.getError(); - if (ev!=null) { - ev.addTrace("Calling Actor "+target+" with function ("+sym+" ...)"); - } - - // SECURITY: must handle state transitions in results correctly - // calling handleStateReturns on 'this' to ensure original values are restored - return handleStateResults(rctx,false); - } - - /** - * Create new forked Context for execution of Actor call. - * SECURITY: Increments depth, will be restored in handleStateResults - * SECURITY: Must change address to the target Actor address. - * SECURITY: Must change caller to current address. - * @param - * @param state for forked context. - * @param target Target actor call address, will become new *address* for context - * @param offer Offer amount for actor call. Must have been pre-subtracted from caller account. - * @return - */ - private Context forkActorCall(State state, Address target, long offer) { - return Context.create(state, juice, EMPTY_BINDINGS, (R)null, depth+1, getOrigin(),getAddress(), target,offer, log,null); - } - - /** - * Handle results at the end of an execution boundary (actor call, transaction etc.) - * @param - * @param returnContext - * @param rollback - * @return - */ - @SuppressWarnings("unchecked") - private Context handleStateResults(Context returnContext, boolean rollback) { - /** Return value */ - Object rv; - if (returnContext.isExceptional()) { - // SECURITY: need to handle exceptional states correctly - AExceptional ex=returnContext.getExceptional(); - if (ex instanceof RollbackValue) { - // roll back state to before Actor call - // Note: this will also refund unused offer. - rollback=true; - rv=((RollbackValue)ex).getValue(); - } else if (ex instanceof HaltValue) { - rv=((HaltValue)ex).getValue(); - } else if (ex instanceof ErrorValue) { - // OK to pass through error, but need to roll back state changes - rollback=true; - rv=ex; - } else if (ex instanceof ReturnValue) { - // Normally doesn't happen (invoke catches this) - // but might in a user transaction. Treat as a Halt. - rv=((ReturnValue)ex).getValue(); - } else { - rollback=true; - String msg; - if (ex instanceof ATrampoline) { - msg="attempt to recur or tail call outside of a function body"; - } if (ex instanceof Reduced) { - msg="reduced used outside of a reduce operation"; - } else { - msg="Unhandled Exception with Code:"+ex.getCode(); - } - rv=ErrorValue.create(ErrorCodes.EXCEPTION, msg); - } - } else { - rv=returnContext.getResult(); - } - - final Address address=getAddress(); // address we are returning to - State returnState; - - if (rollback) { - returnState=this.getState(); - } else { - // take state from the returning context - returnState=returnContext.getState(); - - // Take log from returning context - log=returnContext.getLog(); - - // Refund offer - // Not necessary if rolling back to initial context before offer was subtracted - long refund=returnContext.getOffer(); - if (refund>0) { - // we need to refund caller - AccountStatus cas=returnState.getAccount(address); - long balance=cas.getBalance(); - cas=cas.withBalance(balance+refund); - returnState=returnState.putAccount(address, cas); - } - } - // Rebuild context for the current execution - // SECURITY: must restore origin,depth,caller,address,local bindings, offer - - Context result=this.withState(returnState); - result.juice=returnContext.juice; - result=this.withValue(rv); - return result; - } - - /** - * Deploys an Actor in this context. - * - * Argument argument must be an Actor generation code, which will be evaluated in the new Actor account - * to initialise the Actor - * - * Result will contain the new Actor address if successful, an exception otherwise. - * - * @param code Actor initialisation code - * @return Updated Context with Actor deployed, or an exceptional result - */ - public Context
deployActor(ACell code) { - final State initialState=getState(); - - // deploy initial contract state to next address - Address address=initialState.nextAddress(); - State stateSetup=initialState.tryAddActor(); - - // Deployment execution context with forked context and incremented depth - final Context
exContext=Context.create(stateSetup, juice, EMPTY_BINDINGS, null, depth+1, getOrigin(),getAddress(), address,DEFAULT_OFFER,log,null); - final Context
rctx=exContext.eval(code); - - Context
result=this.handleStateResults(rctx,false); - if (result.isExceptional()) return result; - - return result.withResult(Juice.DEPLOY_CONTRACT,address); - } - - /** - * Create a new Account with a given AccountKey (may be null for actors etc.) - * @param key New Account Key - * @return Updated context with new Account added - */ - public Context
createAccount(AccountKey key) { - final State initialState=getState(); - Address address=initialState.nextAddress(); - AVector accounts=initialState.getAccounts(); - AccountStatus as=AccountStatus.create(0L, key); - accounts=accounts.conj(as); - final State newState=initialState.withAccounts(accounts); - Context
rctx=this.withState(newState); - return rctx.withResult(address); - } - - @SuppressWarnings("unchecked") - public Context withError(Keyword error) { - return (Context) withError(ErrorValue.create(error)); - } - - public Context withError(Keyword errorCode,String message) { - return withError(ErrorValue.create(errorCode,Strings.create(message))); - } - - @SuppressWarnings("unchecked") - public Context withError(ErrorValue error) { - error.addLog(log); - return (Context) withException(error); - } - - - public Context withArityError(String message) { - return withError(ErrorCodes.ARITY,message); - } - - public Context withCompileError(String message) { - return withError(ErrorCodes.COMPILE,message); - } - - public Context withBoundsError(long index) { - return withError(ErrorCodes.BOUNDS,"Index: "+index); - } - - public Context withCastError(int argIndex, AType klass) { - return withError(ErrorCodes.CAST,"Can't convert argument at position "+(argIndex+1)+" to type "+klass); - } - - public Context withCastError(int argIndex, ACell[] args, AType klass) { - return withError(ErrorCodes.CAST,"Can't convert argument at position "+(argIndex+1)+" (with type "+RT.getType(args[argIndex])+ ") to type "+klass); - } - - public Context withCastError(ACell a, AType klass) { - return withError(ErrorCodes.CAST,"Can't convert value of type "+RT.getType(a)+ " to type "+klass); - } - - public Context withCastError(AType klass) { - return withError(ErrorCodes.CAST,"Can't convert value(s) to type "+klass); - } - - public Context withCastError(ACell a, String message) { - return withError(ErrorCodes.CAST,message); - } - - /** - * Gets the error code of this context's return value - * - * @return The ErrorType of the current exceptional value, or null if there is no error. - */ - public ACell getErrorCode() { - if (exception!=null) { - return exception.getCode(); - } - return null; - } - - /** - * Gets the Error from this Context, or null if not an Error - * - * @return The ErrorType of the current exceptional value, or null if there is no error. - */ - public ErrorValue getError() { - if (exception instanceof ErrorValue) { - return (ErrorValue)exception; - } - return null; - } - - public Context withAssertError(String message) { - return withError(ErrorCodes.ASSERT,message); - } - - public Context withFundsError(String message) { - return withError(ErrorCodes.FUNDS,message); - } - - public Context withArgumentError(String message) { - return withError(ErrorCodes.ARGUMENT,message); - } - - /** - * Gets the current timestamp for this context. The timestamp is the greatest timestamp - * of all blocks in consensus (including the currently executing block). - * - * @return Timestamp in milliseconds since UNIX epoch - */ - public CVMLong getTimeStamp() { - return getState().getTimeStamp(); - } - - /** - * Schedules an operation for the specified future timestamp. - * Handles integrity checks and schedule juice. - * - * @param time Timestamp at which to schedule the op. - * @param op Operation to schedule. - * @return Updated context, with scheduled time as the result - */ - public Context schedule(long time, AOp op) { - // check vs current timestamp - long timestamp=getTimeStamp().longValue(); - if (timestamp<0L) return withError(ErrorCodes.ARGUMENT); - if (time ctx=this.withChainState(chainState.withState(s)); - - return ctx.withResult(juice,CVMLong.create(time)); - } - - /** - * Sets the delegated stake on a specified peer to the specified level. - * May set to zero to remove stake. Stake will be capped by current balance. - * - * @param peerKey Peer Account key on which to stake - * @param newStake Amount to stake - * @return Context with amount of coins transferred to Peer as result (may be negative if stake withdrawn) - */ - @SuppressWarnings("unchecked") - public Context setDelegatedStake(AccountKey peerKey, long newStake) { - State s=getState(); - PeerStatus ps=s.getPeer(peerKey); - if (ps==null) return withError(ErrorCodes.STATE,"Peer does not exist for account key: "+peerKey); - if (newStake<0) return this.withArgumentError("Cannot set a negative stake"); - if (newStake>Constants.MAX_SUPPLY) return this.withArgumentError("Target stake out of valid Amount range"); - - Address myAddress=getAddress(); - long balance=getBalance(myAddress); - long currentStake=ps.getDelegatedStake(myAddress); - long delta=newStake-currentStake; - - if (delta==0) return (Context) this; // no change - - // need to check sufficient balance if increasing stake - if (delta>balance) return this.withFundsError("Insufficient balance ("+balance+") to increase Delegated Stake to "+newStake); - - // Final updates. Hopefully everything balances. SECURITY: test this. A lot. - PeerStatus updatedPeer=ps.withDelegatedStake(myAddress, newStake); - s=s.withBalance(myAddress, balance-delta); // adjust own balance - s=s.withPeer(peerKey, updatedPeer); // adjust peer - return withState(s).withResult(CVMLong.create(delta)); - } - - /** - * Sets the stake for a given Peer, transferring coins from the current address. - * @param peerKey Peer Account Key for which to update Stake - * @param newStake New stake for Peer - * @return Updated Context - */ - @SuppressWarnings("unchecked") - public Context setPeerStake(AccountKey peerKey, long newStake) { - State s=getState(); - PeerStatus ps=s.getPeer(peerKey); - if (ps==null) return withError(ErrorCodes.STATE,"Peer does not exist for account key: "+peerKey); - if (newStake<0) return this.withArgumentError("Cannot set a negative stake"); - if (newStake>Constants.MAX_SUPPLY) return this.withArgumentError("Target stake out of valid Amount range"); - - Address myAddress=getAddress(); - if (!ps.getController().equals(myAddress)) return withError(ErrorCodes.STATE,"Current address "+myAddress+" is not the controller of this peer account"); - - long balance=getBalance(myAddress); - long currentStake=ps.getPeerStake(); - long delta=newStake-currentStake; - - if (delta==0) return (Context) this; // no change - - // need to check sufficient balance if increasing stake - if (delta>balance) return this.withFundsError("Insufficient balance ("+balance+") to increase Peer Stake to "+newStake); - - // Final updates assuming everything OK. Hopefully everything balances. SECURITY: test this. A lot. - PeerStatus updatedPeer=ps.withPeerStake(newStake); - s=s.withBalance(myAddress, balance-delta); // adjust own balance - s=s.withPeer(peerKey, updatedPeer); // adjust peer - - return withState(s).withResult(CVMLong.create(delta)); - } - - - /** - * Creates a new peer with the specified stake. - * The accountKey must not be in the list of peers. - * The accountKey must be assigend to the current transaction address - * Stake must be greater than 0. - * Stake must be less than to the account balance. - * - * @param accountKey Peer Account key to create the PeerStatus - * @param initialStake Initial stake amount - * @return Context with final take set - */ - public Context createPeer(AccountKey accountKey, long initialStake) { - State s=getState(); - PeerStatus ps=s.getPeer(accountKey); - if (ps!=null) return withError(ErrorCodes.STATE,"Peer already exists for this account key: "+accountKey.toChecksumHex()); - if (initialStake<0) return this.withArgumentError("Cannot set a negative stake"); - if (initialStake == 0) return this.withArgumentError("Cannot create a peer with zero stake"); - if (initialStake>Constants.MAX_SUPPLY) return this.withArgumentError("Target stake out of valid Amount range"); - - Address myAddress=getAddress(); - - // TODO: SECURITY fix - // AccountStatus as=getAccountStatus(myAddress); - // if (!as.getAccountKey().equals(accountKey)) return this.withArgumentError("Cannot create a peer with a different account-key"); - - long balance=getBalance(myAddress); - if (initialStake>balance) return this.withFundsError("Insufficient balance ("+balance+") to assign an initial stake of "+initialStake); - - PeerStatus newPeerStatus = PeerStatus.create(myAddress, initialStake); - - // Final updates. Hopefully everything balances. SECURITY: test this. A lot. - s=s.withBalance(myAddress, balance-initialStake); // adjust own balance - s=s.withPeer(accountKey, newPeerStatus); // add peer - return withState(s); - } - - /** - * Sets peer data. - * - * @param peerKey Peer to set data for - * @param data Map of data to set for the peer - * @return Context with final peer data set - */ - @SuppressWarnings("unchecked") - public Context setPeerData(AccountKey peerKey, AMap data) { - State s=getState(); - - // get the callers account and account status - Address address = getAddress(); - AccountStatus as = getAccountStatus(address); - - AccountKey ak = as.getAccountKey(); - if (ak == null) return withError(ErrorCodes.STATE,"The account signing this transaction must have a public key"); - PeerStatus ps=s.getPeer(ak); - if (ps==null) return withError(ErrorCodes.STATE,"Peer does not exist for this account and account key: "+ak.toChecksumHex()); - if (!ps.getController().equals(address)) return withError(ErrorCodes.STATE,"Current address "+address+" is not the controller of this peer account"); - - Hash lastStateHash = s.getHash(); - // at the moment only :url is used in the data map - for (ACell key: data.keySet()) { - if (Keywords.URL.equals((Keyword) key)) { - AString url = (AString) data.get(Keywords.URL); - PeerStatus updatedPeer=ps.withHostname(url); - s=s.withPeer(ak, updatedPeer); // adjust peer - } - else { - return withArityError("invalid key name " + key.toString()); - } - } - // if no change just return the current context - if (lastStateHash.equals(s.getHash())){ - return (Context) this; - } - return withState(s); - } - - - - /** - * Sets the holding for a specified target account. Returns NOBODY exception if account does not exist. - * @param targetAddress Account address at which to set the holding - * @param value Value to set for the holding. - * @return Updated context - */ - public Context setHolding(Address targetAddress, ACell value) { - AccountStatus as=getAccountStatus(targetAddress); - if (as==null) return withError(ErrorCodes.NOBODY,"No account in which to set holding"); - as=as.withHolding(getAddress(), value); - return withAccountStatus(targetAddress,as); - } - - /** - * Sets the controller for the current Account - * @param Result type - * @param address New controller Address - * @return Context with current Account controller set - */ - public Context setController(Address address) { - AccountStatus as=getAccountStatus(); - as=as.withController(address); - return withAccountStatus(getAddress(),as); - - } - - /** - * Sets the public key for the current account - * @param Result type - * @param publicKey New Account Public Key - * @return Context with current Account Key set - */ - public Context setAccountKey(AccountKey publicKey) { - AccountStatus as=getAccountStatus(); - as=as.withAccountKey(publicKey); - return withAccountStatus(getAddress(),as); - } - - protected Context withAccountStatus(Address target, AccountStatus accountStatus) { - return withState(getState().putAccount(target, accountStatus)); - } - - /** - * Switches the context to a new address, creating a new execution context. Suitable for testing. - * @param Result type - * @param newAddress New Address to use. - * @return Result type of new Context - */ - public Context forkWithAddress(Address newAddress) { - return createFake(getState(),newAddress); - } - - /** - * Forks this context, creating a new copy of all local state - * @param Result type of new Context - * @return A new forked Context - */ - public Context fork() { - return new Context(chainState, juice, localBindings, null,depth, null,log,compilerState); - } - - @Override - public Blob createEncoding() { - throw new TODOException(); - } - - /** - * Appends a log entry for the current address. - * @param values Values to log - * @return Updated Context - */ - public Context appendLog(AVector values) { - Address addr=getAddress(); - AVector> log=this.log; - if (log==null) { - log=Vectors.empty(); - } - AVector entry = Vectors.of(addr,values); - log=log.conj(entry); - - this.log=log; - return this; - } - - /** - * Gets the log map for the current context. - * - * @return BlobMap of addresses to log entries created in the course of current execution context. - */ - public AVector> getLog() { - if (log==null) return Vectors.empty(); - return log; - } - - public Context lookupCNS(String name) { - Context ctx=this.fork(); - ctx=this.actorCall(Init.REGISTRY_ADDRESS, 0, Symbols.CNS_RESOLVE, Symbol.create(name)); - - return ctx; - } - - /** - * Expands a form with the default *initial-expander* - * @param form Form to expand - * @return Syntax Object resulting from expansion. - */ - public Context expand(ACell form) { - return expand(Core.INITIAL_EXPANDER, form, Core.INITIAL_EXPANDER); - } - - @SuppressWarnings("unchecked") - public Context expand(AFn expander, ACell form, AFn cont) { - // execute with adjusted depth - int savedDepth=getDepth(); - Context ctx =(Context) this.withDepth(savedDepth+1); - if (ctx.isExceptional()) return ctx; // depth error, won't have modified depth - - //AVector savedEnv=getLocalBindings(); - - Context rctx= (Context)invoke(expander, form, cont); - - // reset depth after execution. - //rctx=rctx.withLocalBindings(savedEnv); - rctx=rctx.withDepth(savedDepth); - return rctx; - } - - /** - * Looks up an expander from a form in this context - * @param form Form which might be an expander reference - * @return Expander instance, or null if no expander found - */ - public AFn lookupExpander(ACell form) { - /** - * MapEntry for Expander metadata lookup - */ - AHashMap me = null; - Address addr; - Symbol sym; - - if (form instanceof Symbol) { - sym = (Symbol)form; - me = this.lookupMeta(sym); - addr = null; - } else if (form instanceof AList) { - // Need to check for (lookup ....) as this could reference an expander - @SuppressWarnings("unchecked") - AList listForm = (AList)form; - int n = listForm.size(); - if (n <= 1) return null; - if (!Symbols.LOOKUP.equals(listForm.get(0))) return null; - ACell maybeSym = listForm.get(n-1); - if (!(maybeSym instanceof Symbol)) return null; - sym = (Symbol)maybeSym; - if (n == 2) { - addr = null; - me = lookupMeta(sym); - } else if (n == 3) { - ACell maybeAddress = listForm.get(1); - if (maybeAddress instanceof Symbol) { - // one lookup via Environment for alias - maybeAddress = lookupValue((Symbol)maybeAddress); - } - if (!(maybeAddress instanceof Address)) return null; - addr = (Address)maybeAddress; - me = lookupMeta((Address)maybeAddress,sym); - } else { - return null; - } - } else { - return null; - } - - if (me == null) return null; - - // TODO: examine syntax object for expander details? - ACell expBool = me.get(Keywords.EXPANDER_Q); - if (RT.bool(expBool)) { - // expand form using specified expander and continuation expander - ACell v = lookupValue(addr,sym); - AFn expander = RT.castFunction(v); - if (expander != null) return expander; - } - return null; - } - - - -} diff --git a/convex-core/src/main/java/convex/core/lang/Core.java b/convex-core/src/main/java/convex/core/lang/Core.java deleted file mode 100644 index 0042f1281..000000000 --- a/convex-core/src/main/java/convex/core/lang/Core.java +++ /dev/null @@ -1,2469 +0,0 @@ -package convex.core.lang; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; - -import convex.core.Constants; -import convex.core.ErrorCodes; -import convex.core.State; -import convex.core.data.ABlob; -import convex.core.data.ABlobMap; -import convex.core.data.ACell; -import convex.core.data.ADataStructure; -import convex.core.data.AHashMap; -import convex.core.data.AList; -import convex.core.data.AMap; -import convex.core.data.ASequence; -import convex.core.data.ASet; -import convex.core.data.AString; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.core.data.BlobMaps; -import convex.core.data.Format; -import convex.core.data.Hash; -import convex.core.data.INumeric; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.List; -import convex.core.data.MapEntry; -import convex.core.data.Maps; -import convex.core.data.Sets; -import convex.core.data.Symbol; -import convex.core.data.Syntax; -import convex.core.data.Vectors; -import convex.core.data.prim.APrimitive; -import convex.core.data.prim.CVMBool; -import convex.core.data.prim.CVMByte; -import convex.core.data.prim.CVMChar; -import convex.core.data.prim.CVMDouble; -import convex.core.data.prim.CVMLong; -import convex.core.data.type.Types; -import convex.core.lang.impl.AExceptional; -import convex.core.lang.impl.CoreFn; -import convex.core.lang.impl.CorePred; -import convex.core.lang.impl.ErrorValue; -import convex.core.lang.impl.HaltValue; -import convex.core.lang.impl.RecurValue; -import convex.core.lang.impl.Reduced; -import convex.core.lang.impl.ReturnValue; -import convex.core.lang.impl.RollbackValue; -import convex.core.lang.impl.TailcallValue; -import convex.core.util.Utils; - -/** - * This class builds the core runtime environment at startup. Core runtime - * functions are required to implement basic language features such as: - *
    - *
  • Numerics
  • - *
  • Data structures
  • - *
  • Interaction with on-chain state and execution context
  • - *
- * - * In general, core functions defined in this class are thin Java wrappers over - * functions available in the CVM implementation, but also need to account for: - *
    - *
  • Argument checking
  • - *
  • Exceptional case handling
  • - *
  • Appropriate juice costs
  • - *
- * - * Where possible, we implement core functions in Convex Lisp itself, see - * resources/lang/core.cvx - * - * "Java is the most distressing thing to hit computing since MS-DOS." - Alan - * Kay - */ -@SuppressWarnings("rawtypes") -public class Core { - - /** - * Default initial environment importing core namespace - */ - public static final AHashMap ENVIRONMENT; - - /** - * Default initial core metadata - */ - public static final AHashMap> METADATA; - - - /** - * Symbol for core namespace - */ - public static final Symbol CORE_SYMBOL = Symbol.create("convex.core"); - - private static final HashSet tempReg = new HashSet(); - - private static T reg(T o) { - tempReg.add(o); - return o; - } - - public static final CoreFn> VECTOR = reg(new CoreFn<>(Symbols.VECTOR) { - @Override - public Context> invoke(Context context, ACell[] args) { - // Need to charge juice on per-element basis - long juice = Juice.BUILD_DATA + args.length * Juice.BUILD_PER_ELEMENT; - - // Check juice before building a big vector. - // OK to fail early since will fail with JUICE anyway if vector is too big. - if (!context.checkJuice(juice)) return context.withJuiceError(); - - // Build and return requested vector - AVector result = Vectors.create(args); - return context.withResult(juice, result); - } - }); - - public static final CoreFn> CONCAT = reg(new CoreFn<>(Symbols.CONCAT) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - ASequence result = null; - int n=args.length; - - // initial juice is a load of null - long juice = Juice.CONSTANT; - for (int ix=0; ix seq = RT.sequence(a); - if (seq == null) return context.withCastError(ix,args, Types.SEQUENCE); - - // check juice per element of concatenated sequences - juice += Juice.BUILD_DATA+ seq.count() * Juice.BUILD_PER_ELEMENT; - if (!context.checkJuice(juice)) return context.withJuiceError(); - result = RT.concat(result, seq); - } - return context.withResult(juice, result); - } - }); - - public static final CoreFn> VEC = reg(new CoreFn<>(Symbols.VEC) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - // Arity 1 exactly - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - ACell o = args[0]; - - // Need to compute juice before building potentially big vector - Long n = RT.count(o); - if (n == null) return context.withCastError(0,args, Types.VECTOR); - - long juice = Juice.BUILD_DATA + n * Juice.BUILD_PER_ELEMENT; - if (!context.checkJuice(juice)) return context.withJuiceError(); - - AVector result = RT.castVector(o); - return context.withResult(juice, result); - } - }); - - public static final CoreFn> REVERSE = reg(new CoreFn<>(Symbols.REVERSE) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - // Arity 1 exactly - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - ACell o = args[0]; - - // Need to compute juice before building potentially big vector - ASequence seq = RT.ensureSequence(o); - if (seq == null) return context.withCastError(0,args, Types.SEQUENCE); - - long juice = Juice.BUILD_DATA; - - ASequence result = seq.reverse(); - return context.withResult(juice, result); - } - }); - - public static final CoreFn> SET = reg(new CoreFn<>(Symbols.SET) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - ACell o = args[0]; - - // Need to compute juice before building a potentially big set - Long n = RT.count(o); - if (n == null) return context.withCastError(0,args, Types.SEQUENCE); - long juice = Juice.addMul(Juice.BUILD_DATA ,n,Juice.BUILD_PER_ELEMENT); - if (!context.checkJuice(juice)) return context.withJuiceError(); - - ASet result = RT.castSet(o); - if (result == null) return context.withCastError(0,args, Types.SET); - - return context.withResult(juice, result); - } - }); - - public static final CoreFn> UNION = reg(new CoreFn<>(Symbols.UNION) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - int n=args.length; - ASet result=Sets.empty(); - - long juice=Juice.BUILD_DATA; - - for (int i=0; i set=RT.ensureSet(arg); - if (set==null) return context.withCastError(i,args, Types.SET); - - // check juice before expensive operation - long size=set.count(); - juice = Juice.addMul(juice, size, Juice.BUILD_PER_ELEMENT); - if (!context.checkJuice(juice)) return context.withJuiceError(); - - result=result.includeAll(set); - } - - return context.withResult(juice, result); - } - }); - - public static final CoreFn> INTERSECTION = reg(new CoreFn<>(Symbols.INTERSECTION) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - if (args.length <1) return context.withArityError(minArityMessage(1, args.length)); - - int n=args.length; - ACell arg0=(ACell) args[0]; - ASet result=(arg0==null)?Sets.empty():RT.ensureSet(arg0); - if (result==null) return context.withCastError(0,args, Types.SET); - - long juice=Juice.BUILD_DATA; - - for (int i=1; i set=(arg==null)?Sets.empty():RT.ensureSet(args[i]); - if (set==null) return context.withCastError(i,args, Types.SET); - long size=set.count(); - - juice = Juice.addMul(juice, size, Juice.BUILD_PER_ELEMENT); - if (!context.checkJuice(juice)) return context.withJuiceError(); - - result=result.intersectAll(set); - } - - return context.withResult(juice, result); - } - }); - - public static final CoreFn> DIFFERENCE = reg(new CoreFn<>(Symbols.DIFFERENCE) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - if (args.length <1) return context.withArityError(minArityMessage(1, args.length)); - - int n=args.length; - ACell arg0=args[0]; - ASet result=(arg0==null)?Sets.empty():RT.ensureSet(arg0); - if (result==null) return context.withCastError(0,args, Types.SET); - - long juice=Juice.BUILD_DATA; - - for (int i=1; i set=RT.ensureSet(arg); - if (set==null) return context.withCastError(i,args, Types.SET); - long size=set.count(); - - juice = Juice.addMul(juice, size, Juice.BUILD_PER_ELEMENT); - if (!context.checkJuice(juice)) return context.withJuiceError(); - - result=result.excludeAll(set); - } - - return context.withResult(juice, result); - } - }); - - - - public static final CoreFn> LIST = reg(new CoreFn<>(Symbols.LIST) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - // Any arity is OK - - // Need to compute juice before building a potentially big list - long juice = Juice.BUILD_DATA + args.length * Juice.BUILD_PER_ELEMENT; - if (!context.checkJuice(juice)) return context.withJuiceError(); - - AList result = List.create(args); - return context.withResult(juice, result); - } - }); - - public static final CoreFn STR = reg(new CoreFn<>(Symbols.STR) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - // TODO: pre-check juice? String rendering definitions? - AString result = RT.str(args); - if (result==null) return context.withCastError(Types.STRING); - - long juice = Juice.STR + result.length() * Juice.STR_PER_CHAR; - return context.withResult(juice, result); - } - }); - - public static final CoreFn NAME = reg(new CoreFn<>(Symbols.NAME) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - // Arity 1 - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - // Check can get as a String name - ACell arg = args[0]; - AString result = RT.name(arg); - if (result == null) return context.withCastError(0,args, Types.STRING); - - long juice = Juice.SIMPLE_FN; - return context.withResult(juice, result); - } - }); - - public static final CoreFn KEYWORD = reg(new CoreFn<>(Symbols.KEYWORD) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - // Arity 1 - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell arg=args[0]; - if (arg instanceof Keyword) return context.withResult(Juice.KEYWORD, arg); - - // Check argument is valid name - AString name = RT.name(arg); - if (name == null) return context.withCastError(0,args, Types.KEYWORD); - - // Check name converts to Keyword - Keyword result = Keyword.create(name); - if (result == null) return context.withArgumentError("Invalid Keyword name, must be between 1 and "+Constants.MAX_NAME_LENGTH+ " characters"); - - return context.withResult(Juice.KEYWORD, result); - } - }); - - public static final CoreFn SYMBOL = reg(new CoreFn(Symbols.SYMBOL) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int n = args.length; - if (n!=1) return context.withArityError(exactArityMessage(1,args.length)); - - ACell maybeName=args[n-1]; - - // Fast path for existing Symbol - if (maybeName instanceof Symbol) { - Symbol sym=(Symbol)maybeName; - return context.withResult(Juice.SYMBOL, sym); - } - - // Check argument is valid name for a Symbol - AString name = RT.name(maybeName); - if (name == null) return context.withCastError(0, args, Types.SYMBOL); - - Symbol sym = Symbol.create(name); - if (sym == null) return context.withArgumentError("Invalid Symbol name, must be between 1 and " + Constants.MAX_NAME_LENGTH + " characters"); - - long juice = Juice.SYMBOL; - return context.withResult(juice, sym); - } - }); - - public static final CoreFn> COMPILE = reg(new CoreFn<>(Symbols.COMPILE) { - - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - ACell form = (ACell) args[0]; - // note: compiler takes care of Juice for us - return context.expandCompile(form); - } - - }); - - public static final CoreFn EVAL = reg(new CoreFn<>(Symbols.EVAL) { - - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell form = (ACell) args[0]; - Context rctx = context.eval(form); - return rctx.consumeJuice(Juice.EVAL); - } - - }); - - public static final CoreFn EVAL_AS = reg(new CoreFn<>(Symbols.EVAL_AS) { - - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); - - Address address = RT.ensureAddress(args[0]); - if (address==null) return context.withCastError(0,args, Types.ADDRESS); - - ACell form = (ACell) args[1]; - Context rctx = context.evalAs(address,form); - return rctx.consumeJuice(Juice.EVAL); - } - }); - - public static final CoreFn SCHEDULE_STAR = reg(new CoreFn<>(Symbols.SCHEDULE_STAR) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int n = args.length; - if (n != 2) return context.withArityError(this.exactArityMessage(3, n)); - - // get timestamp target - CVMLong tso = RT.ensureLong(args[0]); - if (tso==null) return context.withCastError(0,args,Types.LONG); - long scheduleTimestamp = tso.longValue(); - - // get operation - ACell opo = args[1]; - if (!(opo instanceof AOp)) return context.withCastError(1,args,Types.OP); - AOp op = (AOp) opo; - - return context.schedule(scheduleTimestamp, op); - } - }); - - public static final CoreFn SYNTAX = reg(new CoreFn<>(Symbols.SYNTAX) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int n=args.length; - if (n < 1) return context.withArityError(minArityMessage(1, args.length)); - if (n > 2) return context.withArityError(maxArityMessage(2, args.length)); - - Syntax result; - if (n==1) { - result=Syntax.create((ACell)args[0]); - } else { - AHashMap meta=RT.ensureHashMap(args[1]); - if (meta==null) return context.withCastError(1,args, Types.MAP); - result = Syntax.create((ACell) args[0],meta); - } - - long juice = Juice.SYNTAX; - - return context.withResult(juice, result); - } - }); - - public static final CoreFn UNSYNTAX = reg(new CoreFn<>(Symbols.UNSYNTAX) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - // Arity 1 - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - // Unwrap Syntax. Cannot fail. - ACell result = Syntax.unwrap(args[0]); - - // Return unwrapped value with juice - long juice = Juice.SYNTAX; - return context.withResult(juice, result); - } - }); - - public static final CoreFn> META = reg(new CoreFn<>(Symbols.META) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell a=args[0]; - - AHashMap result; - if (a instanceof Syntax) { - result = ((Syntax) a).getMeta(); - } else { - result= null; - } - - long juice = Juice.META; - return context.withResult(juice, result); - } - }); - - public static final CorePred SYNTAX_Q = reg(new CorePred(Symbols.SYNTAX_Q) { - @Override - public boolean test(ACell val) { - return val instanceof Syntax; - } - }); - - public static final CoreFn EXPAND = reg(new CoreFn<>(Symbols.EXPAND) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int n = args.length; - if ((n<1)||(n>3)) { - return context.withArityError(name() + " requires a form argument, optional expander and optional continuation expander (arity 1, 2 or 2)"); - } - - //context = context.lookup(Symbols.STAR_INITIAL_EXPANDER); - //if (context.isExceptional()) return (Context) context; - //AFn initialExpander=RT.function(context.getResult()); - //if (initialExpander==null) { - // return context.withError(ErrorCodes.CAST,name()+" requires a valid *initial-expander*, not found in environment"); - //} - - AFn expander=Compiler.INITIAL_EXPANDER; - if (n >= 2) { - // use provided expander - ACell exArg = args[1]; - expander=RT.ensureFunction(exArg); - if (expander==null) return context.withCastError(1,args, Types.FUNCTION); - } - - AFn cont=expander; // use passed expander by default - if (n >= 3) { - // use provided continuation expander - ACell contArg = args[2]; - cont=RT.ensureFunction(contArg); - if (cont==null) return context.withCastError(2,args, Types.FUNCTION); - } - - ACell form = args[0]; - Context rctx = context.expand(expander,form, cont); - return rctx; - } - }); - - public static final AFn INITIAL_EXPANDER = reg(Compiler.INITIAL_EXPANDER); - - public static final AFn QUOTE_EXPANDER = reg(Compiler.QUOTE_EXPANDER); - - public static final AFn QUASIQUOTE_EXPANDER = reg(Compiler.QUASIQUOTE_EXPANDER); - - public static final CoreFn CALLABLE_Q = reg(new CoreFn<>(Symbols.CALLABLE_Q) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); - - Address addr = RT.ensureAddress(args[0]); - if (addr == null) return context.withCastError(1,args, Types.ADDRESS); - - Symbol sym = RT.ensureSymbol(args[1]); - if (sym == null) return context.withCastError(1,args, Types.SYMBOL); - - AccountStatus as = context.getState().getAccount(addr); - if (as == null) return context.withResult(Juice.LOOKUP, CVMBool.FALSE); - - AHashMap symMeta = as.getMetadata().get(sym); - - CVMBool result; - - if (symMeta == null) { - result = CVMBool.FALSE; - } else { - result = CVMBool.of(symMeta.get(Keywords.CALLABLE_Q) == CVMBool.TRUE); - } - - return context.withResult(Juice.LOOKUP, result); - } - }); - - public static final CoreFn
DEPLOY = reg(new CoreFn<>(Symbols.DEPLOY) { - @SuppressWarnings("unchecked") - @Override - public Context
invoke(Context context, ACell[] args) { - if (args.length !=1) return context.withArityError(exactArityMessage(1, args.length)); - - return context.deployActor(args[0]); - } - }); - - - public static final CoreFn ACCEPT = reg(new CoreFn<>(Symbols.ACCEPT) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - // Arity 1 - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - // must cast to Long - CVMLong amount = RT.ensureLong(args[0]); - if (amount == null) return context.withCastError(0,args, Types.LONG); - - return context.acceptFunds(amount.longValue()); - } - }); - - public static final CoreFn CALL_STAR = reg(new CoreFn<>(Symbols.CALL_STAR) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length < 3) return context.withArityError(minArityMessage(1, args.length)); - - // consume juice first? - Context ctx = context.consumeJuice(Juice.CALL_OP); - if (ctx.isExceptional()) return ctx; - - Address target = RT.ensureAddress(args[0]); - if (target == null) return ctx.withCastError(0,args, Types.ADDRESS); - - CVMLong sendAmount = RT.ensureLong(args[1]); - if (sendAmount == null) return ctx.withCastError(1,args, Types.LONG); - - Symbol sym = RT.ensureSymbol(args[2]); - if (sym == null) return ctx.withCastError(2,args, Types.SYMBOL); - - // prepare contract call arguments - int arity = args.length - 3; - ACell[] callArgs = Arrays.copyOfRange(args, 3, 3 + arity); - - return ctx.actorCall(target, sendAmount.longValue(), sym, callArgs); - } - }); - - public static final CoreFn LOG = reg(new CoreFn<>(Symbols.LOG) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - // any arity fine - int n=args.length; - long juice = Juice.LOG+Juice.BUILD_DATA+n*Juice.BUILD_PER_ELEMENT; - if (!context.checkJuice(juice)) { - return context.withJuiceError(); - } - AVector values=Vectors.create(args); - - context=context.appendLog(values); - - return context.withResult(juice, values); - } - }); - - public static final CoreFn UNDEF_STAR = reg(new CoreFn<>(Symbols.UNDEF_STAR) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - Symbol sym=RT.ensureSymbol(args[0]); - if (sym == null) return context.withArgumentError("Invalid Symbol name for undef: " + Utils.toString(args[0])); - - Context ctx=(Context) context.undefine(sym); - - // return nil - return ctx.withResult(Juice.DEF, null); - - } - }); - - - public static final CoreFn LOOKUP = reg(new CoreFn<>(Symbols.LOOKUP) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int n=args.length; - if ((n<1)||(n>2)) return context.withArityError(rangeArityMessage(1,2, args.length)); - - // get Address to perform lookup - Address address=(n==1)?context.getAddress():RT.ensureAddress(args[0]); - if (address==null) return context.withCastError(0,args, Types.ADDRESS); - - // ensure argument converts to a Symbol correctly. - ACell symArg=args[n-1]; - Symbol sym = RT.ensureSymbol(symArg); - if (sym == null) return context.withCastError(n-1,args,Types.SYMBOL); - - MapEntry me = context.lookupDynamicEntry(address,sym); - - long juice = Juice.LOOKUP; - ACell result = (me == null) ? null : me.getValue(); - return context.withResult(juice, result); - } - }); - - public static final CoreFn LOOKUP_META = reg(new CoreFn<>(Symbols.LOOKUP_META) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int n=args.length; - if ((n<1)||(n>2)) return context.withArityError(rangeArityMessage(1,2, args.length)); - - // get Address to perform lookup - Address address=null; - if (n>1) { - address=RT.ensureAddress(args[0]); - if (address==null) return context.withCastError(0,args, Types.ADDRESS); - } - - // ensure argument converts to a Symbol correctly. - ACell symArg=args[n-1]; - Symbol sym = RT.ensureSymbol(symArg); - if (sym == null) return context.withCastError(n-1,args,Types.SYMBOL); - - AHashMap result = context.lookupMeta(address,sym); - - long juice = Juice.LOOKUP; - return context.withResult(juice, result); - } - }); - - public static final CoreFn
ADDRESS = reg(new CoreFn<>(Symbols.ADDRESS) { - @SuppressWarnings("unchecked") - @Override - public Context
invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell o = args[0]; - Address address = RT.castAddress(o); - if (address == null) { - if (o instanceof AString) return context.withArgumentError("String not convertible to a valid Address: " + o); - if (o instanceof ABlob) return context.withArgumentError("Blob not convertiable a valid Address: " + o); - return context.withCastError(0,args, Types.ADDRESS); - } - long juice = Juice.ADDRESS; - - return context.withResult(juice, address); - } - }); - - public static final CoreFn BLOB = reg(new CoreFn<>(Symbols.BLOB) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - // TODO: probably need to pre-cost this? - ABlob blob = RT.castBlob(args[0]); - if (blob == null) return context.withCastError(0,args, Types.BLOB); - - long juice = Juice.BLOB + Juice.BLOB_PER_BYTE * blob.count(); - - return context.withResult(juice, blob); - } - }); - - public static final CoreFn ACCOUNT = reg(new CoreFn<>(Symbols.ACCOUNT) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell a0 = args[0]; - Address address = RT.ensureAddress(a0); - if (address == null) return context.withCastError(0,args, Types.ADDRESS); - - // Note: returns null if the argument is not an address - AccountStatus as = context.getAccountStatus(address); - - return context.withResult(Juice.SIMPLE_FN, as); - } - }); - - public static final CoreFn BALANCE = reg(new CoreFn<>(Symbols.BALANCE) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - Address address = RT.ensureAddress(args[0]); - if (address == null) return context.withCastError(0,args, Types.ADDRESS); - - AccountStatus as = context.getAccountStatus(address); - CVMLong balance = (as != null) ? CVMLong.create(as.getBalance()) : null; - - return context.withResult(Juice.BALANCE, balance); - } - }); - - public static final CoreFn TRANSFER = reg(new CoreFn<>(Symbols.TRANSFER) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); - - Address address = RT.ensureAddress(args[0]); - if (address == null) return context.withCastError(0,args, Types.ADDRESS); - - CVMLong amount = RT.ensureLong(args[1]); - if (amount == null) return context.withCastError(1,args, Types.LONG); - - return context.transfer(address, amount.longValue()).consumeJuice(Juice.TRANSFER); - - } - }); - - public static final CoreFn SET_MEMORY = reg(new CoreFn<>(Symbols.SET_MEMORY) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - CVMLong amount = RT.ensureLong(args[0]); - if (amount == null) return context.withCastError(0,args, Types.LONG); - - return context.setMemory(amount.longValue()).consumeJuice(Juice.TRANSFER); - } - }); - - public static final CoreFn TRANSFER_MEMORY = reg(new CoreFn<>(Symbols.TRANSFER_MEMORY) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); - - Address address = RT.ensureAddress(args[0]); - if (address == null) return context.withCastError(0,args, Types.ADDRESS); - - CVMLong amount = RT.ensureLong(args[1]); - if (amount == null) return context.withCastError(1,args, Types.LONG); - - return context.transferMemoryAllowance(address, amount).consumeJuice(Juice.TRANSFER); - } - }); - - public static final CoreFn STAKE = reg(new CoreFn<>(Symbols.STAKE) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); - - AccountKey accountKey = RT.ensureAccountKey(args[0]); - if (accountKey == null) return context.withCastError(0,args, Types.BLOB); - - CVMLong amount = RT.ensureLong(args[1]); - if (amount == null) return context.withCastError(1,args, Types.LONG); - - return context.setDelegatedStake(accountKey, amount.longValue()).consumeJuice(Juice.TRANSFER); - - } - }); - - public static final CoreFn CREATE_PEER = reg(new CoreFn<>(Symbols.CREATE_PEER) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); - - AccountKey accountKey = RT.ensureAccountKey(args[0]); - if (accountKey == null) return context.withCastError(0,args, Types.BLOB); - - CVMLong amount = RT.ensureLong(args[1]); - if (amount == null) return context.withCastError(1,args, Types.LONG); - - return context.createPeer(accountKey, amount.longValue()).consumeJuice(Juice.PEER_UPDATE); - } - }); - - - public static final CoreFn SET_PEER_DATA = reg(new CoreFn<>(Symbols.SET_PEER_DATA) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 2) return context.withArityError(exactArityMessage(1, args.length)); - - AccountKey peerKey=RT.ensureAccountKey(args[0]); - if (peerKey == null) return context.withCastError(0,args, Types.BLOB); - - AMap data = RT.ensureMap(args[1]); - if (data == null) return context.withCastError(1,args, Types.MAP); - - context=context.consumeJuice(Juice.PEER_UPDATE); - if (context.isExceptional()) return context; - - return context.setPeerData(peerKey,data); - } - }); - - public static final CoreFn SET_PEER_STAKE = reg(new CoreFn<>(Symbols.SET_PEER_STAKE) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); - - AccountKey peerKey=RT.ensureAccountKey(args[0]); - if (peerKey == null) return context.withCastError(0,args, Types.BLOB); - - CVMLong newStake = RT.ensureLong(args[1]); - if (newStake == null) return context.withCastError(1,args, Types.LONG); - long targetStake=newStake.longValue(); - - context=context.consumeJuice(Juice.PEER_UPDATE); - if (context.isExceptional()) return context; - - return context.setPeerStake(peerKey,targetStake); - } - }); - - - public static final CoreFn> HASHMAP = reg(new CoreFn<>(Symbols.HASH_MAP) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - int len = args.length; - // specialised arity check since we need even length - if (Utils.isOdd(len)) return context.withArityError(name() + " requires an even number of arguments"); - - long juice = Juice.BUILD_DATA + len * Juice.BUILD_PER_ELEMENT; - return context.withResult(juice, Maps.create(args)); - } - }); - - - public static final CoreFn BLOB_MAP = reg(new CoreFn<>(Symbols.BLOB_MAP) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int len = args.length; - // specialised arity check since we need even length - if (Utils.isOdd(len)) return context.withArityError(name() + " requires an even number of arguments"); - - long juice = Juice.BUILD_DATA + len * Juice.BUILD_PER_ELEMENT; - if (!context.checkJuice(juice)) return context.withJuiceError(); - - ABlobMap r=BlobMaps.empty(); - int n=len/2; - for (int i=0; i> HASHSET = reg(new CoreFn<>(Symbols.HASH_SET) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - // any arity is OK - - long juice = Juice.BUILD_DATA + (args.length * Juice.BUILD_PER_ELEMENT); - if (!context.checkJuice(juice)) return context.withJuiceError(); - - return context.withResult(juice, Sets.of(args)); - } - }); - - public static final CoreFn> KEYS = reg(new CoreFn<>(Symbols.KEYS) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell a = args[0]; - if (!(a instanceof AMap)) return context.withCastError(0,args, Types.MAP); - - AMap m = (AMap) a; - long juice = Juice.BUILD_DATA + m.count() * Juice.BUILD_PER_ELEMENT; - if (!context.checkJuice(juice)) return context.withJuiceError(); - - AVector keys = RT.keys(m); - - return context.withResult(juice, keys); - } - }); - - public static final CoreFn> VALUES = reg(new CoreFn<>(Symbols.VALUES) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell a = args[0]; - if (!(a instanceof AMap)) return context.withCastError(0,args, Types.MAP); - - AMap m = (AMap) a; - long juice = Juice.BUILD_DATA + m.count() * Juice.BUILD_PER_ELEMENT; - if (!context.checkJuice(juice)) return context.withJuiceError(); - - AVector keys = RT.values(m); - - return context.withResult(juice, keys); - } - }); - - public static final CoreFn> ASSOC = reg(new CoreFn<>(Symbols.ASSOC) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - int n = args.length; - if (n < 1) return context.withArityError(minArityMessage(1, n)); - - if (!Utils.isOdd(n)) return context.withArityError(name() + " requires key/value pairs as successive args"); - - long juice = Juice.BUILD_DATA + (n - 1) * Juice.BUILD_PER_ELEMENT; - if (!context.checkJuice(juice)) return context.withJuiceError(); - - ACell o = args[0]; - - // convert to associative data structure. nil-> empty map - ADataStructure result = RT.ensureAssociative(o); - - // values that are non-null but not a data structure are a cast error - if ((o != null) && (result == null)) return context.withCastError(0,args, Types.DATA_STRUCTURE); - - // assoc additional elements. Must produce a valid non-null data structure after - // each assoc - for (int i = 1; i < n; i += 2) { - ACell key=args[i]; - result = RT.assoc(result, key, args[i + 1]); - if (result == null) return context.withError(ErrorCodes.ARGUMENT, "Cannot assoc value - invalid key of type "+RT.getType(key)); - } - - return context.withResult(juice, (ACell) result); - } - }); - - public static final CoreFn ASSOC_IN = reg(new CoreFn<>(Symbols.ASSOC_IN) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 3) return context.withArityError(exactArityMessage(3, args.length)); - - ASequence ixs = RT.ensureSequence(args[1]); - if (ixs == null) return context.withCastError(1,args, Types.SEQUENCE); - - int n = ixs.size(); - long juice = (Juice.GET+Juice.ASSOC) * (1L + n); - ACell data = args[0]; - ACell value= args[2]; - // simply substitute value if key sequence is empty - if (n==0) return context.withResult(juice, value); - - ADataStructure[] ass=new ADataStructure[n]; - ACell[] ks=new ACell[n]; - for (int i = 0; i < n; i++) { - ADataStructure struct = RT.ensureAssociative(data); // nil-> empty map - if (struct == null) return context.withCastError((ACell)struct,Types.DATA_STRUCTURE); // TODO: Associative type? - ass[i]=struct; - ACell k=ixs.get(i); - ks[i]=k; - data=struct.get(k); - } - - for (int i = n-1; i >=0; i--) { - ADataStructure struct=ass[i]; - ACell k=ks[i]; - value=RT.assoc(struct, k, value); - if (value==null) { - // assoc failed, so key or value type must be invlid - return context.withError(ErrorCodes.ARGUMENT,"Invalid key of type "+RT.getType(k)+" or value of type "+RT.getType(value)+" for " +name()); - } - } - return context.withResult(juice, value); - } - }); - - public static final CoreFn GET_HOLDING = reg(new CoreFn<>(Symbols.GET_HOLDING) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int n = args.length; - if (n !=1) return context.withArityError(exactArityMessage(1, n)); - - Address address=RT.ensureAddress(args[0]); - if (address == null) return context.withCastError(args[0], Types.ADDRESS); - - AccountStatus as=context.getAccountStatus(address); - if (as==null) return context.withError(ErrorCodes.NOBODY,"Account with holdings does not exist."); - ABlobMap holdings=as.getHoldings(); - - // we get the target accounts holdings for the currently executing account - ACell result=holdings.get(context.getAddress()); - - return context.withResult(Juice.LOOKUP, result); - } - }); - - public static final CoreFn SET_HOLDING = reg(new CoreFn<>(Symbols.SET_HOLDING) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int n = args.length; - if (n !=2) return context.withArityError(exactArityMessage(2, n)); - - Address address=RT.ensureAddress(args[0]); - if (address == null) return context.withCastError(args[0], Types.ADDRESS); - - // result is specified by second arg - ACell result= args[1]; - - // we set the target account holdings for the currently executing account - // might return NOBODY if account does not exist - context=(Context) context.setHolding(address,result); - if (context.isExceptional()) return (Context) context; - - return context.withResult(Juice.ASSOC, result); - } - }); - - public static final CoreFn SET_CONTROLLER = reg(new CoreFn<>(Symbols.SET_CONTROLLER) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int n = args.length; - if (n !=1) return context.withArityError(exactArityMessage(1, n)); - - // Get requested controller. Must be a valid address or null - ACell arg=args[0]; - Address controller=null; - if (arg!=null) { - controller=RT.ensureAddress(arg); - if (controller == null) return context.withCastError(arg, Types.ADDRESS); - if (context.getAccountStatus(controller)==null) { - return context.withError(ErrorCodes.NOBODY, name()+" must be passed an address for an existing account as controller."); - } - } - - context=(Context) context.setController(controller); - if (context.isExceptional()) return (Context) context; - - return context.withResult(Juice.ASSOC, controller); - } - }); - - public static final CoreFn SET_KEY = reg(new CoreFn<>(Symbols.SET_KEY) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int n = args.length; - if (n !=1) return context.withArityError(exactArityMessage(1, n)); - - ACell arg=args[0]; - - // Check an account key is being used as argument. nil is permitted - AccountKey publicKey=RT.ensureAccountKey(arg); - if ((publicKey == null)&&(arg!=null)) return context.withCastError(arg, Types.BLOB); - - context=(Context) context.setAccountKey(publicKey); - if (context.isExceptional()) return (Context) context; - - return context.withResult(Juice.ASSOC, publicKey); - } - }); - - - public static final CoreFn GET = reg(new CoreFn<>(Symbols.GET) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int n = args.length; - if ((n < 2) || (n > 3)) { - return context.withArityError(name() + " requires exactly 2 or 3 arguments"); - } - - ACell result; - ACell coll = args[0]; - if (coll == null) { - // Treat nil as empty collection with no keys - result = (n == 3) ? (ACell)args[2] : null; - } else if (n == 2) { - ADataStructure gettable = RT.ensureDataStructure(coll); - if (gettable == null) return context.withCastError(coll, Types.DATA_STRUCTURE); - result = gettable.get(args[1]); - } else { - ADataStructure gettable = RT.ensureDataStructure(coll); - if (gettable == null) return context.withCastError(coll, Types.DATA_STRUCTURE); - result = gettable.get(args[1], args[2]); - } - long juice = Juice.GET; - return context.withResult(juice, result); - } - }); - - public static final CoreFn GET_IN = reg(new CoreFn<>(Symbols.GET_IN) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int n = args.length; - if ((n < 2) || (n > 3)) { - return context.withArityError(name() + " requires exactly 2 or 3 arguments"); - } - - ASequence ixs = RT.ensureSequence(args[1]); - if (ixs == null) return context.withCastError(args[1], Types.SEQUENCE); - - ACell notFound=(n<3)?null:args[2]; - - int il = ixs.size(); - long juice = Juice.GET * (1L + il); - ACell result = (ACell) args[0]; - for (int i = 0; i < il; i++) { - if (result == null) { - result=notFound; - break; // gets in nil produce not-found - } - ADataStructure gettable = RT.ensureDataStructure(result); - if (gettable == null) return context.withCastError(result, Types.DATA_STRUCTURE); - - ACell k=ixs.get(i); - if (gettable.containsKey(k)) { - result = gettable.get(k); - } else { - return context.withResult(juice, notFound); - } - - } - return context.withResult(juice, result); - } - }); - - public static final CoreFn CONTAINS_KEY_Q = reg(new CoreFn<>(Symbols.CONTAINS_KEY_Q) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int n = args.length; - if (n != 2) return context.withArityError(exactArityMessage(2, n)); - - CVMBool result; - ACell coll = args[0]; - if (coll == null) { - result = CVMBool.FALSE; // treat nil as empty collection - } else { - ADataStructure gettable = RT.ensureDataStructure(args[0]); - if (gettable == null) return context.withCastError(args[0], Types.DATA_STRUCTURE); - result = CVMBool.of(gettable.containsKey((ACell) args[1])); - } - - long juice = Juice.GET; - return context.withResult(juice, result); - } - }); - - public static final CoreFn SUBSET_Q = reg(new CoreFn<>(Symbols.SUBSET_Q) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int n = args.length; - if (n != 2) return context.withArityError(exactArityMessage(2, n)); - - ASet s0=RT.ensureSet(args[0]); - if (s0==null) return context.withCastError(args[0], Types.SET); - - long juice = Juice.SET_COMPARE_PER_ELEMENT*s0.count(); - if (!context.checkJuice(juice)) return context.withJuiceError(); - - ASet s1=RT.ensureSet(args[1]); - if (s1==null) return context.withCastError(args[1], Types.SET); - - CVMBool result=CVMBool.of(s0.isSubset(s1)); - return context.withResult(juice, result); - } - }); - - public static final CoreFn> DISSOC = reg(new CoreFn<>(Symbols.DISSOC) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - int n = args.length; - if (args.length < 1) return context.withArityError(minArityMessage(1, args.length)); - - AMap result = RT.ensureMap(args[0]); - if (result == null) return context.withCastError(args[0], Types.MAP); - - for (int i = 1; i < n; i++) { - result = result.dissoc((ACell) args[i]); - } - long juice = Juice.BUILD_DATA + (n - 1) * Juice.BUILD_PER_ELEMENT; - return context.withResult(juice, result); - } - }); - - public static final CoreFn> CONJ = reg(new CoreFn<>(Symbols.CONJ) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - int numAdditions = args.length - 1; - if (args.length <= 0) return context.withArityError(name() + " requires a data structure as first argument"); - - // compute juice up front - long juice = Juice.BUILD_DATA + Juice.BUILD_PER_ELEMENT * numAdditions; - if (!context.checkJuice(juice)) return context.withJuiceError(); - - ADataStructure result = RT.castDataStructure(args[0]); - if (result == null) return context.withCastError(0,args, Types.DATA_STRUCTURE); - - for (int i = 0; i < numAdditions; i++) { - int argIndex=i+1; - ACell val = (ACell) args[argIndex]; - result = result.conj(val); - if (result == null) return context.withError(ErrorCodes.ARGUMENT,"Failure to 'conj' argument at position "+argIndex+" (with Type "+RT.getType(val)+"). Probably not a legal value for this data structure?"); // must be a failed map conj? - } - return context.withResult(juice, result); - } - }); - - public static final CoreFn> DISJ = reg(new CoreFn<>(Symbols.DISJ) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - if (args.length < 1) return context.withArityError(minArityMessage(1, args.length)); - - // compute juice up front - int numAdditions = args.length - 1; - long juice = Juice.BUILD_DATA + Juice.BUILD_PER_ELEMENT * numAdditions; - if (!context.checkJuice(juice)) return context.withJuiceError(); - - ASet result = RT.ensureSet(args[0]); - if (result == null) return context.withCastError(0,args, Types.SET); - - - for (int i = 0; i < numAdditions; i++) { - int argIndex=i+1; - ACell val = args[argIndex]; - result = result.exclude(val); - } - - return context.withResult(juice, result); - } - }); - - public static final CoreFn> CONS = reg(new CoreFn<>(Symbols.CONS) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - int n = args.length; - if (args.length < 2) return context.withArityError(minArityMessage(2, args.length)); - - long juice = Juice.BUILD_DATA + Juice.BUILD_PER_ELEMENT * (n - 1); - if (!context.checkJuice(juice)) return context.withJuiceError(); - - // get sequence from last argument - int lastIndex=n-1; - ASequence seq = RT.sequence(args[lastIndex]); - if (seq == null) return context.withCastError(lastIndex,args, Types.SEQUENCE); - - AList list = RT.cons((ACell) args[n - 2], seq); - - for (int i = n - 3; i >= 0; i--) { - list = RT.cons((ACell)args[i], list); - } - return context.withResult(juice, list); - } - }); - - public static final CoreFn FIRST = reg(new CoreFn<>(Symbols.FIRST) { - // note we could define this as (nth coll 0) but this is more efficient - - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell maybeColl = args[0]; - Long n= RT.count(maybeColl); - if (n == null) return context.withCastError(0,args, Types.SEQUENCE); - if (n<1) return context.withBoundsError(0); - ACell result = RT.nth(maybeColl,0); - - long juice = Juice.SIMPLE_FN; - return context.withResult(juice, result); - } - }); - - public static final CoreFn SECOND = reg(new CoreFn<>(Symbols.SECOND) { - // note we could define this as (nth coll 1) but this is more efficient - - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell maybeColl = (ACell) args[0]; - Long n= RT.count(maybeColl); - if (n == null) return context.withCastError(0,args, Types.SEQUENCE); - if (n<2) return context.withBoundsError(1); - ACell result = RT.nth(maybeColl,1); - - long juice = Juice.SIMPLE_FN; - return context.withResult(juice, result); - } - }); - - public static final CoreFn LAST = reg(new CoreFn<>(Symbols.LAST) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell a = args[0]; - - Long n = RT.count(a); - if (n == null) return context.withCastError(0,args, Types.SEQUENCE); - if (n<=0) return context.withBoundsError(-1); - - ACell result = RT.nth(a,n-1); - - long juice = Juice.SIMPLE_FN; - return context.withResult(juice, result); - } - }); - - public static final CoreFn EQUALS = reg(new CoreFn<>(Symbols.EQUALS) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - - // all arities OK, all args OK - CVMBool result = CVMBool.of(RT.allEqual(args)); - return context.withResult(Juice.EQUALS, result); - } - }); - - public static final CoreFn EQ = reg(new CoreFn<>(Symbols.EQ) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - // all arities OK, but need to watch for non-numeric arguments - Boolean result = RT.eq(args); - if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); - - return context.withResult(Juice.NUMERIC_COMPARE, CVMBool.create(result)); - } - }); - - public static final CoreFn GE = reg(new CoreFn<>(Symbols.GE) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - // all arities OK - Boolean result = RT.ge(args); - if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); - - return context.withResult(Juice.NUMERIC_COMPARE, CVMBool.create(result)); - } - }); - - public static final CoreFn GT = reg(new CoreFn<>(Symbols.GT) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - // all arities OK - - Boolean result = RT.gt(args); - if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); - - return context.withResult(Juice.NUMERIC_COMPARE, CVMBool.create(result)); - } - }); - - public static final CoreFn LE = reg(new CoreFn<>(Symbols.LE) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - // all arities OK - - Boolean result = RT.le(args); - if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); - - return context.withResult(Juice.NUMERIC_COMPARE, CVMBool.create(result)); - } - }); - - public static final CoreFn LT = reg(new CoreFn<>(Symbols.LT) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - // all arities OK - - Boolean result = RT.lt(args); - if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); - - return context.withResult(Juice.NUMERIC_COMPARE, CVMBool.create(result)); - } - }); - - public static final CoreFn INC = reg(new CoreFn<>(Symbols.INC) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell a = args[0]; - CVMLong result = RT.inc(a); - if (result == null) return context.withCastError(0,args, Types.LONG); - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn DEC = reg(new CoreFn<>(Symbols.DEC) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell a = args[0]; - CVMLong result = RT.dec(a); - if (result == null) return context.withCastError(0,args, Types.LONG); - - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn BOOLEAN = reg(new CoreFn<>(Symbols.BOOLEAN) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - // Boolean cast always works for any value - CVMBool result = (RT.bool(args[0])) ? CVMBool.TRUE : CVMBool.FALSE; - - return context.withResult(Juice.SIMPLE_FN, result); - } - }); - - public static final CorePred BOOLEAN_Q = reg(new CorePred(Symbols.BOOLEAN_Q) { - @Override - public boolean test(ACell val) { - return RT.isBoolean(val); - } - }); - - public static final CoreFn ENCODING = reg(new CoreFn<>(Symbols.ENCODING) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell a = args[0]; - ABlob encoding=Format.encodedBlob(a); - - long juice=Juice.addMul(Juice.BLOB, encoding.count(), Juice.BLOB_PER_BYTE); - return context.withResult(juice, encoding); - } - }); - - public static final CoreFn LONG = reg(new CoreFn<>(Symbols.LONG) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell a = args[0]; - CVMLong result = RT.castLong(a); - if (result == null) return context.withCastError(0, args,Types.LONG); - - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn DOUBLE = reg(new CoreFn<>(Symbols.DOUBLE) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell a = args[0]; - CVMDouble result = RT.castDouble(a); - if (result == null) return context.withCastError(0, args,Types.DOUBLE); - - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn CHAR = reg(new CoreFn<>(Symbols.CHAR) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell a = args[0]; - CVMChar result = RT.toCharacter(a); - if (result == null) return context.withCastError(0,args, Types.CHARACTER); - - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn BYTE = reg(new CoreFn<>(Symbols.BYTE) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell a = args[0]; - CVMByte result = RT.castByte(a); - if (result == null) return context.withCastError(0,args, Types.BYTE); - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn PLUS = reg(new CoreFn<>(Symbols.PLUS) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - // All arities OK - - APrimitive result = RT.plus(args); - if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn MINUS = reg(new CoreFn<>(Symbols.MINUS) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length < 1) return context.withArityError(minArityMessage(1, args.length)); - APrimitive result = RT.minus(args); - if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn TIMES = reg(new CoreFn<>(Symbols.TIMES) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - // All arities OK - APrimitive result = RT.times(args); - if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn DIVIDE = reg(new CoreFn<>(Symbols.DIVIDE) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length < 1) return context.withArityError(minArityMessage(1, args.length)); - - CVMDouble result = RT.divide(args); - if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn FLOOR = reg(new CoreFn<>(Symbols.FLOOR) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - CVMDouble result = RT.floor(args[0]); - if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - - public static final CoreFn CEIL = reg(new CoreFn<>(Symbols.CEIL) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - CVMDouble result = RT.ceil(args[0]); - if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - - public static final CoreFn SQRT = reg(new CoreFn<>(Symbols.SQRT) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - CVMDouble result = RT.sqrt(args[0]); - if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn ABS = reg(new CoreFn<>(Symbols.ABS) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - APrimitive result = RT.abs(args[0]); - if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn SIGNUM = reg(new CoreFn<>(Symbols.SIGNUM) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - ACell result = RT.signum(args[0]); - if (result == null) return context.withCastError(args[0], Types.NUMBER); - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn MOD = reg(new CoreFn<>(Symbols.MOD) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); - - CVMLong la=RT.ensureLong(args[0]); - CVMLong lb=RT.ensureLong(args[1]); - if ((lb==null)||(la==null)) return context.withCastError(Types.LONG); - - long num = la.longValue(); - long denom = lb.longValue(); - if (denom==0) return context.withArgumentError("Divsion by zero in "+name()); - - long m = num % denom; - if (m<0) m+=Math.abs(denom); // Correct for Euclidean modular function - CVMLong result=CVMLong.create(m); - - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn REM = reg(new CoreFn<>(Symbols.REM) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); - - CVMLong la=RT.ensureLong(args[0]); - CVMLong lb=RT.ensureLong(args[1]); - if ((lb==null)||(la==null)) return context.withCastError(Types.LONG); - - long num = la.longValue(); - long denom = lb.longValue(); - if (denom==0) return context.withArgumentError("Divsion by zero in "+name()); - - long m = num % denom; - CVMLong result=CVMLong.create(m); - - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn QUOT = reg(new CoreFn<>(Symbols.QUOT) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); - - CVMLong la=RT.ensureLong(args[0]); - CVMLong lb=RT.ensureLong(args[1]); - if ((lb==null)||(la==null)) return context.withCastError(Types.LONG); - - long num = la.longValue(); - long denom = lb.longValue(); - if (denom==0) return context.withArgumentError("Divsion by zero in "+name()); - - long m = num / denom; - CVMLong result=CVMLong.create(m); - - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - - public static final CoreFn POW = reg(new CoreFn<>(Symbols.POW) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); - - CVMDouble result = RT.pow(args); - if (result==null) return context.withCastError(Types.DOUBLE); - - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn EXP = reg(new CoreFn<>(Symbols.EXP) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - CVMDouble result = RT.exp(args[0]); - if (result==null) return context.withCastError(0,Types.DOUBLE); - - return context.withResult(Juice.ARITHMETIC, result); - } - }); - - public static final CoreFn NOT = reg(new CoreFn<>(Symbols.NOT) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - CVMBool result = CVMBool.of(!RT.bool(args[0])); - return context.withResult(Juice.SIMPLE_FN, result); - } - }); - - public static final CoreFn HASH = reg(new CoreFn<>(Symbols.HASH) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ABlob blob=RT.ensureBlob(args[0]); - if (blob==null) return context.withCastError(0,args, Types.BLOB); - - Hash result = blob.getContentHash(); - return context.withResult(Juice.HASH, result); - } - }); - - public static final CoreFn COUNT = reg(new CoreFn<>(Symbols.COUNT) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - Long result = RT.count(args[0]); - if (result == null) return context.withCastError(0,args, Types.DATA_STRUCTURE); - - return context.withResult(Juice.SIMPLE_FN, CVMLong.create(result)); - } - }); - - public static final CoreFn EMPTY = reg(new CoreFn<>(Symbols.EMPTY) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ACell o = args[0]; - - // emptying nil is still nil - if (o == null) return context.withResult(Juice.SIMPLE_FN, null); - - ADataStructure coll = RT.ensureDataStructure(o); - if (coll == null) return context.withCastError(0,args, Types.DATA_STRUCTURE); - - ACell result = coll.empty(); - return context.withResult(Juice.SIMPLE_FN, result); - } - }); - - public static final CoreFn NTH = reg(new CoreFn<>(Symbols.NTH) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - // Arity 2 - if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); - - // First argument must be a countable data structure - ACell arg = (ACell) args[0]; - Long n = RT.count(arg); - if (n == null) return context.withCastError(arg, Types.SEQUENCE); - - // Second argument should be a Long index - CVMLong ix = RT.ensureLong(args[1]); - if (ix == null) return context.withCastError(1,args, Types.LONG); - - long i=ix.longValue(); - - // BOUNDS error if access is out of bounds - if ((i < 0) || (i >= n)) return context.withBoundsError(i); - - // We know the object is a countable collection, so safe to use 'nth' - ACell result = RT.nth(arg, i); - - return context.withResult(Juice.SIMPLE_FN, result); - } - }); - - public static final CoreFn> NEXT = reg(new CoreFn<>(Symbols.NEXT) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - ASequence seq = RT.sequence(args[0]); - if (seq == null) return context.withCastError(0,args, Types.SEQUENCE); - - ASequence result = seq.next(); - // TODO: probably needs to cost a lot? - return context.withResult(Juice.SIMPLE_FN, result); - } - }); - - public static final CoreFn RECUR = reg(new CoreFn<>(Symbols.RECUR) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - // any arity OK? - - AExceptional result = RecurValue.wrap(args); - - return context.withException(Juice.RECUR, result); - } - }); - - public static final CoreFn TAILCALL_STAR = reg(new CoreFn<>(Symbols.TAILCALL_STAR) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int n=args.length; - if (n < 1) return context.withArityError(this.minArityMessage(1, n)); - - AFn f=RT.ensureFunction(args[0]); - if (f==null) return context.withCastError(0, args, Types.FUNCTION); - - ACell[] tailArgs=Arrays.copyOfRange(args, 1, args.length); - AExceptional result = TailcallValue.wrap(f,tailArgs); - - return context.withException(Juice.RECUR, result); - } - }); - - public static final CoreFn ROLLBACK = reg(new CoreFn<>(Symbols.ROLLBACK) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - AExceptional result = RollbackValue.wrap((ACell)args[0]); - - return context.withException(Juice.RETURN, result); - } - }); - - public static final CoreFn HALT = reg(new CoreFn<>(Symbols.HALT) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int n = args.length; - if (n > 1) return context.withArityError(this.maxArityMessage(1, n)); - - AExceptional result = HaltValue.wrap((n > 0) ? (ACell)args[0] : null); - - return context.withException(Juice.RETURN, result); - } - }); - - public static final CoreFn RETURN = reg(new CoreFn<>(Symbols.RETURN) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - AExceptional result = ReturnValue.wrap((ACell)args[0]); - return context.withException(Juice.RETURN, result); - } - }); - - public static final CoreFn FAIL = reg(new CoreFn<>(Symbols.FAIL) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int alen = args.length; - if (alen > 2) return context.withArityError(maxArityMessage(2, alen)); - - // default to :ASSERT if no error code provided. Error code cannot be nil. - ACell code = (alen == 2) ? (ACell)args[0] : ErrorCodes.ASSERT; - if (code==null) return context.withError(ErrorCodes.ARGUMENT,"Error code cannot be nil"); - - // get message, or nil if not provided - ACell message = (alen >0) ? (ACell)args[alen-1] : null; - ErrorValue error = ErrorValue.createRaw(code, message); - - return context.withError(error); - } - }); - - public static final CoreFn APPLY = reg(new CoreFn<>(Symbols.APPLY) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - int alen = args.length; - if (alen < 2) return context.withArityError(minArityMessage(2, alen)); - - final AFn fn = RT.castFunction(args[0]); - if (fn==null ) return context.withCastError(0,args, Types.FUNCTION); - - int lastIndex=alen-1; - ACell lastArg = args[lastIndex]; - ASequence coll = RT.ensureSequence(lastArg); - if (coll == null) return context.withCastError(lastIndex,args, Types.SEQUENCE); - - int vlen = coll.size(); // variable arg length - - // Build an array of arguments for the function - // TODO: bounds on number of arguments? - int n = (alen - 2) + vlen; // number of args to pass to function - ACell[] applyArgs; - if (alen > 2) { - applyArgs = new ACell[n]; - for (int i = 0; i < (alen - 2); i++) { - applyArgs[i] = args[i + 1]; - } - int ix = alen - 2; - for (Iterator it = coll.iterator(); it.hasNext();) { - applyArgs[ix++] = it.next(); - } - } else { - applyArgs = coll.toCellArray(); - } - - Context rctx = context.invoke(fn, applyArgs); - return rctx.consumeJuice(Juice.APPLY); - } - }); - - public static final CoreFn> INTO = reg(new CoreFn<>(Symbols.INTO) { - - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - if (args.length != 2) return context.withArityError(exactArityMessage(2, args.length)); - - ACell a0 = args[0]; - ADataStructure result = RT.ensureDataStructure(a0); - if ((a0 != null) && (result == null)) return context.withCastError(0,args, Types.DATA_STRUCTURE); - - long juice = Juice.BUILD_DATA; - ACell a1 = args[1]; - if (a0 == null) { - // First argument is null. Just keep second arg as complete data structure - result = RT.ensureDataStructure(a1); - if ((a1 != null) && (result == null)) return context.withCastError(a1, Types.DATA_STRUCTURE); - } else { - Long n=RT.count(a1); - if (n == null) return context.withCastError(a1, Types.DATA_STRUCTURE); - - // check juice before running potentially expansive computation - juice += Juice.BUILD_PER_ELEMENT * n; - if (!context.checkJuice(juice)) return context.withJuiceError(); - - ASequence seq = RT.sequence(a1); - if (seq == null) return context.withCastError(a1, Types.DATA_STRUCTURE); - - result = result.conjAll(seq); - if (result == null) return context.withError(ErrorCodes.ARGUMENT,"Invalid element type for 'into'"); - } - - return context.withResult(juice, result); - } - }); - - public static final CoreFn> MERGE = reg(new CoreFn<>(Symbols.MERGE) { - - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - int n=args.length; - if (n==0) return context.withResult(Juice.BUILD_DATA,Maps.empty()); - - // TODO: handle blobmaps? - - ACell arg0=args[0]; - AHashMap result=RT.ensureHashMap(arg0); - if (result == null) return context.withCastError(arg0, Types.MAP); - - long juice=Juice.BUILD_DATA; - for (int i=1; i argMap=RT.ensureHashMap(argi); - if (argMap == null) return context.withCastError(argi, Types.MAP); - - long size=argMap.count(); - juice=Juice.addMul(juice,size,Juice.BUILD_PER_ELEMENT); - - if (!context.checkJuice(juice)) return context.withJuiceError(); - - result=result.merge(argMap); - } - - return context.withResult(juice, result); - } - - }); - - public static final CoreFn> MAP = reg(new CoreFn<>(Symbols.MAP) { - @SuppressWarnings("unchecked") - @Override - public Context> invoke(Context context, ACell[] args) { - if (args.length < 2) return context.withArityError(minArityMessage(2, args.length)); - - // check and cast first argument to a function - ACell fnArg = args[0]; - AFn f = RT.castFunction(fnArg); - if (f == null) return context.withCastError(fnArg, Types.FUNCTION); - - // remaining arguments determine function arity to use - int fnArity = args.length - 1; - ACell[] xs = new ACell[fnArity]; - ASequence[] seqs = new ASequence[fnArity]; - - int length = Integer.MAX_VALUE; - for (int i = 0; i < fnArity; i++) { - ACell maybeSeq = args[1 + i]; - ASequence seq = RT.sequence(maybeSeq); - if (seq == null) return context.withCastError(maybeSeq, Types.SEQUENCE); - seqs[i] = seq; - length = Math.min(length, seq.size()); - } - - final long juice = Juice.addMul(Juice.MAP, Juice.BUILD_DATA , length); - if (!context.checkJuice(juice)) return context.withJuiceError(); - - ArrayList al = new ArrayList<>(); - for (int i = 0; i < length; i++) { - for (int j = 0; j < fnArity; j++) { - xs[j] = seqs[j].get(i); - } - context = (Context) context.invoke(f, xs); - if (context.isExceptional()) return (Context>) context; - ACell r = context.getResult(); - al.add(r); - } - - ASequence result = Vectors.create(al); - return context.withResult(juice, result); - } - }); - - public static final CoreFn REDUCE = reg(new CoreFn<>(Symbols.REDUCE) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context ctx, ACell[] args) { - int ac=args.length; - if ((ac<2)||(ac > 3)) return ctx.withArityError(exactArityMessage(3, ac)); - - // check and cast first argument to a function - ACell fnArg = args[0]; - AFn fn = RT.castFunction(fnArg); - if (fn == null) return ctx.withCastError(0,args, Types.FUNCTION); - - - // last arg must be a data structure - ACell maybeSeq = (ACell) args[ac-1]; - ADataStructure seq = (maybeSeq==null)?Vectors.empty():RT.ensureDataStructure(maybeSeq); - if (seq == null) return ctx.withCastError(ac-1,args, Types.SEQUENCE); - long n = seq.count(); - - ACell result; // Initial value, can be anything - long start=0; // first element for reduction - if (ac==3) { - result=args[1]; - } else { - // 2 arg form of reduce must apply function directly to 0 or 1 elements - int initial=(int)Math.min(2,n); // number of initial arguments to consume - if (initial==0) { - return reduceResult(ctx.invoke(fn, ACell.EMPTY_ARRAY)); - } else if (initial==1) { - return reduceResult(ctx.invoke(fn, new ACell[] {seq.get(0)})); - } - result=seq.get(0); - start = 1; - } - - // Need to reduce over remaining elements - ACell[] xs = new ACell[2]; // accumulator, next element - for (long i = start; i < n; i++) { - xs[0] = result; - xs[1] = seq.get(i); - ctx = ctx.invoke(fn, xs); - if (ctx.isExceptional()) { - return reduceResult(ctx); - } else { - result=ctx.getResult(); - } - } - - return ctx.withResult(Juice.REDUCE, result); - } - }); - - // Helper function for reduce - private static final Context reduceResult(Context ctx) { - Object ex=ctx.getValue(); // might be an ACell or Exception. We need to check for a Reduced result only - if (ex instanceof Reduced) { - ctx=ctx.withResult(((Reduced)ex).getValue()); - } - return ctx.consumeJuice(Juice.REDUCE); // bail out with exception - } - - public static final CoreFn REDUCED = reg(new CoreFn<>(Symbols.REDUCED) { - @SuppressWarnings("unchecked") - @Override - public Context invoke(Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); - - AExceptional result = Reduced.wrap((ACell) args[0]); - return context.withException(Juice.RETURN, result); - } - }); - - // ===================================================================================================== - // Predicates - - public static final CorePred NIL_Q = reg(new CorePred(Symbols.NIL_Q) { - @Override - public boolean test(ACell val) { - return val == null; - } - }); - - public static final CorePred VECTOR_Q = reg(new CorePred(Symbols.VECTOR_Q) { - @Override - public boolean test(ACell val) { - return val instanceof AVector; - } - }); - - public static final CorePred LIST_Q = reg(new CorePred(Symbols.LIST_Q) { - @Override - public boolean test(ACell val) { - return val instanceof AList; - } - }); - - public static final CorePred SET_Q = reg(new CorePred(Symbols.SET_Q) { - @Override - public boolean test(ACell val) { - return val instanceof ASet; - } - }); - - public static final CorePred MAP_Q = reg(new CorePred(Symbols.MAP_Q) { - @Override - public boolean test(ACell val) { - return val instanceof AMap; - } - }); - - public static final CorePred COLL_Q = reg(new CorePred(Symbols.COLL_Q) { - @Override - public boolean test(ACell val) { - return val instanceof ADataStructure; - } - }); - - public static final CorePred EMPTY_Q = reg(new CorePred(Symbols.EMPTY_Q) { - @Override - public boolean test(ACell val) { - // consider null as an empty object - // like with clojure - if (val == null) return true; - - return (val instanceof ADataStructure) && ((ADataStructure) val).isEmpty(); - } - }); - - public static final CorePred SYMBOL_Q = reg(new CorePred(Symbols.SYMBOL_Q) { - @Override - public boolean test(ACell val) { - return val instanceof Symbol; - } - }); - - public static final CorePred KEYWORD_Q = reg(new CorePred(Symbols.KEYWORD_Q) { - @Override - public boolean test(ACell val) { - return val instanceof Keyword; - } - }); - - public static final CorePred BLOB_Q = reg(new CorePred(Symbols.BLOB_Q) { - @Override - public boolean test(ACell val) { - if (!(val instanceof ABlob)) return false; - return ((ABlob)val).isRegularBlob(); - } - }); - - public static final CorePred ADDRESS_Q = reg(new CorePred(Symbols.ADDRESS_Q) { - @Override - public boolean test(ACell val) { - return val instanceof Address; - } - }); - - public static final CorePred LONG_Q = reg(new CorePred(Symbols.LONG_Q) { - @Override - public boolean test(ACell val) { - return val instanceof CVMLong; - } - }); - - public static final CorePred STR_Q = reg(new CorePred(Symbols.STR_Q) { - @Override - public boolean test(ACell val) { - return val instanceof AString; - } - }); - - public static final CorePred NUMBER_Q = reg(new CorePred(Symbols.NUMBER_Q) { - @Override - public boolean test(ACell val) { - return RT.isNumber(val); - } - }); - - public static final CorePred NAN_Q = reg(new CorePred(Symbols.NAN_Q) { - @Override - public boolean test(ACell val) { - return RT.isNaN(val); - } - }); - - public static final CorePred FN_Q = reg(new CorePred(Symbols.FN_Q) { - @Override - public boolean test(ACell val) { - return val instanceof AFn; - } - }); - - public static final CorePred ZERO_Q = reg(new CorePred(Symbols.ZERO_Q) { - @Override - public boolean test(ACell val) { - if (!RT.isNumber(val)) return false; - INumeric n = RT.ensureNumber(val); - - // According to the IEEE 754 standard, negative zero and positive zero should - // compare as equal with the usual (numerical) comparison operators - // This is the behaviour in Java - return n.doubleValue() == 0.0; - } - }); - - - - // ===================================================================================================== - // Core environment generation - - static Symbol symbolFor(ACell o) { - if (o instanceof CoreFn) return ((CoreFn) o).getSymbol(); - throw new Error("Cant get symbol for object of type " + o.getClass()); - } - - private static AHashMap register(AHashMap env, ACell o) { - Symbol sym = symbolFor(o); - assert (!env.containsKey(sym)) : "Duplicate core declaration: " + sym; - return env.assoc(sym, o); - } - - /** - * Bootstrap procedure to load the core.cvx library - * - * @param env Initial environment map - * @return Loaded environment map - * @throws IOException - */ - private static Context registerCoreCode(AHashMap env) throws IOException { - - //Awe use a fake state to build the initial environment with core address. - Address ADDR = Address.ZERO; - State state = State.EMPTY.putAccount(ADDR, AccountStatus.createActor()); - Context ctx = Context.createFake(state, ADDR); - - // Map in forms from env. - for (Map.Entry me : env.entrySet()) { - ctx=ctx.define(me.getKey(), me.getValue()); - } - - ACell form = null; - - // Compile and execute forms in turn. Later definitions can use earlier macros! - AList forms = Reader.readAll(Utils.readResourceAsString("convex/core.cvx")); - for (ACell f : forms) { - form = f; - ctx = ctx.expandCompile(form); - if (ctx.isExceptional()) { - throw new Error("Error compiling form: " + form + "\nException : " + ctx.getExceptional()); - } - AOp op = (AOp)ctx.getResult(); - ctx = ctx.execute(op); - // System.out.println("Core compilation juice: "+ctx.getJuice()); - assert (!ctx.isExceptional()) : "Error executing op: "+ op+ "\nException : "+ ctx.getExceptional().toString(); - } - - return ctx; - } - - @SuppressWarnings("unchecked") - private static Context applyDocumentation(Context ctx) throws IOException { - - AMap> metas = Reader.read(Utils.readResourceAsString("convex/core/metadata.cvx")); - - for (Map.Entry> entry : metas.entrySet()) { - try { - Symbol sym = entry.getKey(); - AHashMap meta = entry.getValue(); - MapEntry definedEntry = ctx.getEnvironment().getEntry(sym); - - if (definedEntry == null) { - // No existing value, might be a special. - AHashMap doc = (AHashMap) meta.get(Keywords.DOC); - if (doc == null) { - // No docs. - System.err.println("CORE WARNING: Missing :doc tag in metadata for: " + sym); - continue; - } else { - if (meta.get(Keywords.SPECIAL_Q) == CVMBool.TRUE) { - // Create a fake entry for special symbols. - ctx=ctx.define(sym, sym); - definedEntry = MapEntry.create(sym, sym); - } else { - System.err.println("CORE WARNING: Documentation for non-existent core symbol: " + sym); - continue; - } - } - } - - ctx = ctx.defineWithSyntax(Syntax.create(sym, meta), definedEntry.getValue()); - } catch (Throwable ex) { - throw new Error("Error applying documentation: " + entry, ex); - } - } - - return ctx; - } - - - - - static { - // Set up `convex.core` environment - AHashMap coreEnv = Maps.empty(); - AHashMap> coreMeta = Maps.empty(); - - try { - - // Register all objects from registered runtime - for (ACell o : tempReg) { - coreEnv = register(coreEnv, o); - } - - Context ctx = registerCoreCode(coreEnv); - ctx=applyDocumentation(ctx); - - coreEnv = ctx.getEnvironment(); - coreMeta = ctx.getMetadata(); - - METADATA = coreMeta; - ENVIRONMENT = coreEnv; - } catch (Throwable e) { - e.printStackTrace(); - throw new Error("Error initialising core!",e); - } - } -} diff --git a/convex-core/src/main/java/convex/core/lang/IFn.java b/convex-core/src/main/java/convex/core/lang/IFn.java deleted file mode 100644 index cd26d5e62..000000000 --- a/convex-core/src/main/java/convex/core/lang/IFn.java +++ /dev/null @@ -1,25 +0,0 @@ -package convex.core.lang; - -import convex.core.data.ACell; - -/** - * Interface for invokable objects with function interface. - * - * "Any sufficiently advanced technology is indistinguishable from magic." - - * Arthur C. Clarke - * - * @param Return type of function - */ -public interface IFn { - - /** - * Invoke this function in the given context. - * - * @param context Context in which the function is to be executed - * @param args Arguments to the function - * @return Context containing result of function invocation, or an exceptional - * value - */ - public abstract Context invoke(Context context, ACell[] args); - -} diff --git a/convex-core/src/main/java/convex/core/lang/Juice.java b/convex-core/src/main/java/convex/core/lang/Juice.java deleted file mode 100644 index 2e95cc492..000000000 --- a/convex-core/src/main/java/convex/core/lang/Juice.java +++ /dev/null @@ -1,332 +0,0 @@ -package convex.core.lang; - -/** - * Static class defining juice costs for executable operations. - * - * "LISP programmers know the value of everything and the cost of nothing." - - * Alan Perlis - * - */ -public class Juice { - /** - * Juice required to resolve a constant value - * - * Very cheap, no allocs / lookup. - */ - public static final long CONSTANT = 10; - - /** - * Juice required to define a value in the current environment. - * - * We make this somewhat expensive - we want to discourage over-use as a general rule - * since it writes to global chain state. However memory accounting helps discourage - * superfluous defs, so it only needs to reflect execution cost. - */ - public static final long DEF = 100; - - /** - * Juice required to look up a value in the local environment. - */ - public static final long LOOKUP = 15; - - /** - * Juice required to look up a value in the dynamic environment. - * - * Potentially a bit pricey since read only, but might hit storage so..... - */ - public static final long LOOKUP_DYNAMIC = 40; - - /** - * Juice required to look up a symbol with a regular Address - */ - public static final long LOOKUP_SYM = LOOKUP_DYNAMIC+CONSTANT; - - /** - * Juice required to execute a Do block - * - * Very cheap, no allocs. - */ - public static final long DO = 10; - - - /** - * Juice required to execute a Let block - * - * Fairly cheap but some parameter munging required. Might revisit binding - * costs? - */ - public static final long LET = 30; - - - - /** - * Juice required to execute a Cond expression - * - * Pretty cheap, nothing nasty here (though conditions / results themselves - * might get pricey). - */ - public static final long COND_OP = 20; - - /** - * Juice required to create a lambda - * - * Sort of expensive - might allocate a bunch of stuff for the closure? - */ - public static final long LAMBDA = 100; - - /** - * Juice required to call an Actor - * - * Slightly expensive for context switching? - */ - public static final long CALL_OP = 100; - - /** - * Juice required to build a data structure. Make a bit expensive? - */ - protected static final long BUILD_DATA = 50; - - /** - * Juice required per element changed when building a data structure. Map entries - * count as two elements. - * - * We need to be a bit harsh on this! Risk of consuming too much heap space, - * might also result in multiple allocs for tree structures. - */ - protected static final long BUILD_PER_ELEMENT = 50; - - protected static final long MAP = 100; - protected static final long REDUCE = 100; - - /** - * Juice for general object equality comparison - * - * Pretty cheap. - */ - public static final long EQUALS = 20; - - /** - * Juice for numeric comparison - * - * Pretty cheap. Bit of casting perhaps. - */ - public static final long NUMERIC_COMPARE = 20; - - /** - * Juice for an apply operation - * - * Bit of cost to allow for parameter construction. Might need to revisit for - * bigger sequences? - */ - public static final long APPLY = 50; - - /** - * Juice for a cryptographic hash - * - * Expensive. - */ - public static final long HASH = 10000; - - /** - * Juice for a very cheap operation. O(1), no new cell allocations or non-trivial lookups. - */ - public static final long CHEAP_OP = 10; - - /** - * Juice for a simple built-in core function. Simple operations are assumed to - * require no expensive resource access, and operate with O(1) allocations - */ - public static final long SIMPLE_FN = 20; - - /** - * Juice for constructing a String - * - * Fairly cheap, since mostly in fast code, but charge extra for additional - * chars. - */ - protected static final long STR = SIMPLE_FN; - protected static final long STR_PER_CHAR = 5; - - /** - * Juice for storing a new constant value permanently in on-chain state Charged - * per node stored - */ - protected static final long STORE = 1000; - - protected static final long FETCH = 100; - - protected static final long ARITHMETIC = SIMPLE_FN; - - protected static final long ADDRESS = 100; - - protected static final long BALANCE = 200; - - /** - * Juice for creation of a blob - */ - protected static final long BLOB = 100; - protected static final long BLOB_PER_BYTE = 1; - - protected static final long GET = 30; - - protected static final long KEYWORD = 50; - - protected static final long SYMBOL = 50; - - public static final long TRANSFER = 100; - - public static final long SIMPLE_MACRO = 200; - - /** - * Juice for a recur form - * - * Fairly cheap, might have to construct some temp structures for recur - * arguments. - */ - public static final long RECUR = 30; - - /** - * Juice for a contract deployment - * - * Make this quite expensive, mainly to deter lots of willy-nilly deploying - */ - public static final long DEPLOY_CONTRACT = 1000; - - /** - * Probably should be expensive? - */ - protected static final long EVAL = 500; - - // Juice amounts for compiler. TODO: figure out if compile / eval should be - // allowed on-chain - - /** - * Juice cost to compile a Constant value - */ - public static final long COMPILE_CONSTANT = 30; - - /** - * Juice cost to compile a Constant value - */ - public static final long COMPILE_LOOKUP = 50; - - /** - * Juice cost to compile a general AST node - */ - public static final long COMPILE_NODE = 200; - - /** - * Juice cost to expand a constant - */ - public static final long EXPAND_CONSTANT = 40; - - /** - * Juice cost to expand a sequence - */ - public static final long EXPAND_SEQUENCE = 100; - - /** - * Juice cost to schedule - */ - public static final long SCHEDULE = 800; - - /** - * Default future schedule juice (10 per hour) - * - * This makes scheduling a few hours / days ahead cheap but year is quite - * expensive (~87,600). Also places an upper bound on advance schedules. - * - * TODO: review this - */ - public static final long SCHEDULE_MILLIS_PER_JUICE_UNIT = 360000; - - - /** - * Juice required to execute an exceptional return (return, halt, rollback etc.) - * - * Pretty cheap, one alloc and a bit of exceptional value handling. - */ - public static final long RETURN = 50; - - /** - * Juice cost for accepting an offer of Convex coins. - * - * We make this a little expensive because it involves updating two separate accounts. - */ - public static final long ACCEPT = 200; - - /** - * Juice cost for constructing a Syntax Object. Fairly lightweight. - */ - public static final long SYNTAX = Juice.SIMPLE_FN; - - /** - * Juice cost for extracting metadata from a Syntax object. - */ - public static final long META = Juice.CHEAP_OP; - - /** - * Juice cost for an 'assoc' - */ - public static final long ASSOC = Juice.BUILD_DATA+Juice.BUILD_PER_ELEMENT*2; - - /** - * Variable Juice cost for set comparison - */ - public static final long SET_COMPARE_PER_ELEMENT = 10; - - public static final long CREATE_ACCOUNT = 100; - - public static final long QUERY = Juice.CHEAP_OP; - - public static final long LOG = 100; - - public static final long SPECIAL = Juice.CHEAP_OP; - - public static final long SET_BANG = 20; - - /** - * Make this quite expensive. Discourage spamming Peer updates - */ - public static final long PEER_UPDATE = 1000; - - /** - * Saturating multiply and add: result = a + (b * c) - * - * Returns Long.MAX_VALUE on overflow. - * - * @param a First number (to be added) - * @param b Second number (to be multiplied) - * @param c Thirst number (to be multiplied) - * @return long result, capped at Long.MAX_VALUE - */ - public static final long addMul(long a, long b, long c) { - return add(a,mul(b,c)); - } - - /** - * Saturating multiply. Returns Long.MAX_VALUE on overflow. - * @param a First number - * @param b Second number - * @return long result, capped at Long.MAX_VALUE - */ - public static final long mul(long a, long b) { - if ((a<0)||(b<0)) return Long.MAX_VALUE; - if (Math.multiplyHigh(a, b)>0) return Long.MAX_VALUE; - return a*b; - } - - /** - * Saturating addition. Returns Long.MAX_VALUE on overflow. - * @param a First number - * @param b Second number - * @return long result, capped at Long.MAX_VALUE - */ - public static final long add(long a, long b) { - if ((a<0)||(b<0)) return Long.MAX_VALUE; - if ((a+b)<0) return Long.MAX_VALUE; - return a+b; - } - - -} diff --git a/convex-core/src/main/java/convex/core/lang/Ops.java b/convex-core/src/main/java/convex/core/lang/Ops.java deleted file mode 100644 index 007c7105b..000000000 --- a/convex-core/src/main/java/convex/core/lang/Ops.java +++ /dev/null @@ -1,94 +0,0 @@ -package convex.core.lang; - -import java.nio.ByteBuffer; - -import convex.core.data.ACell; -import convex.core.exceptions.BadFormatException; -import convex.core.lang.ops.Cond; -import convex.core.lang.ops.Constant; -import convex.core.lang.ops.Def; -import convex.core.lang.ops.Do; -import convex.core.lang.ops.Invoke; -import convex.core.lang.ops.Lambda; -import convex.core.lang.ops.Let; -import convex.core.lang.ops.Local; -import convex.core.lang.ops.Lookup; -import convex.core.lang.ops.Query; -import convex.core.lang.ops.Special; -import convex.core.util.Utils; - -/** - * Static utility class for coded operations. - * - * Ops are the fundamental units of code (e.g. as used to implement Actors), and may be - * effectively considered as "bytecode" for the decentralised state machine. - */ -public class Ops { - public static final byte CONSTANT = 1; - public static final byte INVOKE = 2; - public static final byte DO = 3; - public static final byte COND = 4; - public static final byte LOOKUP = 5; - public static final byte DEF = 6; - public static final byte LAMBDA = 7; - public static final byte LET = 8; - public static final byte QUERY = 9; - public static final byte LOOP = 10; - public static final byte LOCAL=11; - public static final byte SET = 12; - // public static final byte CALL = 9; - // public static final byte RETURN = 10; - - public static final byte SPECIAL_BASE = 64; - - - - /** - * Reads an Op from the given ByteBuffer. Assumes Message tag already read. - * - * @param The return type of the Op - * @param bb ByteBuffer - * @return Op read from ByteBuffer - * @throws BadFormatException If encoding is invalid - */ - @SuppressWarnings("unchecked") - public static AOp read(ByteBuffer bb) throws BadFormatException { - byte opCode = bb.get(); - switch (opCode) { - case Ops.INVOKE: - return Invoke.read(bb); - case Ops.COND: - return Cond.read(bb); - case Ops.CONSTANT: - return Constant.read(bb); - case Ops.DEF: - return Def.read(bb); - case Ops.DO: - return Do.read(bb); - case Ops.LOOKUP: - return Lookup.read(bb); - // case Ops.CALL: return Call.read(bb); - case Ops.LAMBDA: - return (AOp) Lambda.read(bb); - case Ops.LET: - return Let.read(bb,false); - case Ops.QUERY: - return Query.read(bb); - case Ops.LOOP: - return Let.read(bb,true); - case Ops.LOCAL: - return Local.read(bb); - - // case Ops.RETURN: return (AOp) Return.read(bb); - default: - // range 64-127 is special ops - if ((opCode&0xC0) == 0x40) { - Special special=(Special) Special.create(opCode); - if (special==null) throw new BadFormatException("Bad OpCode for Special value: "+Utils.toHexString((byte)opCode)); - return special; - } - - throw new BadFormatException("Invalide OpCode: " + opCode); - } - } -} diff --git a/convex-core/src/main/java/convex/core/lang/RT.java b/convex-core/src/main/java/convex/core/lang/RT.java deleted file mode 100644 index 64fd87824..000000000 --- a/convex-core/src/main/java/convex/core/lang/RT.java +++ /dev/null @@ -1,1381 +0,0 @@ -package convex.core.lang; - -import java.lang.reflect.Array; -import java.util.function.BiFunction; - -import convex.core.Constants; -import convex.core.data.ABlob; -import convex.core.data.ACell; -import convex.core.data.ACollection; -import convex.core.data.ACountable; -import convex.core.data.ADataStructure; -import convex.core.data.AHashMap; -import convex.core.data.AList; -import convex.core.data.AMap; -import convex.core.data.ASequence; -import convex.core.data.ASet; -import convex.core.data.AString; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.Address; -import convex.core.data.Blob; -import convex.core.data.Blobs; -import convex.core.data.Hash; -import convex.core.data.INumeric; -import convex.core.data.Keyword; -import convex.core.data.Lists; -import convex.core.data.MapEntry; -import convex.core.data.Maps; -import convex.core.data.Ref; -import convex.core.data.Sets; -import convex.core.data.Strings; -import convex.core.data.Symbol; -import convex.core.data.Vectors; -import convex.core.data.prim.APrimitive; -import convex.core.data.prim.CVMBool; -import convex.core.data.prim.CVMByte; -import convex.core.data.prim.CVMChar; -import convex.core.data.prim.CVMDouble; -import convex.core.data.prim.CVMLong; -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.impl.KeywordFn; -import convex.core.lang.impl.MapFn; -import convex.core.lang.impl.SeqFn; -import convex.core.lang.impl.SetFn; -import convex.core.util.Utils; - -/** - * Static utility class for Runtime functions. Mostly low-level support for Core - * language capabilities, which will be wrapped as functions in the initial - * execution environment. - * - * "Low-level programming is good for the programmer's soul." — John Carmack - */ -public class RT { - - /** - * Returns true if all elements in an array are equal. Nulls are equal to null - * only. - * - * @param Type of values - * @param values Array of values - * @return True if all values are equal - */ - public static Boolean allEqual(T[] values) { - for (int i = 0; i < values.length - 1; i++) { - if (!Utils.equals(values[i], values[i + 1])) return false; - } - return true; - } - - // Numerical comparison functions - - /** - * Check if the values passed are a short (length 0 or 1) array of numbers which - * is a special case for comparison operations. - * - * @return Boolean result, or null if the values are not comparable - */ - private static Boolean checkShortCompare(ACell[] values) { - int len = values.length; - if (len == 0) return true; - if (len == 1) { - if (null == RT.ensureNumber(values[0])) return null; // cast failure - return true; - } - return false; - } - - public static Boolean eq(ACell[] values) { - Boolean check = checkShortCompare(values); - if (check == null) return null; - if (check) return true; - for (int i = 0; i < values.length - 1; i++) { - Long comp = RT.compare(values[i], values[i + 1],Long.MAX_VALUE); - if (comp == null) return null; // cast error - if (comp != 0) return false; - } - return true; - } - - public static Boolean ge(ACell[] values) { - Boolean check = checkShortCompare(values); - if (check == null) return null; - if (check) return true; - for (int i = 0; i < values.length - 1; i++) { - Long comp = RT.compare(values[i], values[i + 1],Long.MIN_VALUE); - if (comp == null) return null; // cast error - if (comp < 0) return false; - } - return true; - } - - public static Boolean gt(ACell[] values) { - Boolean check = checkShortCompare(values); - if (check == null) return null; - if (check) return true; - for (int i = 0; i < values.length - 1; i++) { - Long comp = RT.compare(values[i], values[i + 1],Long.MIN_VALUE); - if (comp == null) return null; // cast error - if (comp <= 0) return false; - } - return true; - } - - public static Boolean le(ACell[] values) { - Boolean check = checkShortCompare(values); - if (check == null) return null; - if (check) return true; - for (int i = 0; i < values.length - 1; i++) { - Long comp = RT.compare(values[i], values[i + 1],Long.MAX_VALUE); - if (comp == null) return null; // cast error - if (comp > 0) return false; - } - return true; - } - - public static Boolean lt(ACell[] values) { - Boolean check = checkShortCompare(values); - if (check == null) return null; - if (check) return true; - for (int i = 0; i < values.length - 1; i++) { - Long comp = RT.compare(values[i], values[i + 1],Long.MAX_VALUE); - if (comp == null) return null; // cast error - if (comp >= 0) return false; - } - return true; - } - - /** - * Get the target common numeric type for a given set of arguments. - Integers - * upcast to Long - Anything else upcasts to Double - * - * @param args Argument array - * - * @return The target numeric type, or null if there is a non-numeric argument - */ - public static Class commonNumericType(ACell[] args) { - Class highestFound=Long.class; - for (int i = 0; i < args.length; i++) { - ACell a = args[i]; - Class klass = numericType(a); - if (klass == null) return null; // break if non-numeric - if (klass == Double.class) highestFound=Double.class; - } - return highestFound; - } - - /** - * Finds the first non-numeric value in an array. Used for error reporting. - * - * @param args Argument array - * @return First non-numeric value, or null if not found. - */ - public static int findNonNumeric(ACell[] args) { - for (int i = 0; i < args.length; i++) { - ACell a = args[i]; - Class klass = numericType(a); - if (klass == null) return i; - } - return -1; - } - - /** - * Gets the numeric class of an object - * - * @param a Numerical value - * @return Long.class or Double.class if cast possible, or null if not numeric. - */ - public static Class numericType(ACell a) { - if (a instanceof INumeric) { - return ((INumeric)a).numericType(); - } - return null; - } - - public static APrimitive plus(ACell[] args) { - Class type = commonNumericType(args); - if (type == null) return null; - if (type == Double.class) return plusDouble(args); - long result = 0; - for (int i = 0; i < args.length; i++) { - result += RT.longValue(args[i]); - } - return CVMLong.create(result); - } - - public static CVMDouble plusDouble(ACell[] args) { - double result = 0; - for (int i = 0; i < args.length; i++) { - result += RT.doubleValue(args[i]); - } - return CVMDouble.create(result); - } - - public static APrimitive minus(ACell[] args) { - Class type = commonNumericType(args); - if (type == null) return null; - if (type == Double.class) return minusDouble(args); - int n = args.length; - long result = longValue(args[0]); - if (n == 1) result= -result; - for (int i = 1; i < n; i++) { - result -= RT.longValue(args[i]); - } - return CVMLong.create(result); - } - - public static APrimitive minusDouble(ACell[] args) { - int n = args.length; - double result = doubleValue(args[0]); - if (n == 1) result= -result; - for (int i = 1; i < args.length; i++) { - result -= RT.doubleValue(args[i]); - } - return CVMDouble.create(result); - } - - public static APrimitive times(ACell[] args) { - Class type = commonNumericType(args); - if (type == null) return null; - if (type == Double.class) return timesDouble(args); - long result = 1; - for (int i = 0; i < args.length; i++) { - result *= RT.longValue(args[i]); - } - return CVMLong.create(result); - } - - public static APrimitive timesDouble(ACell[] args) { - double result = 1; - for (int i = 0; i < args.length; i++) { - result *= RT.doubleValue(args[i]); - } - return CVMDouble.create(result); - } - - public static CVMDouble divide(ACell[] args) { - int n = args.length; - CVMDouble arg0 = ensureDouble(args[0]); - if (arg0 == null) return null; - double result=arg0.doubleValue(); - - if (n == 1) return CVMDouble.create(1.0 / result); - for (int i = 1; i < args.length; i++) { - CVMDouble v = ensureDouble(args[i]); - if (v == null) return null; - result = result / v.doubleValue(); - } - return CVMDouble.create(result); - } - - /** - * Computes the result of a pow operation. Returns null if a cast fails. - * @param args Argument array, should be length 2 - * @return Result of exponentiation - */ - public static CVMDouble pow(ACell[] args) { - CVMDouble a = ensureDouble(args[0]); - CVMDouble b = ensureDouble(args[1]); - if ((a==null)||(b==null)) return null; - return CVMDouble.create(StrictMath.pow(a.doubleValue(), b.doubleValue())); - } - - /** - * Computes the result of a exp operation. Returns null if a cast fails. - * @param arg Numeric value - * @return Numeric result, or null - */ - public static CVMDouble exp(ACell arg) { - CVMDouble a = ensureDouble(arg); - if (a==null) return null; - return CVMDouble.create(StrictMath.exp(a.doubleValue())); - } - - /** - * Gets the floor a number after casting to a double. Equivalent to java.lang.StrictMath.floor(...) - * - * @param a Numerical Value - * @return The floor of the number, or null if cast fails - */ - public static CVMDouble floor(ACell a) { - CVMDouble d = RT.ensureDouble(a); - if (d == null) return null; - return CVMDouble.create(StrictMath.floor(d.doubleValue())); - } - - /** - * Gets the ceiling a number after casting to a double. Equivalent to java.lang.StrictMath.ceil(...) - * - * @param a Numerical Value - * @return The ceiling of the number, or null if cast fails - */ - public static CVMDouble ceil(ACell a) { - CVMDouble d = RT.ensureDouble(a); - if (d == null) return null; - return CVMDouble.create(StrictMath.ceil(d.doubleValue())); - } - - /** - * Gets the exact positive square root of a number after casting to a double. - * Returns NaN for negative numbers. - * - * @param a Numerical Value - * @return The square root of the number, or null if cast fails - */ - public static CVMDouble sqrt(ACell a) { - CVMDouble d = RT.ensureDouble(a); - if (d == null) return null; - return CVMDouble.create(StrictMath.sqrt(d.doubleValue())); - } - - /** - * Gets the absolute value of a numeric value. Supports double and long. - * - * @param a Numeric CVM value - * @return Absolute value, or null if not a numeric value - */ - public static APrimitive abs(ACell a) { - INumeric x=RT.ensureNumber(a); - if (x==null) return null; - if (x instanceof CVMLong) return CVMLong.create( Math.abs(((CVMLong) x).longValue())); - return CVMDouble.create(Math.abs(x.toDouble().doubleValue())); - } - - /** - * Gets the signum of a numeric value - * - * @param a Numeric value - * @return value of -1, 0 or 1, NaN is argument is NaN, or null if the argument is not numeric - */ - public static ACell signum(ACell a) { - INumeric x=RT.ensureNumber(a); - if (x==null) return null; - return x.signum(); - } - - /** - * Compares two objects representing numbers numerically. - * - * @param a First numeric value - * @param b Second numeric value - * @param nanValue Value to return in case of a NaN result - * @return Less than 0 if a is smaller, greater than 0 if a is larger, 0 if a - * equals b - */ - public static Long compare(ACell a, ACell b,Long nanValue) { - Class ca = numericType(a); - if (ca == null) return null; - Class cb = numericType(b); - if (cb == null) return null; - - if ((ca == Long.class) && (cb == Long.class)) return RT.compare(longValue(a), longValue(b)); - - double da=doubleValue(a); - double db=doubleValue(b); - if (da==db) return 0L; - if (dadb) return 1L; - - return nanValue; - } - - /** - * Compares two long values numerically, according to Java primitive - * comparisons. - * - * @param a First number - * @param b Second number - * @return -1 if a is less than b, 1 if greater, 0 is they are equal - */ - public static long compare(long a, long b) { - if (a < b) return -1; - if (a > b) return 1; - return 0; - } - - /** - * Converts a CVM value to the standard numeric representation. Result will be one of: - *
    - *
  • Long for Byte, Long
  • - *
  • Double for Double
  • - *
  • null for any non-numeric value
  • - *
- * - * @param a Value to convert to numeric representation - * @return The number value, or null if cannot be converted - */ - public static INumeric ensureNumber(ACell a) { - if (a == null) return null; - - if (a instanceof INumeric) { - return ((INumeric)a).toStandardNumber(); - } - - return null; - } - - /** - * Tests if a Value is a valid numerical value - * @param val Value to test - * @return True if a number, false otherwise - */ - public static boolean isNumber(ACell val) { - return (val instanceof INumeric); - } - - /** - * Increments a Long value - * @param x Value to increment - * @return Long Value, or null if conversion fails - */ - public static CVMLong inc(ACell x) { - CVMLong n = ensureLong(x); - if (n == null) return null; - return CVMLong.create(n.longValue() + 1L); - } - - /** - * Decrements a Long value - * @param x Value to decrement - * @return Long Value, or null if conversion fails - */ - public static CVMLong dec(ACell x) { - CVMLong n = ensureLong(x); - if (n == null) return null; - return CVMLong.create(n.longValue() - 1L); - } - - /** - * Converts a numerical value to a CVM Double. - * @param a Value to cast - * @return Double value, or null if not convertible - */ - public static CVMDouble castDouble(ACell a) { - if (a instanceof CVMDouble) return (CVMDouble) a; - - CVMLong l=castLong(a); - if (l==null) return null; - return l.toDouble(); - } - - /** - * Ensures the argument is a CVM Long value. - * @param a Value to cast - * @return CVMDouble value, or null if not convertible - */ - public static CVMDouble ensureDouble(ACell a) { - if (a instanceof INumeric) { - INumeric ap=(INumeric)a; - return ap.toDouble(); - } - return null; - } - - /** - * Converts a numerical value to a CVM Long. Doubles and floats will be converted if possible. - * @param a Value to cast - * @return Long value, or null if not convertible - */ - public static CVMLong castLong(ACell a) { - if (a instanceof CVMLong) return (CVMLong) a; - INumeric n = ensureNumber(a); - if (n != null) { - return n.toLong(); - }; - - if (a instanceof APrimitive) { - return CVMLong.create(((APrimitive)a).longValue()); - } - - if (a instanceof ABlob) { - long lv=((ABlob)a).toLong(); - return CVMLong.create(lv); - } - - return null; - } - - /** - * Ensures the argument is a CVM Long value. - * @param a Value to cast - * @return CVMLong value, or null if not convertible - */ - public static CVMLong ensureLong(ACell a) { - if (a instanceof CVMLong) return (CVMLong) a; - if (a instanceof INumeric) { - INumeric ap=(INumeric)a; - if (ap.numericType()==Long.class) return ap.toLong(); - } - return null; - } - - /** - * Explicitly converts a numerical value to a CVM Byte. - * - * Doubles and floats will be converted if possible. - * - * @param a Value to cast - * @return Long value, or null if not convertible - */ - public static CVMByte castByte(ACell a) { - if (a instanceof CVMByte) return (CVMByte) a; - CVMLong l=castLong(a); - if (l == null) return null; - return CVMByte.create((byte)l.longValue()); - } - - /** - * Casts a value to a Character - * @param a Value to cast - * @return CVMChar value, or null if cast fails - */ - public static CVMChar toCharacter(ACell a) { - if (a instanceof CVMChar) return (CVMChar) a; - CVMLong l=castLong(a); - if (l == null) return null; - return CVMChar.create(l.longValue()); - } - - private static long longValue(ACell a) { - if (a instanceof APrimitive) return ((APrimitive) a).longValue(); - throw new IllegalArgumentException("Can't convert to long: " + Utils.getClassName(a)); - } - - private static double doubleValue(ACell a) { - if (a instanceof APrimitive) return ((APrimitive) a).doubleValue(); - throw new IllegalArgumentException("Can't convert to double: " + Utils.getClassName(a)); - } - - /** - * Converts any data structure to a vector - * - * @param o Object to attemptto convert to a Vector - * @return AVector instance, or null if not convertible - */ - @SuppressWarnings("unchecked") - public static AVector vec(Object o) { - if (o==null) return Vectors.empty(); - if (o instanceof ACell) return castVector((ACell) o); - - if (o.getClass().isArray()) { - ACell[] arr = Utils.toCellArray(o); - return Vectors.create(arr); - } - - if (o instanceof java.util.List) return Vectors.create((java.util.List) o); - - return null; - } - - /** - * Converts any countable data structure to a vector. Might be O(n) - * - * @param o Value to convert - * @return AVector instance, or null if conversion fails - */ - @SuppressWarnings("unchecked") - public static AVector castVector(ACell o) { - if (o == null) return Vectors.empty(); - if (o instanceof ACollection) return vec((ACollection) o); - if (o instanceof ACountable) { - ACountable ds=(ACountable) o; - long n=ds.count(); - AVector r=Vectors.empty(); - for (int i=0; i ASet castSet(ACell o) { - if (o == null) return Sets.empty(); - if (o instanceof ASet) return (ASet) o; - if (o instanceof ADataStructure) return Sets.create((ADataStructure) o); - return null; - } - - /** - * Converts any collection to a vector. Always succeeds, but may have O(n) cost - * - * Null values are converted to empty vector (considered as empty sequence) - * @param coll Collection to convert to a Vector - * @return Vector instance - */ - public static AVector vec(ACollection coll) { - if (coll == null) return Vectors.empty(); - return coll.toVector(); - } - - - /** - * Converts any collection of cells into a Sequence data structure. - * - * Potentially O(n) in size of collection. - * - * Nulls are converted to an empty vector. - * - * Returns null if conversion is not possible. - * - * @param Type of cell in collection - * @param o An object that contains a collection of cells - * @return An ASequence instance, or null if the argument cannot be converted to - * a sequence - */ - @SuppressWarnings("unchecked") - public static ASequence sequence(ACell o) { - if (o == null) return Vectors.empty(); - if (o instanceof ASequence) return (ASequence) o; - if (o instanceof ACollection) return ((ACollection) o).toVector(); - if (o instanceof AMap) { - // TODO: probably needs fixing! SECURITY - return sequence(((AMap) o).entryVector()); - } - - return null; - } - - /** - * Ensures argument is a sequence data structure. - * - * Nulls are converted to an empty vector. - * - * Returns null if conversion is not possible. - * - * @param Type of sequence elements - * @param o Value to cast to sequence - * @return An ASequence instance, or null if the argument cannot be converted to - * a sequence - */ - @SuppressWarnings("unchecked") - public static ASequence ensureSequence(ACell o) { - if (o == null) return Vectors.empty(); - if (o instanceof ASequence) return (ASequence) o; - return null; - } - - /** - * Gets the nth element from a sequential collection. - * - * Throws an exception if access is out of bounds - caller responsibility to check bounds first - * - * @param Type of element in collection - * @param o Countable Value - * @param i Index of element to get - * @return Element from collection at the specified position - */ - @SuppressWarnings("unchecked") - public static T nth(ACell o, long i) { - // special case, we treat nil as empty sequence - if (o == null) throw new IndexOutOfBoundsException("Can't get nth element from null"); - - if (o instanceof ACountable) return ((ACountable) o).get(i); // blobs, maps and collections - - throw new ClassCastException("Don't know how to get nth item of type "+RT.getType(o)); - } - - /** - * Variant of nth that also handles Java Arrays. Used for destructuring. - * - * @param Return type - * @param o Object to check for indexed element - * @param i Index to check - * @return Element at specified index - */ - @SuppressWarnings("unchecked") - public static T nth(Object o, long i) { - if (o instanceof ACell) return nth((ACell)o,i); - - try { - if (o.getClass().isArray()) { - return (T) Array.get(o, Utils.checkedInt(i)); - } - } catch (IllegalArgumentException e) { - // can come from checkedInt calls - throw new IndexOutOfBoundsException(e.getMessage()); - } - - throw new ClassCastException("Can't get nth element from object of class: " + Utils.getClassName(o)); - } - - /** - * Gets the count of elements in a collection or Java array. Null is considered an empty collection. - * - * @param o An Object representing a collection of items to be counted - * @return The count of elements in the collection, or null if not countable - */ - public static Long count(Object o) { - if (o == null) return 0L; - if (o instanceof ACell) return count((ACell)o); - if (o.getClass().isArray()) { - return (long) Array.getLength(o); - } - return null; - } - - /** - * Gets the count of elements in a countable data structure. Null is considered an empty collection. - * - * @param a Any Cell potentially representing a collection of items to be counted - * @return The count of elements in the collection, or null if not countable - */ - public static Long count(ACell a) { - if (a == null) return 0L; - if (a instanceof ACountable) return ((ACountable) a).count(); - return null; - } - - - - /** - * Converts arguments to an AString representation. Handles: - *
    - *
  • CVM Strings (unchanged)
  • - *
  • Blobs (converted to hex)
  • - *
  • Numbers (converted to canonical numeric representation)
  • - *
  • Other Objects (printed in canonical format) - *
- * - * @param args Values to convert to String - * @return AString value - */ - public static AString str(ACell[] args) { - // TODO: execution cost limits?? - StringBuilder sb = new StringBuilder(); - for (ACell o : args) { - String s=RT.str(o); - sb.append(s); - } - return Strings.create(sb.toString()); - } - - /** - * Converts a value to a CVM String representation. Required to work for all valid - * types. - * - * @param a Value to convert to a String - * @return String representation of object - */ - public static String str(ACell a) { - if (a == null) return "nil"; - if (a instanceof Blob) return ((Blob)a).toHexString(); - String s = a.toString(); - return s; - } - - /** - * Gets the name from a CVM value. Supports Strings, Keywords and Symbols. - * - * @param a Value to cast to a name - * @return Name of the argument, or null if not Named - */ - public static AString name(ACell a) { - if (a instanceof AString) return (AString) a; - if (a instanceof Keyword) return Strings.create(((Keyword) a).getName()); - if (a instanceof Symbol) return Strings.create(((Symbol) a).getName()); - return null; - } - - /** - * Prepends an element to a sequential data structure. The new element will - * always be in position 0 - * - * @param Type of elements - * @param x Element to prepend - * @param xs Any sequential object, or null (will be treated as empty sequence) - * @return A new list with the cons'ed element at the start - */ - @SuppressWarnings("unchecked") - public static AList cons(T x, ASequence xs) { - if (xs == null) return Lists.of(x); - return ((ASequence) xs).cons(x); - } - - /** - * Prepends two elements to a sequential data structure. The new elements will - * always be in position 0 and 1 - * - * @param Type of elements - * @param x Element to prepend at position 0 - * @param y Element to prepend at position 1 - * @param xs Any sequential object, or null (will be treated as empty sequence) - * @return A new list with the cons'ed elements at the start - */ - public static AList cons(T x, T y, ACell xs) { - ASequence nxs = RT.sequence(xs); - if (xs == null) return Lists.of(x, y); - return nxs.cons(y).cons(x); - } - - /** - * Prepends three elements to a sequential data structure. The new elements will - * always be in position 0, 1 and 2 - * - * @param Type of elements - * @param x Element to prepend at position 0 - * @param y Element to prepend at position 1 - * @param z Element to prepend at position 2 - * @param xs Any sequential object - * @return A new list with the cons'ed elements at the start - */ - public static AList cons(T x, T y, T z, ACell xs) { - ASequence nxs = RT.sequence(xs); - return nxs.cons(y).cons(x).cons(z); - } - - /** - * Coerces any object to a collection type, or returns null if not possible. - * Null is converted to an empty vector. - * - * @param a value to coerce to collection type. - * @return Collection object, or null if coercion failed. - */ - @SuppressWarnings("unchecked") - static ACollection collection(ACell a) { - if (a == null) return Vectors.empty(); - if (a instanceof ACollection) return (ACollection) a; - return null; - } - - /** - * Coerces any object to a data structure type, or returns null if not possible. - * Null is converted to an empty vector. - * - * @param a value to coerce to collection type. - * @return Collection object, or null if coercion failed. - */ - @SuppressWarnings("unchecked") - static ADataStructure castDataStructure(ACell a) { - if (a == null) return Vectors.empty(); - if (a instanceof ADataStructure) return (ADataStructure) a; - return null; - } - - /** - * Coerces an argument to a function interface. Certain values e.g. Keywords can - * be used / applied in function position. - * - * @param Function return type - * @param a Value to cast to a function - * @return AFn instance, or null if the argument cannot be coerced to a - * function. - * - */ - @SuppressWarnings("unchecked") - public static AFn castFunction(ACell a) { - if (a instanceof AFn) return (AFn) a; - if (a instanceof AMap) return MapFn.wrap((AMap) a); - if (a instanceof ASequence) return SeqFn.wrap((ASequence) a); - if (a instanceof ASet) return (AFn) SetFn.wrap((ASet) a); - if (a instanceof Keyword) return KeywordFn.wrap((Keyword) a); - return null; - } - - /** - * Ensure the argument is a valid CVM function. Returns null otherwise. - * - * @param Function return type - * @param a Value to cast to a function - * @return IFn instance, or null if the argument cannot be coerced to a - * function. - * - */ - @SuppressWarnings("unchecked") - public static AFn ensureFunction(ACell a) { - if (a instanceof AFn) return (AFn) a; - return null; - } - - /** - * Casts the argument to a valid Address. - * - * Handles: - *
    - *
  • Strings, which are interpreted as 16-character hex strings
  • - *
  • Addresses, which are returned unchanged
  • - *
  • Blobs, which are converted to addresses if and only if they are of the correct length (8 bytes)
  • - *
  • Numeric Longs, which are converted to the equivalent Address
  • - *
- * - * @param a Value to cast to an Address - * @return Address value or null if not castable to a valid address - */ - public static Address castAddress(ACell a) { - if (a instanceof Address) return (Address) a; - if (a instanceof ABlob) return Address.create((ABlob)a); - if (a instanceof AString) return Address.fromHex(a.toString()); - CVMLong value=RT.ensureLong(a); - if (value==null) return null; - return Address.create(value.longValue()); - } - - /** - * Casts an arbitrary value to an Address - * @param a Value to cast. Strings or CVM values accepted - * @return Address instance, or null if not convertible - */ - public static Address castAddress(Object a) { - if (a instanceof ACell) return castAddress((ACell)a); - if (a instanceof String) return Address.parse((String)a); - return null; - } - - /** - * Ensures the argument is a valid Address. - * - * @param a Value to cast - * @return Address value or null if not a valid address - */ - public static Address ensureAddress(ACell a) { - if (a instanceof Address) return (Address) a; - return null; - } - - /** - * Implicit cast to an AccountKey. Accepts blobs of correct length - * @param a Value to cast - * @return AccountKey instance, or null if coercion fails - */ - public static AccountKey ensureAccountKey(ACell a) { - if (a==null) return null; - if (a instanceof AccountKey) return (AccountKey) a; - if (a instanceof ABlob) { - ABlob b = (ABlob) a; - return AccountKey.create(b); - } - - return null; - } - - /** - * Coerce to an AccountKey. Accepts strings and blobs of correct length - * @param a Value to cast - * @return AccountKey instance, or null if coercion fails - */ - public static AccountKey castAccountKey(ACell a) { - if (a==null) return null; - if (a instanceof AString) return AccountKey.fromHexOrNull((AString)a); - return ensureAccountKey(a); - } - - /** - * Converts an object to a canonical blob representation. Handles blobs, - * addresses, hashes and hex strings - * - * @param a Object to convert to a Blob - * @return Blob value, or null if not convertable to a blob - */ - public static ABlob castBlob(ACell a) { - // handle address, hash, blob instances - if (a instanceof ABlob) return Blobs.toCanonical((ABlob) a); - if (a instanceof AString) return Blobs.fromHex(a.toString()); - return null; - } - - /** - * Converts the argument to a non-null Map. Nulls are implicitly converted to the empty - * map. - * - * @param Type of map keys - * @param Type of map values - * @param a Value to cast - * @return Map instance, or null if argument cannot be converted to a map - * - */ - @SuppressWarnings("unchecked") - public static AMap ensureMap(ACell a) { - if (a == null) return Maps.empty(); - if (a instanceof AMap) return (AMap) a; - return null; - } - - /** - * Gets an element from a data structure using the given key. - * - * @param coll Collection to query - * @param key Key to look up in collection - * @return Value from collection with the specified key, or null if not found. - */ - public static ACell get(ADataStructure coll, ACell key) { - if (coll == null) return null; - return coll.get(key); - } - - /** - * Gets an element from a data structure using the given key. Returns the - * notFound parameter if the data structure does not have the specified key - * - * @param coll Collection to query - * @param key Key to look up in collection - * @param notFound Value to return if the lookup failed - * @return Value from collection with the specified key, or notFound argument - * if not found. - */ - public static ACell get(ADataStructure coll, ACell key, ACell notFound) { - if (coll == null) return notFound; - return coll.get(key,notFound); - } - - /** - * Converts any CVM value to a boolean value. An value is considered falsey if null - * or equal to CVMBool.FALSE, truthy otherwise - * - * @param a Object to convert to boolean value - * @return true if object is truthy, false otherwise - */ - public static boolean bool(ACell a) { - return !((a == null) || (a == CVMBool.FALSE)); - } - - /** - * Converts an object to a map entry. Handles MapEntries and length 2 vectors. - * - * @param Type of map key - * @param Type of map value - * @param x Value to cast - * @return MapEntry instance, or null if conversion fails - */ - @SuppressWarnings("unchecked") - public static MapEntry ensureMapEntry(ACell x) { - MapEntry me; - if (x instanceof MapEntry) { - me = (MapEntry) x; - } else if (x instanceof AVector) { - AVector v = (AVector) x; - if (v.count() != 2) return null; - me = MapEntry.createRef(v.getRef(0), v.getRef(1)); - } else { - return null; - } - return me; - } - - /** - * Coerces to Hash type. Converts blobs of correct length. - * - * @param o Value to cast - * @return Hash instance, or null if conversion not possible - */ - public static Hash ensureHash(ACell o) { - if (o instanceof Hash) return ((Hash) o); - if (o instanceof ABlob) { - ABlob blob=(ABlob)o; - if (blob.count()!=Hash.LENGTH) return null; - return Hash.wrap(blob.getBytes()); - } - - return null; - } - - /** - * Coerces an named argument to a keyword. - * - * @param a Value to cast - * @return Keyword if correctly constructed, or null if a failure occurs - */ - public static Keyword castKeyword(ACell a) { - if (a instanceof Keyword) return (Keyword) a; - AString name = name(a); - if (name == null) return null; - Keyword k = Keyword.create(name); - return k; - } - - /** - * Coerces an named argument to a Symbol. - * - * @param a Value to cast - * @return Symbol if correctly constructed, or null if a failure occurs - */ - public static Symbol ensureSymbol(ACell a) { - if (a instanceof Symbol) return (Symbol) a; - return null; - } - - - - /** - * Casts to an ADataStructure instance - * - * @param Type of data structure element - * @param a Value to cast - * @return ADataStructure instance, or null if not a data structure - */ - @SuppressWarnings("unchecked") - public static ADataStructure ensureDataStructure(ACell a) { - if (a instanceof ADataStructure) return (ADataStructure) a; - return null; - } - - /** - * Casts to an ACountable instance - * - * @param Type of countable element - * @param a Value to cast - * @return ADataStructure instance, or null if not a data structure - */ - @SuppressWarnings("unchecked") - public static ACountable ensureCountable(ACell a) { - if (a instanceof ACountable) return (ACountable) a; - return null; - } - - /** - * Tests if a value is one of the canonical boolean values 'true' or 'false' - * - * @param value Value to test - * @return True if the value is a canonical boolean value. - */ - public static boolean isBoolean(ACell value) { - return (value == CVMBool.TRUE) || (value == CVMBool.FALSE); - } - - /** - * Concatenates two sequences. Ignores nulls. - * - * @param a First sequence. Will be used to determine the type of the result if - * not null. - * @param b Second sequence. Will be the result if the first parameter is null. - * @return Concatenated Sequence - */ - @SuppressWarnings({ "rawtypes", "unchecked" }) - public static ASequence concat(ASequence a, ASequence b) { - if (a == null) return b; - if (b == null) return a; - return a.concat((ASequence) b); - } - - /** - * Validates an object. Might be a Cell or Ref - * - * @param o Object to validate - * @throws InvalidDataException For any validation failure - */ - public static void validate(Object o) throws InvalidDataException { - if (o==null) return; - if (o instanceof ACell) { - ((ACell) o).validate(); - } else if (o instanceof Ref) { - ((Ref) o).validate(); - } else { - throw new InvalidDataException("Data of class" + Utils.getClass(o) - + " neither IValidated, canonical nor embedded: ", o); - } - - } - - /** - * Validate a Cell. - * - * @param o Object to validate - * @throws InvalidDataException For any validation failure - */ - public static void validateCell(ACell o) throws InvalidDataException { - if (o==null) return; - if (o instanceof ACell) { - ((ACell) o).validateCell(); - } - } - - /** - * Associates a key with a given value in an associative data structure - * - * @param coll Any associative data structure - * @param key Key to update or add - * @param value Value to associate with key - * @return Updated data structure, or null if cast fails - */ - @SuppressWarnings("unchecked") - public static ADataStructure assoc(ADataStructure coll, ACell key, ACell value) { - if (coll == null) return (ADataStructure) Maps.create(key, value); - return coll.assoc(key, value); - } - - /** - * Returns the vector of keys of a map, or null if the object is not a map - * - * @param a Value to extract keys from (i.e. a Map) - * @return Vector of keys in the map - */ - @SuppressWarnings("unchecked") - public static AVector keys(ACell a) { - if (!(a instanceof AMap)) return null; - AMap m = (AMap) a; - return m.reduceEntries(new BiFunction<>() { - @Override - public AVector apply(AVector t, MapEntry u) { - return t.conj(u.getKey()); - } - }, Vectors.empty()); - } - - /** - * Returns the vector of values of a map, or null if the object is not a map - * - * @param a Value to extract values from (i.e. a Map) - * @return Vector of values from a map, or null if the object is not a map - */ - @SuppressWarnings("unchecked") - public static AVector values(ACell a) { - if (!(a instanceof AMap)) return null; - AMap m = (AMap) a; - return m.reduceValues(new BiFunction, R, AVector>() { - @Override - public AVector apply(AVector t, R u) { - return t.conj(u); - } - }, Vectors.empty()); - } - - /** - * Ensures the argument is an IAssociative instance. A null argument is considered an empty map. - * - * @param o Value to cast - * @return IAssociative instance, or null if conversion is not possible - */ - public static ADataStructure ensureAssociative(ACell o) { - if (o==null) return Maps.empty(); - if (o instanceof ADataStructure) return (ADataStructure) o; - return null; - } - - /** - * Ensures the value is a set. null is converted to the empty set. - * - * Returns null if the argument is not a set. - * - * @param Type of set element - * @param a Value to cast - * @return A set instance, or null if the argument cannot be converted to a set - */ - @SuppressWarnings("unchecked") - public static ASet ensureSet(ACell a) { - if (a==null) return Sets.empty(); - if (!(a instanceof ASet)) return null; - return (ASet) a; - } - - /** - * Casts the argument to a hashmap. null is converted to the empty HashMap. - * @param Type of keys - * @param Type of values - * @param a Any object - * @return AHashMap instance, or null if not a hash map - */ - @SuppressWarnings("unchecked") - public static AHashMap ensureHashMap(ACell a) { - if (a==null) return Maps.empty(); - if (a instanceof AHashMap) return (AHashMap) a; - return null; - } - - /** - * Implicitly casts the argument to a Blob - * @param object Value to cast to Blob - * @return Blob instance, or null if cast fails - */ - public static ABlob ensureBlob(ACell object) { - if (object instanceof ABlob) return ((ABlob)object); - return null; - } - - /** - * Implicitly casts the argument to a CVM String - * - * @param a Value to cast to a String - * @return AString instance, or null if cast fails - */ - public static AString ensureString(ACell a) { - if (a instanceof AString) return ((AString)a); - return null; - } - - public static boolean isValidAmount(long amount) { - return ((amount>=0)&&(amount T cvm(Object o) { - if (o==null) return null; - if (o instanceof ACell) return ((T)o); - if (o instanceof String) return (T) Strings.create((String)o); - if (o instanceof Double) return (T)CVMDouble.create(((Double)o)); - if (o instanceof Number) return (T)CVMLong.create(((Number)o).longValue()); - if (o instanceof Character) return (T)CVMChar.create((Character)o); - if (o instanceof Boolean) return (T)CVMBool.create((Boolean)o); - throw new IllegalArgumentException("Can't convert to CVM type with class: "+Utils.getClassName(o)); - } - - /** - * Converts a CVM value to equivalent JVM value - * @param o Value to convert to JVM type - * @return Java value, or unchanged input - */ - @SuppressWarnings("unchecked") - public static T jvm(ACell o) { - if (o instanceof AString) return (T) o.toString(); - if (o instanceof CVMLong) return (T)(Long)((CVMLong)o).longValue(); - if (o instanceof CVMDouble) return (T)(Double)((CVMDouble)o).doubleValue(); - if (o instanceof CVMByte) return (T)(Byte)(byte)((CVMByte)o).longValue(); - if (o instanceof CVMBool) return (T)(Boolean)((CVMBool)o).booleanValue(); - if (o instanceof CVMChar) return (T)(Character)((CVMChar)o).charValue(); - return (T)o; - } - - /** - * Compute mode. - * @param a First numeric argument (numerator) - * @param b First numeric argument (divisor) - * @return Numeric value or null if cast fails - */ - public static CVMLong mod(ACell a , ACell b) { - CVMLong la=RT.castLong(a); - if (la==null) return null; - - CVMLong lb=RT.castLong(b); - if (lb==null) return null; - - long num = la.longValue(); - long denom = lb.longValue(); - long result = num % denom; - if (result<0) result+=denom; - - return CVMLong.create(result); - } - - /** - * Get the runtime Type of any CVM value - * @param a Any CVM value - * @return Type of CVM value - */ - public static AType getType(ACell a) { - if (a==null) return Types.NIL; - return a.getType(); - } - - public static boolean isNaN(ACell val) { - return CVMDouble.NaN.equals(val); - } - - - - - -} diff --git a/convex-core/src/main/java/convex/core/lang/Reader.java b/convex-core/src/main/java/convex/core/lang/Reader.java deleted file mode 100644 index f68216dc0..000000000 --- a/convex-core/src/main/java/convex/core/lang/Reader.java +++ /dev/null @@ -1,78 +0,0 @@ -package convex.core.lang; - -import java.io.IOException; - -import convex.core.data.ACell; -import convex.core.data.AList; -import convex.core.data.Syntax; -import convex.core.lang.reader.AntlrReader; -import convex.core.util.Utils; - -/** - * Parboiled Parser implementation which reads source code and produces a tree - * of parsed objects. - * - * Supports reading in either raw form (ACell) mode or wrapping with Syntax Objects. The - * latter is required for source references etc. - * - * "Talk is cheap. Show me the code." - Linus Torvalds - */ -@SuppressWarnings("javadoc") -public class Reader { - /** - * Parses an expression and returns a Syntax object - * - * @param source - * @return Parsed form - */ - public static Syntax readSyntax(String source) { - return Syntax.create(read(source)); - } - - public static ACell readResource(String path) { - String source; - try { - source = Utils.readResourceAsString(path); - } catch (IOException e) { - throw Utils.sneakyThrow(e); - } - return read(source); - } - - public static ACell readResourceAsData(String path) throws IOException { - String source = Utils.readResourceAsString(path); - return read(source); - } - - /** - * Parses an expression list and returns a list of raw forms - * - * @param source - * @return List of Syntax Objects - */ - public static AList readAll(String source) { - return AntlrReader.readAll(source); - } - - /** - * Parses an expression and returns a form as an Object - * - * @param source - * @return Parsed form - */ - public static ACell read(java.io.Reader source) throws IOException { - return AntlrReader.read(source); - } - - /** - * Parses an expression and returns a form - * - * @param source - * @return Parsed form - */ - @SuppressWarnings("unchecked") - public static R read(String source) { - return (R) AntlrReader.read(source); - } - -} diff --git a/convex-core/src/main/java/convex/core/lang/Symbols.java b/convex-core/src/main/java/convex/core/lang/Symbols.java deleted file mode 100644 index 6efe8d9fc..000000000 --- a/convex-core/src/main/java/convex/core/lang/Symbols.java +++ /dev/null @@ -1,297 +0,0 @@ -package convex.core.lang; - -import convex.core.data.Symbol; - -/** - * Static class for Symbol constants. - * - * "If you have more things than names, your design is broken" - Stuart Halloway - */ -public class Symbols { - public static final Symbol COUNT = Symbol.create("count"); - public static final Symbol CONJ = Symbol.create("conj"); - public static final Symbol CONS = Symbol.create("cons"); - public static final Symbol GET = Symbol.create("get"); - public static final Symbol GET_IN = Symbol.create("get-in"); - public static final Symbol ASSOC = Symbol.create("assoc"); - public static final Symbol ASSOC_IN = Symbol.create("assoc-in"); - public static final Symbol DISSOC = Symbol.create("dissoc"); - public static final Symbol DISJ = Symbol.create("disj"); - public static final Symbol NTH = Symbol.create("nth"); - - public static final Symbol VECTOR = Symbol.create("vector"); - public static final Symbol VEC = Symbol.create("vec"); - public static final Symbol SET = Symbol.create("set"); - public static final Symbol HASH_MAP = Symbol.create("hash-map"); - public static final Symbol BLOB_MAP = Symbol.create("blob-map"); - public static final Symbol HASH_SET = Symbol.create("hash-set"); - public static final Symbol LIST = Symbol.create("list"); - public static final Symbol EMPTY = Symbol.create("empty"); - public static final Symbol INTO = Symbol.create("into"); - - public static final Symbol CONCAT = Symbol.create("concat"); - public static final Symbol MAP = Symbol.create("map"); - public static final Symbol REDUCE = Symbol.create("reduce"); - - public static final Symbol KEYS = Symbol.create("keys"); - public static final Symbol VALUES = Symbol.create("values"); - - public static final Symbol STR = Symbol.create("str"); - public static final Symbol KEYWORD = Symbol.create("keyword"); - public static final Symbol SYMBOL = Symbol.create("symbol"); - public static final Symbol NAME = Symbol.create("name"); - - public static final Symbol EQUALS = Symbol.create("="); - - public static final Symbol EQ = Symbol.create("=="); - public static final Symbol LT = Symbol.create("<"); - public static final Symbol GT = Symbol.create(">"); - public static final Symbol LE = Symbol.create("<="); - public static final Symbol GE = Symbol.create(">="); - - public static final Symbol NOT = Symbol.create("not"); - public static final Symbol OR = Symbol.create("or"); - public static final Symbol AND = Symbol.create("and"); - - public static final Symbol ASSERT = Symbol.create("assert"); - public static final Symbol FAIL = Symbol.create("fail"); - public static final Symbol TRY = Symbol.create("try"); - public static final Symbol CATCH = Symbol.create("catch"); - - public static final Symbol APPLY = Symbol.create("apply"); - public static final Symbol HASH = Symbol.create("hash"); - - public static final Symbol QUOTE = Symbol.create("quote"); - public static final Symbol QUASIQUOTE = Symbol.create("quasiquote"); - - public static final Symbol SYNTAX_QUOTE = Symbol.create("syntax-quote"); - public static final Symbol UNQUOTE = Symbol.create("unquote"); - public static final Symbol UNQUOTE_SPLICING = Symbol.create("unquote-splicing"); - - public static final Symbol DO = Symbol.create("do"); - public static final Symbol COND = Symbol.create("cond"); - public static final Symbol DEF = Symbol.create("def"); - public static final Symbol UNDEF = Symbol.create("undef"); - public static final Symbol UNDEF_STAR = Symbol.create("undef*"); - - public static final Symbol FN = Symbol.create("fn"); - public static final Symbol MACRO = Symbol.create("macro"); - public static final Symbol EXPANDER = Symbol.create("expander"); - - public static final Symbol IF = Symbol.create("if"); - public static final Symbol WHEN = Symbol.create("when"); - public static final Symbol LET = Symbol.create("let"); - - public static final Symbol STORE = Symbol.create("store"); - public static final Symbol FETCH = Symbol.create("fetch"); - - public static final Symbol ADDRESS = Symbol.create("address"); - public static final Symbol BALANCE = Symbol.create("balance"); - public static final Symbol TRANSFER = Symbol.create("transfer"); - public static final Symbol ACCEPT = Symbol.create("accept"); - public static final Symbol ACCOUNT = Symbol.create("account"); - - public static final Symbol ACCOUNT_Q = Symbol.create("account?"); - - public static final Symbol STAKE = Symbol.create("stake"); - public static final Symbol CREATE_PEER = Symbol.create("create-peer"); - public static final Symbol SET_PEER_DATA = Symbol.create("set-peer-data"); - public static final Symbol SET_PEER_STAKE = Symbol.create("set-peer-stake"); - - public static final Symbol CALL = Symbol.create("call"); - public static final Symbol CALL_STAR = Symbol.create("call*"); - - public static final Symbol HALT = Symbol.create("halt"); - public static final Symbol ROLLBACK = Symbol.create("rollback"); - - public static final Symbol CALLABLE_Q = Symbol.create("callable?"); - public static final Symbol DEPLOY = Symbol.create("deploy"); - public static final Symbol DEPLOY_ONCE = Symbol.create("deploy-once"); - - public static final Symbol BLOB = Symbol.create("blob"); - - public static final Symbol INC = Symbol.create("inc"); - public static final Symbol DEC = Symbol.create("dec"); - - public static final Symbol LONG = Symbol.create("long"); - public static final Symbol BYTE = Symbol.create("byte"); - public static final Symbol CHAR = Symbol.create("char"); - - public static final Symbol DOUBLE = Symbol.create("double"); - - public static final Symbol BOOLEAN = Symbol.create("boolean"); - public static final Symbol BOOLEAN_Q = Symbol.create("boolean?"); - - public static final Symbol PLUS = Symbol.create("+"); - public static final Symbol MINUS = Symbol.create("-"); - public static final Symbol TIMES = Symbol.create("*"); - public static final Symbol DIVIDE = Symbol.create("/"); - - public static final Symbol ABS = Symbol.create("abs"); - public static final Symbol SIGNUM = Symbol.create("signum"); - public static final Symbol SQRT = Symbol.create("sqrt"); - public static final Symbol EXP = Symbol.create("exp"); - public static final Symbol POW = Symbol.create("pow"); - public static final Symbol MOD = Symbol.create("mod"); - public static final Symbol QUOT = Symbol.create("quot"); - public static final Symbol REM = Symbol.create("rem"); - - - public static final Symbol FLOOR = Symbol.create("floor"); - public static final Symbol CEIL = Symbol.create("ceil"); - - public static final Symbol NAN = Symbol.create("NaN"); - - public static final Symbol LOOP = Symbol.create("loop"); - public static final Symbol RECUR = Symbol.create("recur"); - public static final Symbol TAILCALL_STAR = Symbol.create("tailcall*"); - - public static final Symbol RETURN = Symbol.create("return"); - public static final Symbol BREAK = Symbol.create("break"); - public static final Symbol REDUCED = Symbol.create("reduced"); - - - public static final char SPECIAL_STAR = '*'; - public static final Symbol STAR_ADDRESS = Symbol.create("*address*"); - public static final Symbol STAR_MEMORY = Symbol.create("*memory*"); - public static final Symbol STAR_CALLER = Symbol.create("*caller*"); - public static final Symbol STAR_ORIGIN = Symbol.create("*origin*"); - public static final Symbol STAR_JUICE = Symbol.create("*juice*"); - public static final Symbol STAR_BALANCE = Symbol.create("*balance*"); - public static final Symbol STAR_DEPTH = Symbol.create("*depth*"); - public static final Symbol STAR_RESULT = Symbol.create("*result*"); - public static final Symbol STAR_TIMESTAMP = Symbol.create("*timestamp*"); - public static final Symbol STAR_OFFER = Symbol.create("*offer*"); - public static final Symbol STAR_STATE = Symbol.create("*state*"); - public static final Symbol STAR_HOLDINGS = Symbol.create("*holdings*"); - public static final Symbol STAR_SEQUENCE = Symbol.create("*sequence*"); - public static final Symbol STAR_KEY = Symbol.create("*key*"); - - public static final Symbol STAR_LANG = Symbol.create("*lang*"); - - public static final Symbol STAR_INITIAL_EXPANDER = Symbol.create("*initial-expander*"); - - public static final Symbol HERO = Symbol.create("hero"); - - public static final Symbol COMPILE = Symbol.create("compile"); - public static final Symbol READ = Symbol.create("read"); - public static final Symbol EVAL = Symbol.create("eval"); - public static final Symbol EVAL_AS = Symbol.create("eval-as"); - - public static final Symbol QUERY = Symbol.create("query"); - public static final Symbol QUERY_AS = Symbol.create("query-as"); - - - public static final Symbol EXPAND = Symbol.create("expand"); - - public static final Symbol SCHEDULE = Symbol.create("schedule"); - public static final Symbol SCHEDULE_STAR = Symbol.create("schedule*"); - - public static final Symbol FIRST = Symbol.create("first"); - public static final Symbol SECOND = Symbol.create("second"); - public static final Symbol LAST = Symbol.create("last"); - public static final Symbol NEXT = Symbol.create("next"); - public static final Symbol REVERSE = Symbol.create("reverse"); - - public static final Symbol AMPERSAND = Symbol.create("&"); - public static final Symbol UNDERSCORE = Symbol.create("_"); - - public static final Symbol X = Symbol.create("x"); - public static final Symbol E = Symbol.create("e"); - - public static final Symbol NIL = Symbol.create("nil"); - - public static final Symbol NIL_Q = Symbol.create("nil?"); - public static final Symbol LIST_Q = Symbol.create("list?"); - public static final Symbol VECTOR_Q = Symbol.create("vector?"); - public static final Symbol SET_Q = Symbol.create("set?"); - public static final Symbol MAP_Q = Symbol.create("map?"); - - public static final Symbol COLL_Q = Symbol.create("coll?"); - public static final Symbol EMPTY_Q = Symbol.create("empty?"); - - public static final Symbol SYMBOL_Q = Symbol.create("symbol?"); - public static final Symbol KEYWORD_Q = Symbol.create("keyword?"); - public static final Symbol BLOB_Q = Symbol.create("blob?"); - public static final Symbol ADDRESS_Q = Symbol.create("address?"); - public static final Symbol LONG_Q = Symbol.create("long?"); - public static final Symbol STR_Q = Symbol.create("str?"); - public static final Symbol NUMBER_Q = Symbol.create("number?"); - public static final Symbol HASH_Q = Symbol.create("hash?"); - - public static final Symbol FN_Q = Symbol.create("fn?"); - public static final Symbol ACTOR_Q = Symbol.create("actor?"); - - public static final Symbol ZERO_Q = Symbol.create("zero?"); - - public static final Symbol CONTAINS_KEY_Q = Symbol.create("contains-key?"); - - public static final Symbol FOO = Symbol.create("foo"); - public static final Symbol BAR = Symbol.create("bar"); - public static final Symbol BAZ = Symbol.create("baz");; - - - public static final Symbol LOOKUP = Symbol.create("lookup"); - - // State global fields - public static final Symbol TIMESTAMP = Symbol.create("timestamp"); - public static final Symbol JUICE_PRICE = Symbol.create("juice-price"); - public static final Symbol FEES = Symbol.create("fees"); - - // source info - public static final Symbol START = Symbol.create("start"); - public static final Symbol END = Symbol.create("end"); - public static final Symbol SOURCE = Symbol.create("source"); - - public static final Symbol SYNTAX_Q = Symbol.create("syntax?"); - public static final Symbol SYNTAX = Symbol.create("syntax"); - public static final Symbol GET_META = Symbol.create("get-meta"); - public static final Symbol UNSYNTAX = Symbol.create("unsyntax"); - - public static final Symbol DOC = Symbol.create("doc"); - public static final Symbol META = Symbol.create("meta"); - public static final Symbol META_STAR = Symbol.create("*meta*"); - - public static final Symbol LOOKUP_META = Symbol.create("lookup-meta"); - - public static final Symbol GET_HOLDING = Symbol.create("get-holding"); - public static final Symbol SET_HOLDING = Symbol.create("set-holding"); - - public static final Symbol GET_CONTROLLER = Symbol.create("get-controller"); - public static final Symbol SET_CONTROLLER = Symbol.create("set-controller"); - public static final Symbol CHECK_TRUSTED_Q = Symbol.create("check-trusted?"); - - public static final Symbol SET_MEMORY = Symbol.create("set-memory"); - public static final Symbol TRANSFER_MEMORY = Symbol.create("transfer-memory"); - - public static final Symbol RECEIVE_ALLOWANCE = Symbol.create("receive-allowance"); - public static final Symbol RECEIVE_COIN = Symbol.create("receive-coin"); - public static final Symbol RECEIVE_ASSET = Symbol.create("receive-asset"); - - - public static final Symbol SET_BANG = Symbol.create("set!"); - public static final Symbol SET_STAR = Symbol.create("set*"); - - public static final Symbol REGISTER = Symbol.create("register"); - - public static final Symbol SUBSET_Q = Symbol.create("subset?"); - public static final Symbol UNION = Symbol.create("union"); - public static final Symbol INTERSECTION = Symbol.create("intersection"); - public static final Symbol DIFFERENCE = Symbol.create("difference"); - - public static final Symbol MERGE = Symbol.create("merge"); - - public static final Symbol ENCODING = Symbol.create("encoding"); - - public static final Symbol CREATE_ACCOUNT = Symbol.create("create-account"); - public static final Symbol SET_KEY = Symbol.create("set-key"); - - public static final Symbol LOG = Symbol.create("log"); - - public static final Symbol CNS_RESOLVE = Symbol.create("cns-resolve"); - - public static final Symbol NAN_Q = Symbol.create("nan?"); - - - -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/AClosure.java b/convex-core/src/main/java/convex/core/lang/impl/AClosure.java deleted file mode 100644 index 099dfdbca..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/AClosure.java +++ /dev/null @@ -1,35 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.lang.AFn; - -/** - * Abstract base class for functions that can close over a lexical environment. - * - * @param Return type of function - */ -public abstract class AClosure extends AFn { - /** - * Lexical environment saved for this closure - */ - protected final AVector lexicalEnv; - - protected AClosure(AVector lexicalEnv) { - this.lexicalEnv=lexicalEnv; - } - - /** - * Produces an copy of this closure with the specified environment - * - * @param env New lexical environment to use for this closure - * @return Closure updated with new lexical environment - */ - public abstract > F withEnvironment(AVector env); - - /** - * Print the "internal" representation of a closure e.g. "[x] 1", excluding the 'fn' symbol. - * @param sb StringBuilder to print to - */ - public abstract void printInternal(StringBuilder sb); -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/ADataFn.java b/convex-core/src/main/java/convex/core/lang/impl/ADataFn.java deleted file mode 100644 index f5439ed2e..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/ADataFn.java +++ /dev/null @@ -1,77 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.data.ACell; -import convex.core.data.IRefFunction; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.AFn; -import convex.core.lang.Context; - -/** - * Abstract base class for data structure lookup functions. - * - * Not a canonical object, can't exist as CVM value. - * - * @param Type of function return value - */ -public abstract class ADataFn extends AFn { - - @Override - public int estimatedEncodingSize() { - throw new UnsupportedOperationException(); - } - - @Override - public Context invoke(Context context, ACell[] args) { - throw new UnsupportedOperationException(); - } - - @Override - public AFn updateRefs(IRefFunction func) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean hasArity(int n) { - throw new UnsupportedOperationException(); - } - - @Override - public void validateCell() throws InvalidDataException { - throw new UnsupportedOperationException(); - } - - @Override - public int encode(byte[] bs, int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isCanonical() { - return false; - } - - @Override - public ACell toCanonical() { - throw new UnsupportedOperationException("Can't make canonical!"); - } - - @Override - public boolean isCVMValue() { - return false; - } - - @Override - public byte getTag() { - throw new UnsupportedOperationException(); - } - - @Override - public int getRefCount() { - throw new UnsupportedOperationException(); - } -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/AExceptional.java b/convex-core/src/main/java/convex/core/lang/impl/AExceptional.java deleted file mode 100644 index 207555044..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/AExceptional.java +++ /dev/null @@ -1,32 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.data.ACell; - -/** - * Abstract base class for exceptional return values. - * - * Java exceptions are expensive and don't make it easy to provide exactly the - * semantics we want so we return exceptional values in response to errors - * during on-chain execution. - * - * Notable uses: - Early return values from functions - Tail calls - Loop / - * recur - * - * "Do not fear to be eccentric in opinion, for every opinion now accepted was - * once eccentric." ― Bertrand Russell - */ -public abstract class AExceptional { - - /** - * Returns the Exception code for this exceptional value - * @return Exception Code - */ - public abstract ACell getCode(); - - /** - * Gets the message for an exceptional value. May or may not be meaningful. - * @return Exception Message - */ - public abstract ACell getMessage(); - -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/AReturn.java b/convex-core/src/main/java/convex/core/lang/impl/AReturn.java deleted file mode 100644 index 6916be607..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/AReturn.java +++ /dev/null @@ -1,8 +0,0 @@ -package convex.core.lang.impl; - -/** - * Abstract base class for exceptional returns - */ -public abstract class AReturn extends AExceptional { - -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/ATrampoline.java b/convex-core/src/main/java/convex/core/lang/impl/ATrampoline.java deleted file mode 100644 index 34746f90b..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/ATrampoline.java +++ /dev/null @@ -1,28 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.data.ACell; - -/** - * Abstract base class for trampolining function return values - */ -public abstract class ATrampoline extends AReturn { - - protected final ACell[] args; - - public ATrampoline(ACell[] values) { - this.args=values; - } - - public ACell getValue(int i) { - return args[i]; - } - - public ACell[] getValues() { - return args; - } - - public int arity() { - return args.length; - } - -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/CoreFn.java b/convex-core/src/main/java/convex/core/lang/impl/CoreFn.java deleted file mode 100644 index b59d86f0e..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/CoreFn.java +++ /dev/null @@ -1,128 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.data.ACell; -import convex.core.data.IRefFunction; -import convex.core.data.Ref; -import convex.core.data.Symbol; -import convex.core.data.Tag; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.AFn; -import convex.core.lang.Context; - -/** - * Abstract base class for core language functions implemented in the Runtime - * - * Core functions are tagged using their symbols in on-chain representation - * - * @param Type of function result - */ -public abstract class CoreFn extends AFn implements ICoreDef { - - private Symbol symbol; - private int arity; - private boolean variadic; - - protected CoreFn(Symbol symbol) { - this.symbol = symbol; - this.arity=0; - this.variadic=true; - } - - @Override - public abstract Context invoke(Context context, ACell[] args); - - public Symbol getSymbol() { - return symbol; - } - - public byte getTag() { - return Tag.CORE_DEF; - } - - protected String name() { - return symbol.getName().toString(); - } - - @Override - public boolean isCanonical() { - return true; - } - - @Override - public CoreFn toCanonical() { - return this; - } - - protected String minArityMessage(int minArity, int actual) { - return name() + " requires minimum arity " + minArity + " but called with: " + actual; - } - - protected String maxArityMessage(int maxArity, int actual) { - return name() + " requires maximum arity " + maxArity + " but called with: " + actual; - } - - protected String rangeArityMessage(int minArity, int maxArity, int actual) { - return name() + " requires arity between "+minArity+ " and " + maxArity + " but called with: " + actual; - } - - protected String exactArityMessage(int arity, int actual) { - return name() + " requires arity " + arity + " but called with: " + actual; - } - - @Override - public boolean hasArity(int n) { - if (n==arity) return true; - if (n Ref getRef(int i) { - throw new IndexOutOfBoundsException("Bad ref index: "+i); - } - - @Override - public CoreFn updateRefs(IRefFunction func) { - return this; - } - - @Override - public int estimatedEncodingSize() { - return 20; - } - - @Override - public void validateCell() throws InvalidDataException { - symbol.validateCell(); - } - - @Override - public boolean isEmbedded() { - // embed core functions, since they are the same size as small symbols - return true; - } - -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/CorePred.java b/convex-core/src/main/java/convex/core/lang/impl/CorePred.java deleted file mode 100644 index c7737d351..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/CorePred.java +++ /dev/null @@ -1,29 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.data.ACell; -import convex.core.data.Symbol; -import convex.core.data.prim.CVMBool; -import convex.core.lang.Context; -import convex.core.lang.Juice; - -/** - * Abstract base class for core predicate functions - */ -public abstract class CorePred extends CoreFn { - - protected CorePred(Symbol symbol) { - super(symbol); - } - - @SuppressWarnings("unchecked") - @Override - public Context invoke(@SuppressWarnings("rawtypes") Context context, ACell[] args) { - if (args.length != 1) return context.withArityError(name() + " requires exactly one argument"); - ACell val = args[0]; - // ensure we return one of the two canonical boolean values - CVMBool result = test(val) ? CVMBool.TRUE : CVMBool.FALSE; - return context.withResult(Juice.SIMPLE_FN, result); - } - - public abstract boolean test(ACell val); -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/ErrorValue.java b/convex-core/src/main/java/convex/core/lang/impl/ErrorValue.java deleted file mode 100644 index cca4f38a9..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/ErrorValue.java +++ /dev/null @@ -1,137 +0,0 @@ -package convex.core.lang.impl; - -import java.util.ArrayList; -import java.util.List; - -import convex.core.data.ACell; -import convex.core.data.AString; -import convex.core.data.Strings; - -/** - * Class representing an Error value produced by the CVM. - * - * See "Error Handling" CAD. - * - * Contains: - *
    - *
  • An immutable Error Code
  • - *
  • An immutable Error Message
  • - *
  • A mutable error trace (for information purposes outside the CVM)
  • - *
- * - * "Computers are useless. They can only give you answers." - * - Pablo Picasso - * - */ -public class ErrorValue extends AExceptional { - - private final ACell code; - private final ACell message; - private final ArrayList trace=new ArrayList<>(); - private ACell log; - - private ErrorValue(ACell code, ACell message) { - if (code==null) throw new IllegalArgumentException("Error code must not be null"); - this.code=code; - this.message=message; - } - - public static ErrorValue create(ACell code) { - return new ErrorValue(code,null); - } - - /** - * Creates an ErrorValue with the specified type and message. Message may be null. - * @param code Type of error - * @param message Off-chain message as CVM String - * @return New ErrorValue instance - */ - public static ErrorValue create(ACell code, AString message) { - return new ErrorValue(code,message); - } - - /** - * Creates an ErrorValue with the specified type and message. Message may be null. - * @param code Type of error - * @param message Off-chain message, must be valid CVM Value - * @return New ErrorValue instance - */ - public static ErrorValue createRaw(ACell code, ACell message) { - return new ErrorValue(code,message); - } - - /** - * Creates an ErrorValue with the specified type and message. Message may be null. - * @param code Code of error - * @param message Off-chain message as Java String - * @return New ErrorValue instance - */ - public static ErrorValue create(ACell code, String message) { - return new ErrorValue(code,Strings.create(message)); - } - - /** - * Gets the Error Code for this ErrorVAlue instance. The Error Code may be any value, but - * by convention (and exclusively in Convex runtime code) it is a upper-case keyword e.g. :ASSERT - * - * @return Error code value - */ - public ACell getCode() { - return code; - } - - public void addTrace(String traceMessage) { - trace.add(Strings.create(traceMessage)); - } - - public void addLog(ACell log) { - this.log=log; - } - - /** - * Gets the optional message associated with this error value, or null if not supplied. - * @return The message carried with this error - */ - public ACell getMessage() { - return message; - } - - @Override - public String toString() { - StringBuilder sb=new StringBuilder(); - sb.append("ErrorValue["+code+"]"+((message==null)?"":" : "+message)); - if (trace!=null) { - for (Object o:trace) { - sb.append("\n"); - sb.append(o.toString()); - } - } - - return sb.toString(); - } - - /** - * Gets the trace for this Error. - * - * The trace List is mutable, and may be used to implement accumulation of additional trace entries. - * - * @return List of trace entries. - */ - public List getTrace() { - return trace; - } - - /** - * Gets the CVM local log at the time of the Error. - * - * The trace List is mutable, and may be used to implement accumulation of additional trace entries. - * - * @return List of trace entries. - */ - public ACell getLog() { - return log; - } - - - -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/Fn.java b/convex-core/src/main/java/convex/core/lang/impl/Fn.java deleted file mode 100644 index 19049c3ca..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/Fn.java +++ /dev/null @@ -1,216 +0,0 @@ -package convex.core.lang.impl; - -import java.nio.ByteBuffer; - -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.Format; -import convex.core.data.IRefFunction; -import convex.core.data.Ref; -import convex.core.data.Tag; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.Symbols; -import convex.core.util.Utils; - -/** - * Value class representing a instantiated closure / lambda function. - * - * Includes the following information: - *
    - *
  1. Parameters of the function, as a vector of Syntax objects
  2. - *
  3. Body of the function, as a compiled operation
  4. - *
  5. captured lexical bindings at time of creation.
  6. - *
- * - * @param Return type of function - */ -public class Fn extends AClosure { - - // note: embedding these fields directly for efficiency rather than going by - // Refs. - - private final AVector params; - private final AOp body; - - private Long variadic=null; - - private Fn(AVector params, AOp body, AVector lexicalEnv) { - super(lexicalEnv); - this.params = params; - this.body = body; - } - - public static Fn create(AVector params, AOp body) { - AVector binds = Context.EMPTY_BINDINGS; - return new Fn(params, body, binds); - } - - @SuppressWarnings("unchecked") - @Override - public > F withEnvironment(AVector env) { - if (this.lexicalEnv==env) return (F) this; - return (F) new Fn(params, body, env); - } - - @Override - public boolean hasArity(int n) { - long var=checkVariadic(); - long pc=params.count(); - if (var>=0) return n>=(pc-2); // n must be at least number of params excluding [& more] - return (n==pc); - } - - /** - * Checks if the function is variadic. - * - * @return negative if non-variadic, index of variadic parameter if variadic - */ - private Long checkVariadic() { - if (variadic!=null) return variadic; - long pc=params.count(); - for (int i=0; i invoke(Context context, ACell[] args) { - // update local bindings for the duration of this function call - final AVector savedBindings = context.getLocalBindings(); - - // update to correct lexical environment, then bind function parameters - context = context.withLocalBindings(lexicalEnv); - - Context boundContext = context.updateBindings(params, args); - if (boundContext.isExceptional()) return boundContext.withLocalBindings(savedBindings); - - Context ctx = boundContext.execute(body); - - // return with restored bindings - return ctx.withLocalBindings(savedBindings); - } - - @Override - public boolean isCanonical() { - return true; - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.FN; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = params.encode(bs,pos); - pos = body.encode(bs,pos); - pos = lexicalEnv.encode(bs,pos); - return pos; - } - - @Override - public int estimatedEncodingSize() { - return 1+params.estimatedEncodingSize()+body.estimatedEncodingSize()+lexicalEnv.estimatedEncodingSize(); - } - - public static Fn read(ByteBuffer bb) throws BadFormatException { - try { - AVector params = Format.read(bb); - if (params==null) throw new BadFormatException("Null parameters to Fn"); - AOp body = Format.read(bb); - if (body==null) throw new BadFormatException("Null body in Fn"); - AVector lexicalEnv = Format.read(bb); - return new Fn<>(params, body, lexicalEnv); - } catch (ClassCastException e) { - throw new BadFormatException("Bad Fn format", e); - } - } - - @Override - public void print(StringBuilder sb) { - sb.append("(fn "); - printInternal(sb); - sb.append(')'); - } - - @Override - public void printInternal(StringBuilder sb) { - // Custom param printing, avoid printing Syntax metadata for now - sb.append('['); - long size = params.count(); - for (long i = 0; i < size; i++) { - if (i > 0) sb.append(' '); - Utils.print(sb,params.get(i)); - } - sb.append(']'); - - sb.append(' '); - body.print(sb); - } - - /** - * Returns the declared param names for a function. - * - * @return A binding vector describing the parameters for this function - */ - public AVector getParams() { - return params; - } - - public AOp getBody() { - return body; - } - - @Override - public int getRefCount() { - return params.getRefCount() + body.getRefCount() + lexicalEnv.getRefCount(); - } - - @Override - public Ref getRef(int i) { - int pc = params.getRefCount(); - if (i < pc) return params.getRef(i); - i -= pc; - int bc = body.getRefCount(); - if (i < bc) return body.getRef(i); - i -= bc; - return lexicalEnv.getRef(i); - } - - @Override - public Fn updateRefs(IRefFunction func) { - AVector newParams = params.updateRefs(func); - AOp newBody = body.updateRefs(func); - AVector newLexicalEnv = lexicalEnv.updateRefs(func); - if ((params == newParams) && (body == newBody) && (lexicalEnv == newLexicalEnv)) return this; - return new Fn<>(newParams, newBody, lexicalEnv); - } - - @Override - public void validateCell() throws InvalidDataException { - params.validateCell(); - body.validateCell(); - lexicalEnv.validateCell(); - } - - @Override - public ACell toCanonical() { - return this; - } - - - - - -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/HaltValue.java b/convex-core/src/main/java/convex/core/lang/impl/HaltValue.java deleted file mode 100644 index e428b60b5..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/HaltValue.java +++ /dev/null @@ -1,43 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.ErrorCodes; -import convex.core.data.ACell; - -/** - * Class representing a halt return value - * - * "Computers are useless. They can only give you answers." - Pablo Picasso - * - * @param Type of return value - */ -public class HaltValue extends AReturn { - - private final T value; - - public HaltValue(T value) { - this.value = value; - } - - public static HaltValue wrap(T value) { - return new HaltValue(value); - } - - public T getValue() { - return value; - } - - @Override - public String toString() { - return "HaltValue: " + value; - } - - @Override - public ACell getCode() { - return ErrorCodes.HALT; - } - - @Override - public ACell getMessage() { - return null; - } -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/ICoreDef.java b/convex-core/src/main/java/convex/core/lang/impl/ICoreDef.java deleted file mode 100644 index 9e3d38633..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/ICoreDef.java +++ /dev/null @@ -1,20 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.data.Symbol; - -/** - * Interface for objects that act as definitions in the core environment. - * - * These are serialised as symbolic references, and will be deserialised to - * point to the same core object. - */ -public interface ICoreDef { - - /** - * Defines the symbol for this core definition. - * - * @return The symbol for this core definition. - */ - public Symbol getSymbol(); - -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/KeywordFn.java b/convex-core/src/main/java/convex/core/lang/impl/KeywordFn.java deleted file mode 100644 index 2be864cfc..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/KeywordFn.java +++ /dev/null @@ -1,52 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.data.ACell; -import convex.core.data.ADataStructure; -import convex.core.data.Keyword; -import convex.core.data.type.Types; -import convex.core.lang.Context; -import convex.core.lang.RT; - -public class KeywordFn extends ADataFn { - private Keyword key; - - public KeywordFn(Keyword k) { - this.key = k; - } - - public static KeywordFn wrap(Keyword k) { - return new KeywordFn(k); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public Context invoke(Context context, ACell[] args) { - int n = args.length; - T result; - if (n == 1) { - ADataStructure gettable = RT.ensureAssociative(args[0]); - if (gettable == null) return context.withCastError(0, Types.DATA_STRUCTURE); - result = (T) gettable.get(key); - } else if (n == 2) { - ACell ds = args[0]; - ACell notFound = args[1]; - if (ds == null) { - result = (T) notFound; - } else { - ADataStructure gettable = RT.ensureAssociative(ds); - if (gettable == null) return context.withCastError(0, Types.DATA_STRUCTURE); - result = (T) RT.get(gettable, key, notFound); - } - } else { - return context.withArityError("Expected arity 1 or 2 for keyword lookup but got: " + n); - } - return context.withResult(result); - } - - @Override - public void print(StringBuilder sb) { - key.print(sb); - } - - -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/MapFn.java b/convex-core/src/main/java/convex/core/lang/impl/MapFn.java deleted file mode 100644 index 751ac88be..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/MapFn.java +++ /dev/null @@ -1,41 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.data.ACell; -import convex.core.data.AMap; -import convex.core.lang.Context; - -public class MapFn extends ADataFn { - - private AMap map; - - public MapFn(AMap m) { - this.map = m; - } - - public static MapFn wrap(AMap m) { - return new MapFn(m); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public Context invoke(Context context, ACell[] args) { - int n = args.length; - T result; - if (n == 1) { - ACell key = args[0]; - result = (T) map.get(key); - } else if (n == 2) { - K key = (K) args[0]; - result = (T) map.get(key, args[1]); - } else { - return context.withArityError("Expected arity 1 or 2 for map lookup but got: " + n); - } - return context.withResult(result); - } - - @Override - public void print(StringBuilder sb) { - map.print(sb); - } - -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/MultiFn.java b/convex-core/src/main/java/convex/core/lang/impl/MultiFn.java deleted file mode 100644 index cb845d3fc..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/MultiFn.java +++ /dev/null @@ -1,149 +0,0 @@ -package convex.core.lang.impl; - -import java.nio.BufferUnderflowException; -import java.nio.ByteBuffer; - -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.Format; -import convex.core.data.IRefFunction; -import convex.core.data.Ref; -import convex.core.data.Tag; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.AFn; -import convex.core.lang.Context; - -public class MultiFn extends AClosure { - - private final AVector> fns; - private final int num; - - private MultiFn(AVector> fns, AVector env) { - super(env); - this.fns=fns; - this.num=fns.size(); - } - - private MultiFn(AVector> fns) { - this(fns,Context.EMPTY_BINDINGS); - } - - - public static MultiFn create(AVector> fns) { - return new MultiFn<>(fns); - } - - @Override - public boolean isCanonical() { - return true; - } - - @Override - public MultiFn toCanonical() { - return this; - } - - @Override - public void print(StringBuilder sb) { - sb.append("(fn "); - printInternal(sb); - sb.append(')'); - } - - @Override - public void printInternal(StringBuilder sb) { - for (long i=0; i0) sb.append(' '); - sb.append('('); - fns.get(i).printInternal(sb); - sb.append(')'); - } - } - - @Override - public Context invoke(Context context, ACell[] args) { - for (int i=0; i fn=fns.get(i); - if (fn.supportsArgs(args)) { - return fn.invoke((Context) context, args); - } - } - // TODO: type specific message? - return context.withArityError("No matching function arity found for arity "+args.length); - } - - @Override - public boolean hasArity(int n) { - for (int i=0; i fn=fns.get(i); - if (fn.hasArity(n)) { - return true; - } - } - return false; - } - - @Override - public AFn updateRefs(IRefFunction func) { - AVector> newFns=fns.updateRefs(func); - if (fns==newFns) return this; - return new MultiFn(newFns); - } - - @Override - public void validateCell() throws InvalidDataException { - if (num<=0) throw new InvalidDataException("MultiFn must contain at least one function",this); - fns.validateCell(); - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.FN_MULTI; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = fns.encode(bs,pos); - return pos; - } - - public static MultiFn read(ByteBuffer bb) throws BadFormatException, BufferUnderflowException { - AVector> fns=Format.read(bb); - if (fns==null) throw new BadFormatException("Null fns!"); - return new MultiFn(fns); - } - - @Override - public int estimatedEncodingSize() { - return fns.estimatedEncodingSize()+1; - } - - @Override - public int getRefCount() { - return fns.getRefCount(); - } - - @Override - public Ref getRef(int i) { - return fns.getRef(i); - } - - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override - public > F withEnvironment(AVector env) { - // TODO: Can make environment update more efficient? - if (env==this.lexicalEnv) return (F) this; - return (F) new MultiFn(fns.map(fn->fn.withEnvironment(env)),env); - } - - - - - - - - -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/RecordFormat.java b/convex-core/src/main/java/convex/core/lang/impl/RecordFormat.java deleted file mode 100644 index e58b5d4ad..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/RecordFormat.java +++ /dev/null @@ -1,59 +0,0 @@ -package convex.core.lang.impl; - -import java.util.HashMap; -import java.util.Set; - -import convex.core.data.AVector; -import convex.core.data.Keyword; -import convex.core.data.Sets; -import convex.core.data.Vectors; - -public class RecordFormat { - - protected final long count; - protected final AVector keys; - protected final HashMap indexes = new HashMap<>(); - protected final Set keySet; - - private RecordFormat(AVector keys) { - this.keys = keys; - count = keys.count(); - for (int i = 0; i < count; i++) { - indexes.put(keys.get(i), Long.valueOf(i)); - } - keySet = Sets.create(keys); - } - - public long count() { - return count; - } - - public AVector getKeys() { - return keys; - } - - public boolean containsKey(Object key) { - return indexes.containsKey(key); - } - - public static RecordFormat of(Keyword... keys) { - return new RecordFormat(Vectors.create(keys)); - } - - public Set keySet() { - return keySet; - } - - public Long indexFor(Object key) { - return indexes.get(key); - } - - /** - * Gets the key at the specified index - * @param i Index of record key - * @return Keyword at the specified index - */ - public Keyword getKey(int i) { - return keys.get(i); - } -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/RecurValue.java b/convex-core/src/main/java/convex/core/lang/impl/RecurValue.java deleted file mode 100644 index 9d97ca577..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/RecurValue.java +++ /dev/null @@ -1,45 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.ErrorCodes; -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.Vectors; - -/** - * Class representing a function return value. - * - * Contains argument values for each parameter to be substituted in the - * surrounding function / loop - */ -public class RecurValue extends ATrampoline { - - private RecurValue(ACell[] values) { - super(values); - } - - /** - * Wraps an object array as a RecurValue - * - * @param values Values to recur with - * @return new RecurValue - */ - public static RecurValue wrap(ACell... values) { - return new RecurValue(values); - } - - @Override - public String toString() { - AVector seq = Vectors.create(args); // should always convert OK - return "RecurValue: " + seq; - } - - @Override - public ACell getCode() { - return ErrorCodes.RECUR; - } - - @Override - public ACell getMessage() { - return null; - } -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/Reduced.java b/convex-core/src/main/java/convex/core/lang/impl/Reduced.java deleted file mode 100644 index a88d343cb..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/Reduced.java +++ /dev/null @@ -1,32 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.ErrorCodes; -import convex.core.data.ACell; - -public class Reduced extends AReturn { - - private final ACell value; - - public Reduced(ACell value) { - this.value=value; - } - - public static Reduced wrap(ACell value) { - return new Reduced(value); - } - - @Override - public ACell getCode() { - return ErrorCodes.REDUCED; - } - - @Override - public ACell getMessage() { - return null; - } - - public ACell getValue() { - return value; - } - -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/ReturnValue.java b/convex-core/src/main/java/convex/core/lang/impl/ReturnValue.java deleted file mode 100644 index d773ab3a3..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/ReturnValue.java +++ /dev/null @@ -1,43 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.ErrorCodes; -import convex.core.data.ACell; - -/** - * Class representing a function return value - * - * "Computers are useless. They can only give you answers." - Pablo Picasso - * - * @param Type of return value - */ -public class ReturnValue extends AReturn { - - private final T value; - - public ReturnValue(T value) { - this.value = value; - } - - public static ReturnValue wrap(T value) { - return new ReturnValue(value); - } - - public T getValue() { - return value; - } - - @Override - public String toString() { - return "ReturnValue: " + value; - } - - @Override - public ACell getCode() { - return ErrorCodes.RETURN; - } - - @Override - public ACell getMessage() { - return null; - } -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/RollbackValue.java b/convex-core/src/main/java/convex/core/lang/impl/RollbackValue.java deleted file mode 100644 index f61a144cf..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/RollbackValue.java +++ /dev/null @@ -1,43 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.ErrorCodes; -import convex.core.data.ACell; - -/** - * Class representing a function return value - * - * "Computers are useless. They can only give you answers." - Pablo Picasso - * - * @param Type of return value - */ -public class RollbackValue extends AReturn { - - private final T value; - - public RollbackValue(T value) { - this.value = value; - } - - public static RollbackValue wrap(T value) { - return new RollbackValue(value); - } - - public T getValue() { - return value; - } - - @Override - public String toString() { - return "RollbackValue: " + value; - } - - @Override - public ACell getCode() { - return ErrorCodes.ROLLBACK; - } - - @Override - public ACell getMessage() { - return value; - } -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/SeqFn.java b/convex-core/src/main/java/convex/core/lang/impl/SeqFn.java deleted file mode 100644 index 12b3b1eff..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/SeqFn.java +++ /dev/null @@ -1,56 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.data.ACell; -import convex.core.data.ASequence; -import convex.core.data.prim.CVMLong; -import convex.core.data.type.Types; -import convex.core.lang.Context; -import convex.core.lang.RT; - -/** - * Wrapper for interpreting a sequence object as an invokable function - * - * - * @param Type of values to return - */ -public class SeqFn extends ADataFn { - - private ASequence seq; - - public SeqFn(ASequence m) { - this.seq = m; - } - - public static SeqFn wrap(ASequence m) { - return new SeqFn(m); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public Context invoke(Context context, ACell[] args) { - int n = args.length; - if (n == 1) { - CVMLong key = RT.ensureLong(args[0]); - if (key==null) return context.withCastError(0,args, Types.LONG); - long ix=key.longValue(); - if ((ix < 0) || (ix >= seq.count())) return (Context) context.withBoundsError(ix); - T result = (T) seq.get(key); - return context.withResult(result); - } else if (n == 2) { - CVMLong key = RT.ensureLong(args[0]); - if (key==null) return context.withCastError(0,args, Types.LONG); - long ix=key.longValue(); - if ((ix < 0) || (ix >= seq.count())) return (Context) context.withResult((T)args[1]); - T result = (T) seq.get(key); - return context.withResult(result); - } else { - return context.withArityError("Expected arity 1 or 2 for sequence lookup"); - } - } - - @Override - public void print(StringBuilder sb) { - seq.print(sb); - } - -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/SetFn.java b/convex-core/src/main/java/convex/core/lang/impl/SetFn.java deleted file mode 100644 index de86c164e..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/SetFn.java +++ /dev/null @@ -1,38 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.data.ACell; -import convex.core.data.ASet; -import convex.core.data.prim.CVMBool; -import convex.core.lang.Context; - -public class SetFn extends ADataFn { - - private ASet set; - - public SetFn(ASet m) { - this.set = m; - } - - public static SetFn wrap(ASet m) { - return new SetFn(m); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override - public Context invoke(Context context, ACell[] args) { - int n = args.length; - if (n == 1) { - ACell key = args[0]; - CVMBool result = CVMBool.create(set.contains(key)); - return context.withResult(result); - } else { - return context.withArityError("Expected arity 1 for set lookup but got: " + n + " in set: " + set); - } - } - - @Override - public void print(StringBuilder sb) { - set.print(sb); - } - -} diff --git a/convex-core/src/main/java/convex/core/lang/impl/TailcallValue.java b/convex-core/src/main/java/convex/core/lang/impl/TailcallValue.java deleted file mode 100644 index b30677eed..000000000 --- a/convex-core/src/main/java/convex/core/lang/impl/TailcallValue.java +++ /dev/null @@ -1,49 +0,0 @@ -package convex.core.lang.impl; - -import convex.core.ErrorCodes; -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.Vectors; -import convex.core.lang.AFn; - -/** - * Class representing a function return value. - * - * Contains argument values for each parameter to be substituted in the - * surrounding function / loop - */ -public class TailcallValue extends ATrampoline { - - private AFn function; - - private TailcallValue(AFn f, ACell[] values) { - super(values); - this.function=f; - } - - public static TailcallValue wrap(AFn f, ACell[] args) { - return new TailcallValue(f,args); - } - - @Override - public String toString() { - AVector seq = Vectors.create(args); // should always convert OK - return "Tailcall: " + seq; - } - - @Override - public ACell getCode() { - return ErrorCodes.TAILCALL; - } - - @Override - public ACell getMessage() { - return null; - } - - public AFn getFunction() { - return function; - } - - -} diff --git a/convex-core/src/main/java/convex/core/lang/ops/AMultiOp.java b/convex-core/src/main/java/convex/core/lang/ops/AMultiOp.java deleted file mode 100644 index 0cb2c533d..000000000 --- a/convex-core/src/main/java/convex/core/lang/ops/AMultiOp.java +++ /dev/null @@ -1,67 +0,0 @@ -package convex.core.lang.ops; - -import convex.core.data.ACell; -import convex.core.data.ASequence; -import convex.core.data.AVector; -import convex.core.data.Format; -import convex.core.data.IRefFunction; -import convex.core.data.Ref; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.AOp; - -/** - * Abstract base class for Ops with multiple nested operations - * - * MultiOps may selectively evaluate sub-expressions. - * - * @param Type of function return - */ -public abstract class AMultiOp extends AOp { - protected final AVector> ops; - - protected AMultiOp(AVector> ops) { - // TODO: need to think about bounds on number of child ops? - this.ops = ops; - } - - /** - * Recreates this object with an updated list of child Ops. - * - * @param newOps - * @return - */ - protected abstract AMultiOp recreate(ASequence> newOps); - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = Format.write(bs,pos, ops); - return pos; - } - - @Override - public int estimatedEncodingSize() { - return 10+ops.estimatedEncodingSize(); - } - - @Override - public AMultiOp updateRefs(IRefFunction func) { - ASequence> newOps = ops.updateRefs(func); - return recreate(newOps); - } - - @Override - public int getRefCount() { - return ops.getRefCount(); - } - - @SuppressWarnings("unchecked") - @Override - public Ref getRef(int i) { - return (Ref) ops.getRef(i); - } - - @Override - public void validateCell() throws InvalidDataException { - ops.validateCell(); - } -} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Cond.java b/convex-core/src/main/java/convex/core/lang/ops/Cond.java deleted file mode 100644 index 9cd3d0697..000000000 --- a/convex-core/src/main/java/convex/core/lang/ops/Cond.java +++ /dev/null @@ -1,112 +0,0 @@ -package convex.core.lang.ops; - -import java.nio.ByteBuffer; - -import convex.core.data.ACell; -import convex.core.data.ASequence; -import convex.core.data.AVector; -import convex.core.data.Format; -import convex.core.data.IRefFunction; -import convex.core.data.Vectors; -import convex.core.exceptions.BadFormatException; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.Juice; -import convex.core.lang.Ops; -import convex.core.lang.RT; - -/** - * Op representing a conditional expression. - * - * Child ops: - * 1. Should be condition / result pairs (with an optional single default result). - * 2. Are executed in sequence until the first condition succeeds - * 3. Are only executed if required, i.e. cond operates as a "short-circuiting" conditional. - * - * @param Result type of Op - */ -public class Cond extends AMultiOp { - - protected Cond(AVector> ops) { - super(ops); - } - - /** - * Create a Cond operation with the given nested operations - * - * @param Return type of Cond - * @param ops Ops to execute conditionally - * @return Cond instance - */ - public static Cond create(AOp... ops) { - ASequence> refOps=Vectors.create(ops); - return create(refOps); - } - - @Override - protected Cond recreate(ASequence> newOps) { - if (ops==newOps) return this; - return new Cond(newOps.toVector()); - } - - public static Cond create(ASequence> ops) { - return new Cond(ops.toVector()); - } - - @SuppressWarnings("unchecked") - @Override - public Context execute(Context context) { - int n=ops.size(); - Context ctx=context.consumeJuice(Juice.COND_OP); - if (ctx.isExceptional()) return (Context) ctx; - - for (int i=0; i<(n-1); i+=2) { - AOp testOp=ops.get(i); - ctx=ctx.execute(testOp); - - // bail out from exceptional result in test - if (ctx.isExceptional()) return (Context) ctx; - - ACell test=ctx.getResult(); - if (RT.bool(test)) { - return (Context) ctx.execute(ops.get(i+1)); - } - } - if ((n&1)==0) { - // no default value, return null - return ctx.withResult((T)null); - } else { - // default value - return (Context) ctx.execute(ops.get(n-1)); - } - } - - @Override - public void print(StringBuilder sb) { - sb.append("(cond"); - int len=ops.size(); - for (int i=0; i Cond read(ByteBuffer b) throws BadFormatException { - AVector> ops=Format.read(b); - return create(ops); - } - - @Override - public Cond updateRefs(IRefFunction func) { - ASequence> newOps= ops.updateRefs(func); - return recreate(newOps); - } - - -} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Constant.java b/convex-core/src/main/java/convex/core/lang/ops/Constant.java deleted file mode 100644 index 465adb512..000000000 --- a/convex-core/src/main/java/convex/core/lang/ops/Constant.java +++ /dev/null @@ -1,131 +0,0 @@ -package convex.core.lang.ops; - -import java.nio.ByteBuffer; - -import convex.core.data.AList; -import convex.core.data.AString; -import convex.core.data.AVector; -import convex.core.data.Format; -import convex.core.data.IRefFunction; -import convex.core.data.List; -import convex.core.data.Ref; -import convex.core.data.Strings; -import convex.core.data.VectorLeaf; -import convex.core.data.prim.CVMBool; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.Juice; -import convex.core.lang.Ops; -import convex.core.lang.RT; -import convex.core.data.ACell; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * Operation representing a constant value - * - * "One man's constant is another man's variable." - Alan Perlis - * - * @param Type of constant value - */ -public class Constant extends AOp { - - public static final Constant NULL = new Constant<>(Ref.NULL_VALUE); - public static final Constant TRUE = new Constant<>(Ref.TRUE_VALUE); - public static final Constant FALSE = new Constant<>(Ref.FALSE_VALUE); - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static final Constant> EMPTY_VECTOR = new Constant(VectorLeaf.EMPTY_REF); - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static final Constant> EMPTY_LIST = new Constant(List.EMPTY_REF); - - - private final Ref valueRef; - - private Constant(Ref valueRef) { - this.valueRef = valueRef; - } - - public static Constant create(T value) { - return new Constant(Ref.get(value)); - } - - public static Constant of(Object value) { - return create(RT.cvm(value)); - } - - public static Constant createString(String stringValue) { - return new Constant(Strings.create(stringValue).getRef()); - } - - public static Constant createFromRef(Ref valueRef) { - if (valueRef == null) throw new IllegalArgumentException("Can't create with null ref"); - return new Constant(valueRef); - } - - @Override - public Context execute(Context context) { - return context.withResult(Juice.CONSTANT, valueRef.getValue()); - } - - @Override - public void print(StringBuilder sb) { - Utils.print(sb,valueRef.getValue()); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = valueRef.encode(bs,pos); - return pos; - } - - @Override - public int estimatedEncodingSize() { - return 1+Format.MAX_EMBEDDED_LENGTH; - } - - public static AOp read(ByteBuffer bb) throws BadFormatException { - Ref ref = Format.readRef(bb); - return createFromRef(ref); - } - - @Override - public byte opCode() { - return Ops.CONSTANT; - } - - @Override - public int getRefCount() { - return 1; - } - - @SuppressWarnings("unchecked") - @Override - public Ref getRef(int i) { - if (i != 0) throw new IndexOutOfBoundsException(Errors.badIndex(i)); - return (Ref) valueRef; - } - - @Override - public Constant updateRefs(IRefFunction func) { - @SuppressWarnings("unchecked") - Ref newRef = (Ref) func.apply(valueRef); - if (valueRef == newRef) return this; - return createFromRef(newRef); - } - - @SuppressWarnings("unchecked") - public static AOp nil() { - return (AOp) Constant.NULL; - } - - @Override - public void validateCell() throws InvalidDataException { - if (valueRef == null) throw new InvalidDataException("Missing contant value ref!", this); - } - - -} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Def.java b/convex-core/src/main/java/convex/core/lang/ops/Def.java deleted file mode 100644 index 1053bf5a4..000000000 --- a/convex-core/src/main/java/convex/core/lang/ops/Def.java +++ /dev/null @@ -1,155 +0,0 @@ -package convex.core.lang.ops; - -import java.nio.ByteBuffer; - -import convex.core.data.Format; -import convex.core.data.IRefFunction; -import convex.core.data.Ref; -import convex.core.data.Symbol; -import convex.core.data.Syntax; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.Juice; -import convex.core.lang.Ops; -import convex.core.lang.RT; -import convex.core.data.ACell; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * Op that creates a definition in the current environment. - * - * Def may optionally have symbolic metadata attached to the symbol. - * - * @param Type of defined value - */ -public class Def extends AOp { - - // symbol Syntax Object including metadata to add to the defined environment - private final ACell symbol; - - // expression to execute to determine the defined value - private final Ref> op; - - private Def(ACell key, Ref> op) { - this.op = op; - this.symbol = key; - if (symbol==null) throw new IllegalArgumentException("Null key in Def!!!"); - } - - public static Def create(ACell key, Ref> op) { - if (!validKey(key)) throw new IllegalArgumentException("Invalid Def key: "+key); - return new Def(key, op); - } - - public static Def create(Syntax key, Ref> op) { - if (!validKey(key)) throw new IllegalArgumentException("Invalid Def key: "+key); - return new Def(key, op); - } - - public static Def create(Syntax key, AOp op) { - return create(key, op.getRef()); - } - - public static Def create(Symbol key, AOp op) { - return new Def(key, op.getRef()); - } - - public static Def create(ACell key, AOp op) { - return new Def(key, op.getRef()); - } - - public static Def create(String key, AOp op) { - return create(Symbol.create(key), op); - } - - @SuppressWarnings("unchecked") - @Override - public Context execute(Context context) { - Context ctx = context.execute(op.getValue()); - if (ctx.isExceptional()) return ctx; - - ACell opResult = ctx.getResult(); - - // TODO: defined syntax metadata - if (symbol instanceof Syntax) { - Syntax syn=(Syntax)symbol; - ctx = ctx.defineWithSyntax(syn, opResult); - } else { - ctx=ctx.define((Symbol)symbol, opResult); - } - return (Context) ctx.withResult(Juice.DEF, opResult); - } - - @Override - public int getRefCount() { - return 1; - } - - @SuppressWarnings("unchecked") - @Override - public Ref> getRef(int i) { - if (i != 0) throw new IndexOutOfBoundsException(Errors.badIndex(i)); - return op; - } - - @SuppressWarnings("unchecked") - @Override - public Def updateRefs(IRefFunction func) { - ACell newSymbol=symbol.updateRefs(func); - Ref> newRef = (Ref>) func.apply(op); - if (op == newRef) return this; - return new Def(newSymbol, newRef); - } - - @Override - public void print(StringBuilder sb) { - sb.append("(def "); - symbol.print(sb); - sb.append(' '); - Utils.print(sb, op.getValue()); - sb.append(')'); - } - - @Override - public byte opCode() { - return Ops.DEF; - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = Format.write(bs,pos, symbol); - pos = op.encode(bs,pos); - return pos; - } - - @Override - public int estimatedEncodingSize() { - return symbol.estimatedEncodingSize()+Format.MAX_EMBEDDED_LENGTH; - } - - public static Def read(ByteBuffer b) throws BadFormatException { - ACell symbol = Format.read(b); - Ref> ref = Format.readRef(b); - if (!validKey(symbol)) throw new BadFormatException("Symbol not valid for Def op"); - return new Def<>(symbol, ref); - } - - @Override - public void validateCell() throws InvalidDataException { - if (!validKey(symbol)) { - throw new InvalidDataException("Def requires a Symbol or Syntax Object for definition but was: "+RT.getType(symbol),this); - } - symbol.validateCell(); - } - - private static boolean validKey(ACell key) { - if (key instanceof Symbol) return true; - if (!(key instanceof Syntax)) return false; - return ((Syntax)key).getValue() instanceof Symbol; - } - - -} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Do.java b/convex-core/src/main/java/convex/core/lang/ops/Do.java deleted file mode 100644 index 628ad38c5..000000000 --- a/convex-core/src/main/java/convex/core/lang/ops/Do.java +++ /dev/null @@ -1,88 +0,0 @@ -package convex.core.lang.ops; - -import java.nio.ByteBuffer; - -import convex.core.data.ACell; -import convex.core.data.ASequence; -import convex.core.data.AVector; -import convex.core.data.Format; -import convex.core.data.Vectors; -import convex.core.exceptions.BadFormatException; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.Juice; -import convex.core.lang.Ops; - -/** - * Op for executing a sequence of child operations in order - * - * "Design is to take things apart in such a way that they can be put back - * together" - * - Rich Hickey - * - * @param Result type of Do Op - */ -public class Do extends AMultiOp { - - public static final Do EMPTY = Do.create(); - - protected Do(AVector> ops) { - super(ops); - } - - public static Do create(AOp... ops) { - return new Do(Vectors.create(ops)); - } - - @Override - protected Do recreate(ASequence> newOps) { - if (ops == newOps) return this; - return new Do(newOps.toVector()); - } - - public static Do create(ASequence> ops) { - return new Do(ops.toVector()); - } - - @SuppressWarnings("unchecked") - @Override - public Context execute(Context context) { - int n = ops.size(); - if (n == 0) return (Context) context.withResult(Juice.DO, null); // need cast to avoid bindings overload - - Context ctx = (Context) context.consumeJuice(Juice.DO); - if (ctx.isExceptional()) return ctx; - - // execute each operation in turn - // TODO: early return - for (int i = 0; i < n; i++) { - AOp op = ops.get(i); - ctx = (Context) ctx.execute(op); - - if (ctx.isExceptional()) break; - - } - return ctx; - } - - @Override - public void print(StringBuilder sb) { - sb.append("(do"); - int len = ops.size(); - for (int i = 0; i < len; i++) { - sb.append(' '); - ops.get(i).print(sb); - } - sb.append(')'); - } - - @Override - public byte opCode() { - return Ops.DO; - } - - public static Do read(ByteBuffer b) throws BadFormatException { - AVector> ops = Format.read(b); - return create(ops); - } -} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Invoke.java b/convex-core/src/main/java/convex/core/lang/ops/Invoke.java deleted file mode 100644 index 9bb1abeee..000000000 --- a/convex-core/src/main/java/convex/core/lang/ops/Invoke.java +++ /dev/null @@ -1,108 +0,0 @@ -package convex.core.lang.ops; - -import java.nio.ByteBuffer; - -import convex.core.data.ACell; -import convex.core.data.ASequence; -import convex.core.data.AVector; -import convex.core.data.Format; -import convex.core.data.Vectors; -import convex.core.data.type.Types; -import convex.core.exceptions.BadFormatException; -import convex.core.lang.AFn; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.Ops; -import convex.core.lang.RT; - -/** - * Op representing the invocation of a function. - * - * The first child Op identifies the function to be called, the remaining ops - * are arguments. - * - * @param Result type of Op - */ -public class Invoke extends AMultiOp { - - protected Invoke(AVector> ops) { - super(ops); - } - - public static Invoke create(ASequence> ops) { - AVector> vops = ops.toVector(); - return new Invoke(vops); - } - - public static Invoke create(AOp... ops) { - return create(Vectors.create(ops)); - } - - @SuppressWarnings("unchecked") - public static , F extends AOp> Invoke create(F f, ASequence args) { - ASequence> nargs = (ASequence>) args; - ASequence> ops = nargs.cons(f); - - return create(ops); - } - - @Override - protected Invoke recreate(ASequence> newOps) { - if (ops == newOps) return this; - return create(newOps); - } - - public static Invoke create(String string, AOp... args) { - return create(Lookup.create(string), Vectors.create(args)); - } - - @SuppressWarnings("unchecked") - @Override - public Context execute(Context context) { - // execute first op to obtain function value - AOp fnOp=ops.get(0); - Context ctx = (Context) context.execute(fnOp); - if (ctx.isExceptional()) return ctx; - - ACell rf = ctx.getResult(); - AFn fn = RT.castFunction(rf); - if (fn == null) return context.withCastError(0, Types.FUNCTION); - - int arity = ops.size() - 1; - ACell[] args = new ACell[arity]; - for (int i = 0; i < arity; i++) { - // Compute the op for each argument in order - AOp argOp=ops.get(i + 1); - ctx = (Context) ctx.execute(argOp); - if (ctx.isExceptional()) return ctx; - - args[i] = ctx.getResult(); - } - - ctx = ctx.invoke(fn, args); - return (Context) ctx; - } - - @Override - public void print(StringBuilder sb) { - sb.append('('); - int len = ops.size(); - for (int i = 0; i < len; i++) { - if (i > 0) sb.append(' '); - ops.get(i).print(sb); - } - sb.append(')'); - } - - @Override - public byte opCode() { - return Ops.INVOKE; - } - - public static Invoke read(ByteBuffer bb) throws BadFormatException { - AVector> ops = Format.read(bb); - if (ops == null) throw new BadFormatException("Can't read an Invoke with no ops"); - - return create(ops); - } -} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Lambda.java b/convex-core/src/main/java/convex/core/lang/ops/Lambda.java deleted file mode 100644 index 8d3af901b..000000000 --- a/convex-core/src/main/java/convex/core/lang/ops/Lambda.java +++ /dev/null @@ -1,111 +0,0 @@ -package convex.core.lang.ops; - -import java.nio.ByteBuffer; - -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.Format; -import convex.core.data.IRefFunction; -import convex.core.data.Ref; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.Juice; -import convex.core.lang.Ops; -import convex.core.lang.impl.AClosure; -import convex.core.lang.impl.Fn; -import convex.core.util.Errors; -import convex.core.util.Utils; - -/** - * Op responsible for creating a new function (closure). - * - * Captures value of local variable bindings during execution. - * - * Equivalent to (fn [...] ...) - * - * @param Result type of Closure - */ -public class Lambda extends AOp> { - - private Ref> function; - - protected Lambda(Ref> newFunction) { - this.function=newFunction; - } - - public static Lambda create(AVector params, AOp body) { - return new Lambda(Fn.create(params,body).getRef()); - } - - public static Lambda create(AClosure fn) { - return new Lambda(fn.getRef()); - } - - @Override - public Context> execute(Context context) { - AClosure fn= function.getValue().withEnvironment(context.getLocalBindings()); - return context.withResult(Juice.LAMBDA,fn); - } - - @Override - public int getRefCount() { - return 1; - } - - @SuppressWarnings("unchecked") - @Override - public Ref getRef(int i) { - if (i==0) return (Ref) function; - throw new IndexOutOfBoundsException(Errors.badIndex(i)); - } - - @Override - public Lambda updateRefs(IRefFunction func) { - @SuppressWarnings("unchecked") - Ref> newFunction=(Ref>) func.apply(function); - if (function==newFunction) return this; - return new Lambda(newFunction); - } - - @Override - public void print(StringBuilder sb) { - function.getValue().print(sb); - } - - @Override - public byte opCode() { - return Ops.LAMBDA; - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos=function.encode(bs, pos); - return pos; - } - - public static Lambda read(ByteBuffer bb) throws BadFormatException { - Ref> function=Format.readRef(bb); - return new Lambda(function); - } - - @Override - public void validate() throws InvalidDataException { - super.validate(); - - ACell fn=function.getValue(); - if (!(fn instanceof AClosure)) { - throw new InvalidDataException("Lambda child must be a closure but got: "+Utils.getClassName(fn),this); - } - } - - @Override - public void validateCell() throws InvalidDataException { - // nothing to do? - } - - public AClosure getFunction() { - return function.getValue(); - } -} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Let.java b/convex-core/src/main/java/convex/core/lang/ops/Let.java deleted file mode 100644 index 38c9e1bbd..000000000 --- a/convex-core/src/main/java/convex/core/lang/ops/Let.java +++ /dev/null @@ -1,179 +0,0 @@ -package convex.core.lang.ops; - -import java.nio.ByteBuffer; - -import convex.core.data.ACell; -import convex.core.data.ASequence; -import convex.core.data.AVector; -import convex.core.data.Format; -import convex.core.data.IRefFunction; -import convex.core.data.Ref; -import convex.core.exceptions.BadFormatException; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.Juice; -import convex.core.lang.Ops; -import convex.core.lang.impl.RecurValue; -import convex.core.util.Utils; - -/** - * Op for executing a body after lexically binding one or more symbols. - * - * Can represent (let [..] ..) and (loop [..] ..). - * - * Loop version can act as a target for (recur ...) expressions. - * - * @param Result type of Op - */ -public class Let extends AMultiOp { - - /** - * Vector of binding forms. Can be destructuring forms - */ - protected final AVector symbols; - - protected final int bindingCount; - protected final boolean isLoop; - - protected Let(AVector syms, AVector> ops, boolean isLoop) { - super(ops); - symbols = syms; - bindingCount = syms.size(); - this.isLoop = isLoop; - } - - public static Let create(AVector syms, AVector> ops, boolean isLoop) { - return new Let(syms, ops, isLoop); - } - - @Override - public Let updateRefs(IRefFunction func) { - ASequence> newOps = ops.updateRefs(func); - AVector newSymbols = symbols.updateRefs(func); - - return recreate(newOps, newSymbols); - } - - @Override - public int getRefCount() { - return super.getRefCount() + symbols.getRefCount(); - } - - @Override - public final Ref getRef(int i) { - int n = super.getRefCount(); - if (i < n) return super.getRef(i); - return symbols.getRef(i - n); - } - - @Override - protected Let recreate(ASequence> newOps) { - return recreate(newOps, symbols); - } - - protected Let recreate(ASequence> newOps, AVector newSymbols) { - if ((ops == newOps) && (symbols == newSymbols)) return this; - return new Let(newSymbols, newOps.toVector(), isLoop); - } - - @SuppressWarnings("unchecked") - @Override - public Context execute(final Context context) { - Context ctx = context.consumeJuice(Juice.LET); - if (ctx.isExceptional()) return (Context) ctx; - - AVector savedEnv = ctx.getLocalBindings(); - - // execute each operation for bound values in turn - for (int i = 0; i < bindingCount; i++) { - AOp op = ops.get(i); - ctx = ctx.executeLocalBinding(symbols.get(i), op); - if (ctx.isExceptional()) { - // return if exception during initial binding. - // No chance to recur since we didn't enter loop body - return ctx.withLocalBindings(savedEnv); - } - } - - ctx = executeBody(ctx); - if (isLoop&&ctx.isExceptional()) { - // check for recur if this Let form is a loop - // other exceptionals we can just let slip - Object o = ctx.getExceptional(); - while (o instanceof RecurValue) { - RecurValue rv = (RecurValue) o; - ACell[] newArgs = rv.getValues(); - if (newArgs.length != bindingCount) { - // recur arity is wrong, need to break loop with exceptional result - String message="Expected " + bindingCount + " value(s) for recur but got: " + newArgs.length; - ctx = ctx.withArityError(message); - break; - } - - // restore old lexical environment, then add back new ones - ctx=ctx.withLocalBindings(savedEnv); - ctx = ctx.updateBindings(symbols, newArgs); - if (ctx.isExceptional()) break; - - ctx = executeBody(ctx); - o = ctx.getValue(); - } - } - // restore old lexical environment before returning - return ctx.withLocalBindings(savedEnv); - } - - public Context executeBody(Context ctx) { - int end = ops.size(); - if (bindingCount == end) return ctx.withResult(null); - for (int i = bindingCount; i < end; i++) { - ctx = ctx.execute(ops.get(i)); - if (ctx.isExceptional()) { - return ctx; - } - } - return ctx; - } - - @Override - public void print(StringBuilder sb) { - sb.append(isLoop ? "(loop [" : "(let ["); - int len = ops.size(); - for (int i = 0; i < bindingCount; i++) { - if (i > 0) sb.append(' '); - Utils.print(sb, symbols.get(i)); - sb.append(' '); - ops.get(i).print(sb); - sb.append(' '); - } - sb.append("] "); - - for (int i = bindingCount; i < len; i++) { - sb.append(' '); - ops.get(i).print(sb); - } - sb.append(')'); - } - - @Override - public byte opCode() { - return (isLoop)?Ops.LOOP:Ops.LET; - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = Format.write(bs,pos, symbols); - return super.encodeRaw(bs,pos); // AMultiOp superclass writeRaw - } - - @Override - public int estimatedEncodingSize() { - return super.estimatedEncodingSize()+symbols.estimatedEncodingSize(); - } - - public static Let read(ByteBuffer b, boolean isLoop) throws BadFormatException { - AVector syms = Format.read(b); - AVector> ops = Format.read(b); - return create(syms, ops.toVector(),isLoop); - } -} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Local.java b/convex-core/src/main/java/convex/core/lang/ops/Local.java deleted file mode 100644 index ac7bca5c2..000000000 --- a/convex-core/src/main/java/convex/core/lang/ops/Local.java +++ /dev/null @@ -1,104 +0,0 @@ -package convex.core.lang.ops; - -import java.nio.ByteBuffer; - -import convex.core.ErrorCodes; -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.Format; -import convex.core.data.IRefFunction; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.Juice; -import convex.core.lang.Ops; - -/** - * Op to look up a local value from the lexical environment - * - * @param Result type of Op - */ -public class Local extends AOp { - - /** - * Stack position in lexical stack - */ - private final long position; - - - private Local(long position) { - this.position=position; - } - - - /** - * Creates Local to look up a lexical value in the given position - * - * @param position Position in lexical value vector - * @return Special instance, or null if not found - */ - public static final Local create(long position) { - if (position<0) return null; - return new Local(position); - } - - @SuppressWarnings("unchecked") - @Override - public Context execute(Context context) { - Context ctx=(Context) context; - AVector env=ctx.getLocalBindings(); - long ec=env.count(); - if ((position<0)||(position>=ec)) { - return ctx.withError(ErrorCodes.BOUNDS,"Bad position for Local: "+position); - } - T result = (T)env.get(position); - return (Context) ctx.withResult(Juice.LOOKUP,result); - } - - @Override - public byte opCode() { - return Ops.LOCAL; - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos=Format.writeVLCLong(bs, pos, position); - return pos; - } - - public static Local read(ByteBuffer bb) throws BadFormatException { - long position=Format.readVLCLong(bb); - return create(position); - } - - @Override - public Local updateRefs(IRefFunction func) { - return this; - } - - @Override - public void validateCell() throws InvalidDataException { - if (position<0) { - throw new InvalidDataException("Invalid Local position "+position, this); - } - } - - @Override - public int getRefCount() { - return 0; - } - - @Override - public void print(StringBuilder sb) { - sb.append(toString()); - } - - @Override - public String toString() { - return "%" + position; - } - - - -} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Lookup.java b/convex-core/src/main/java/convex/core/lang/ops/Lookup.java deleted file mode 100644 index 9a2020dde..000000000 --- a/convex-core/src/main/java/convex/core/lang/ops/Lookup.java +++ /dev/null @@ -1,136 +0,0 @@ -package convex.core.lang.ops; - -import java.nio.ByteBuffer; - -import convex.core.ErrorCodes; -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.data.Format; -import convex.core.data.IRefFunction; -import convex.core.data.Ref; -import convex.core.data.Symbol; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.Juice; -import convex.core.lang.Ops; -import convex.core.lang.RT; - -/** - * Op to look up a Symbol in the current execution context. - * - * Holds an optional Address to specify lookup in another Account environment. If null, the Lookup will be performed in - * the current environment. - * - * Consumes juice for lookup when executed. - * - * @param Result type of Op - */ -public class Lookup extends AOp { - private final AOp
address; - private final Symbol symbol; - - private Lookup(AOp
address,Symbol symbol) { - this.address=address; - this.symbol = symbol; - } - - public static Lookup create(AOp
address, Symbol form) { - return new Lookup(address,form); - } - - public static Lookup create(AOp
address, String name) { - return create(address,Symbol.create(name)); - } - - public static Lookup create(Address addr, Symbol sym) { - return create(Constant.of(addr),sym); - } - - public static Lookup create(Symbol symbol) { - return create((AOp
)null,symbol); - } - - public static Lookup create(String name) { - return create(Symbol.create(name)); - } - - @SuppressWarnings("unchecked") - @Override - public Context execute(Context context) { - Context rctx=(Context) context; - Address namespaceAddress=null; - if (address!=null) { - rctx=(Context) rctx.execute(address); - if (rctx.isExceptional()) return rctx; - ACell maybeAddress=rctx.getResult(); - namespaceAddress=RT.ensureAddress(maybeAddress); - if (namespaceAddress==null) return rctx.withError(ErrorCodes.CAST,"Lookup requires Address but got: "+RT.getType(maybeAddress)); - } - - // Do a dynamic lookup, with address if specified or address from current context otherwise - namespaceAddress=(address==null)?context.getAddress():namespaceAddress; - return rctx.lookupDynamic(namespaceAddress,symbol).consumeJuice(Juice.LOOKUP_DYNAMIC); - } - - @Override - public void print(StringBuilder sb) { - if (address!=null) { - address.print(sb); - sb.append('/'); - } - symbol.print(sb); - } - - @Override - public byte opCode() { - return Ops.LOOKUP; - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos= symbol.encode(bs, pos); - pos= Format.write(bs,pos, address); // might be null - return pos; - } - - public static Lookup read(ByteBuffer bb) throws BadFormatException { - Symbol sym = Format.read(bb); - if (sym==null) throw new BadFormatException("Lookup symbol cannot be null"); - AOp
address = Format.read(bb); - return create(address,sym); - } - - @Override - public int getRefCount() { - if (address==null) return 0; - return address.getRefCount(); - } - - @Override - public Ref getRef(int i) { - if (address==null) throw new IndexOutOfBoundsException(); - return address.getRef(i); - } - - @Override - public Lookup updateRefs(IRefFunction func) { - if (address==null) return this; - AOp
newAddress=address.updateRefs(func); - if (address==newAddress) return this; - return create(newAddress,symbol); - } - - @Override - public void validateCell() throws InvalidDataException { - if (address!=null) address.validateCell(); - symbol.validateCell(); - } - - public AOp
getAddress() { - return address; - } - - -} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Query.java b/convex-core/src/main/java/convex/core/lang/ops/Query.java deleted file mode 100644 index 6cf0fea30..000000000 --- a/convex-core/src/main/java/convex/core/lang/ops/Query.java +++ /dev/null @@ -1,94 +0,0 @@ -package convex.core.lang.ops; - -import java.nio.ByteBuffer; - -import convex.core.State; -import convex.core.data.ACell; -import convex.core.data.ASequence; -import convex.core.data.AVector; -import convex.core.data.Format; -import convex.core.data.Vectors; -import convex.core.exceptions.BadFormatException; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.Juice; -import convex.core.lang.Ops; - -/** - * Op for executing a sequence of child operations in order - * - * "Design is to take things apart in such a way that they can be put back - * together" - * - Rich Hickey - * - * @param Result type of Op - */ -public class Query extends AMultiOp { - - protected Query(AVector> ops) { - super(ops); - } - - public static Query create(AOp... ops) { - return new Query(Vectors.create(ops)); - } - - @Override - protected Query recreate(ASequence> newOps) { - if (ops == newOps) return this; - return new Query(newOps.toVector()); - } - - public static Query create(ASequence> ops) { - return new Query(ops.toVector()); - } - - @SuppressWarnings("unchecked") - @Override - public Context execute(Context context) { - State savedState=context.getState(); - - int n = ops.size(); - if (n == 0) return (Context) context.withResult(Juice.QUERY, null); // need cast to avoid bindings overload - - Context ctx = (Context) context.consumeJuice(Juice.QUERY); - if (ctx.isExceptional()) return ctx; - - AVector savedBindings=context.getLocalBindings(); - - // execute each operation in turn - // TODO: early return - for (int i = 0; i < n; i++) { - AOp op = ops.get(i); - ctx = (Context) ctx.execute(op); - - if (ctx.isExceptional()) break; - - } - // restore state unconditionally. - ctx=ctx.withState(savedState); - ctx=ctx.withLocalBindings(savedBindings); - return ctx; - } - - @Override - public void print(StringBuilder sb) { - sb.append("(query"); - int len = ops.size(); - for (int i = 0; i < len; i++) { - sb.append(' '); - ops.get(i).print(sb); - } - sb.append(')'); - } - - @Override - public byte opCode() { - return Ops.QUERY; - } - - public static Query read(ByteBuffer b) throws BadFormatException { - AVector> ops = Format.read(b); - return create(ops); - } -} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Set.java b/convex-core/src/main/java/convex/core/lang/ops/Set.java deleted file mode 100644 index 5f29ecee0..000000000 --- a/convex-core/src/main/java/convex/core/lang/ops/Set.java +++ /dev/null @@ -1,128 +0,0 @@ -package convex.core.lang.ops; - -import java.nio.ByteBuffer; - -import convex.core.ErrorCodes; -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.Format; -import convex.core.data.IRefFunction; -import convex.core.data.Ref; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.Juice; -import convex.core.lang.Ops; -import convex.core.util.Errors; - -/** - * Op to set a lexical value in the local execution context. - * - * @param Result type of Op - */ -public class Set extends AOp { - - /** - * Stack position in lexical stack - */ - private final long position; - - /** - * Op to compute new value - */ - private final Ref> op; - - private Set(long position, Ref> op) { - this.position = position; - this.op = op; - } - - /** - * Creates special Op for the given opCode - * - * @param position Position in lexical value vector - * @param op Op to calculate new value - * @return Special instance, or null if not found - */ - public static final Set create(long position, AOp op) { - if (position < 0) return null; - return new Set(position, op.getRef()); - } - - @SuppressWarnings("unchecked") - @Override - public Context execute(Context context) { - Context ctx = (Context) context; - AVector env = ctx.getLocalBindings(); - long ec = env.count(); - if ((position < 0) || (position >= ec)) - return context.withError(ErrorCodes.BOUNDS, "Bad position for set!: " + position); - - ctx = ctx.execute(op.getValue()); - if (ctx.isExceptional()) return ctx; - ACell value = ctx.getResult(); - - AVector newEnv = env.assoc(position, value); - ctx = ctx.withLocalBindings(newEnv); - return ctx.consumeJuice(Juice.SET_BANG); - } - - @Override - public byte opCode() { - return Ops.SET; - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = Format.writeVLCLong(bs, pos, position); - return pos; - } - - public static Set read(ByteBuffer bb) throws BadFormatException { - long position = Format.readVLCLong(bb); - AOp op = Format.read(bb); - return create(position, op); - } - - @Override - public Set updateRefs(IRefFunction func) { - @SuppressWarnings("unchecked") - Ref> newOp = (Ref>) func.apply(op); - if (op == newOp) return this; - return new Set(position, newOp); - } - - @Override - public void validateCell() throws InvalidDataException { - if (op == null) { - throw new InvalidDataException("Null Set op ", this); - } - if (position < 0) { - throw new InvalidDataException("Invalid Local position " + position, this); - } - } - - @Override - public int getRefCount() { - return 1; - } - - @SuppressWarnings("unchecked") - @Override - public Ref> getRef(int i) { - if (i != 0) throw new IndexOutOfBoundsException(Errors.badIndex(i)); - return op; - } - - @Override - public void print(StringBuilder sb) { - sb.append(toString()); - } - - @Override - public String toString() { - return "(set! %" + position + " " + op.getValue() + ")"; - } - -} diff --git a/convex-core/src/main/java/convex/core/lang/ops/Special.java b/convex-core/src/main/java/convex/core/lang/ops/Special.java deleted file mode 100644 index 53d7d877b..000000000 --- a/convex-core/src/main/java/convex/core/lang/ops/Special.java +++ /dev/null @@ -1,154 +0,0 @@ -package convex.core.lang.ops; - -import java.util.HashMap; - -import convex.core.data.ACell; -import convex.core.data.IRefFunction; -import convex.core.data.Symbol; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.Juice; -import convex.core.lang.Ops; -import convex.core.lang.Symbols; - -public class Special extends AOp { - - private final byte opCode; - - private static int NUM_SPECIALS=14; - private static final int BASE=Ops.SPECIAL_BASE; - private static final int LIMIT=BASE+NUM_SPECIALS; - private static final Symbol[] symbols=new Symbol[NUM_SPECIALS]; - private static final Special[] specials=new Special[NUM_SPECIALS]; - private static final HashMap opcodes=new HashMap<>(); - - private static final byte S_JUICE=BASE+0; - private static final byte S_CALLER=BASE+1; - private static final byte S_ADDRESS=BASE+2; - private static final byte S_MEMORY=BASE+3; - private static final byte S_BALANCE=BASE+4; - private static final byte S_ORIGIN=BASE+5; - private static final byte S_RESULT=BASE+6; - private static final byte S_TIMESTAMP=BASE+7; - private static final byte S_DEPTH=BASE+8; - private static final byte S_OFFER=BASE+9; - private static final byte S_STATE=BASE+10; - private static final byte S_HOLDINGS=BASE+11; - private static final byte S_SEQUENCE=BASE+12; - private static final byte S_KEY=BASE+13; - - static { - reg(S_JUICE,Symbols.STAR_JUICE); - reg(S_CALLER,Symbols.STAR_CALLER); - reg(S_ADDRESS,Symbols.STAR_ADDRESS); - reg(S_MEMORY,Symbols.STAR_MEMORY); - reg(S_BALANCE,Symbols.STAR_BALANCE); - reg(S_ORIGIN,Symbols.STAR_ORIGIN); - reg(S_RESULT,Symbols.STAR_RESULT); - reg(S_TIMESTAMP,Symbols.STAR_TIMESTAMP); - reg(S_DEPTH,Symbols.STAR_DEPTH); - reg(S_OFFER,Symbols.STAR_OFFER); - reg(S_STATE,Symbols.STAR_STATE); - reg(S_HOLDINGS,Symbols.STAR_HOLDINGS); - reg(S_SEQUENCE,Symbols.STAR_SEQUENCE); - reg(S_KEY,Symbols.STAR_KEY); - } - - private static byte reg(byte opCode, Symbol sym) { - int i=opCode-BASE; - symbols[i]=sym; - Special special=new Special<>(opCode); - specials[i]=special; - opcodes.put(sym,Integer.valueOf(opCode)); - return opCode; - } - - - private Special(byte opCode) { - this.opCode=opCode; - } - - - /** - * Creates special Op for the given opCode - * @param opCode Special opcode - * @return Special instance, or null if not found - */ - public static final Special create(int opCode) { - if ((opCodeLIMIT)) return null; - return specials[opCode-BASE]; - } - - @SuppressWarnings("unchecked") - @Override - public Context execute(Context context) { - Context ctx=(Context) context; - switch (opCode) { - case S_JUICE: ctx= ctx.withResult(CVMLong.create(ctx.getJuice())); break; - case S_CALLER: ctx= ctx.withResult(ctx.getCaller()); break; - case S_ADDRESS: ctx= ctx.withResult(ctx.getAddress()); break; - case S_MEMORY: ctx= ctx.withResult(CVMLong.create(ctx.getAccountStatus().getMemory())); break; - case S_BALANCE: ctx= ctx.withResult(CVMLong.create(ctx.getBalance())); break; - case S_ORIGIN: ctx= ctx.withResult(ctx.getOrigin()); break; - case S_RESULT: break; // unchanged context with current result - case S_TIMESTAMP: ctx= ctx.withResult(ctx.getState().getTimeStamp()); break; - case S_DEPTH: ctx= ctx.withResult(CVMLong.create(ctx.getDepth()-1)); break; // Depth before executing this Op - case S_OFFER: ctx= ctx.withResult(CVMLong.create(ctx.getOffer())); break; - case S_STATE: ctx= ctx.withResult(ctx.getState()); break; - case S_HOLDINGS: ctx= ctx.withResult(ctx.getHoldings()); break; - case S_SEQUENCE: ctx= ctx.withResult(CVMLong.create(ctx.getAccountStatus().getSequence())); break; - case S_KEY: ctx= ctx.withResult(ctx.getAccountStatus().getAccountKey()); break; - default: - throw new Error("Bad Opcode"+opCode); - } - return ctx.consumeJuice(Juice.SPECIAL); - } - - @Override - public byte opCode() { - return opCode; - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - // No data - return pos; - } - - @Override - public Special updateRefs(IRefFunction func) { - return this; - } - - @Override - public void validateCell() throws InvalidDataException { - if ((opCode=LIMIT)) { - throw new InvalidDataException("Invalid Special opCode "+opCode, this); - } - } - - @Override - public int getRefCount() { - return 0; - } - - @Override - public void print(StringBuilder sb) { - symbols[opCode-BASE].print(sb); - } - - /** - * Gets the special Op for a given Symbol, or null if not found - * @param Result type - * @param sym Symbol to look up - * @return Special Op or null - */ - @SuppressWarnings("unchecked") - public static Special forSymbol(Symbol sym) { - Integer special=opcodes.get(sym); - if (special==null) return null; - return (Special) specials[special-BASE]; - } -} diff --git a/convex-core/src/main/java/convex/core/lang/reader/AntlrReader.java b/convex-core/src/main/java/convex/core/lang/reader/AntlrReader.java deleted file mode 100644 index 4679a1c36..000000000 --- a/convex-core/src/main/java/convex/core/lang/reader/AntlrReader.java +++ /dev/null @@ -1,477 +0,0 @@ -package convex.core.lang.reader; - -import java.io.IOException; -import java.util.ArrayList; - -import org.antlr.v4.runtime.CharStream; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; -import org.antlr.v4.runtime.ParserRuleContext; -import org.antlr.v4.runtime.tree.ErrorNode; -import org.antlr.v4.runtime.tree.ParseTree; -import org.antlr.v4.runtime.tree.ParseTreeWalker; -import org.antlr.v4.runtime.tree.TerminalNode; - -import convex.core.data.ACell; -import convex.core.data.AHashMap; -import convex.core.data.AList; -import convex.core.data.Address; -import convex.core.data.Blob; -import convex.core.data.Keyword; -import convex.core.data.Lists; -import convex.core.data.Maps; -import convex.core.data.Sets; -import convex.core.data.Strings; -import convex.core.data.Symbol; -import convex.core.data.Syntax; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMBool; -import convex.core.data.prim.CVMChar; -import convex.core.data.prim.CVMDouble; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.ParseException; -import convex.core.lang.RT; -import convex.core.lang.Symbols; -import convex.core.lang.reader.antlr.ConvexLexer; -import convex.core.lang.reader.antlr.ConvexListener; -import convex.core.lang.reader.antlr.ConvexParser; -import convex.core.lang.reader.antlr.ConvexParser.AddressContext; -import convex.core.lang.reader.antlr.ConvexParser.BlobContext; -import convex.core.lang.reader.antlr.ConvexParser.BoolContext; -import convex.core.lang.reader.antlr.ConvexParser.CharacterContext; -import convex.core.lang.reader.antlr.ConvexParser.CommentedContext; -import convex.core.lang.reader.antlr.ConvexParser.DataStructureContext; -import convex.core.lang.reader.antlr.ConvexParser.DoubleValueContext; -import convex.core.lang.reader.antlr.ConvexParser.FormContext; -import convex.core.lang.reader.antlr.ConvexParser.FormsContext; -import convex.core.lang.reader.antlr.ConvexParser.KeywordContext; -import convex.core.lang.reader.antlr.ConvexParser.ListContext; -import convex.core.lang.reader.antlr.ConvexParser.LiteralContext; -import convex.core.lang.reader.antlr.ConvexParser.LongValueContext; -import convex.core.lang.reader.antlr.ConvexParser.MapContext; -import convex.core.lang.reader.antlr.ConvexParser.NilContext; -import convex.core.lang.reader.antlr.ConvexParser.PathSymbolContext; -import convex.core.lang.reader.antlr.ConvexParser.QuotedContext; -import convex.core.lang.reader.antlr.ConvexParser.SetContext; -import convex.core.lang.reader.antlr.ConvexParser.SingleFormContext; -import convex.core.lang.reader.antlr.ConvexParser.SpecialLiteralContext; -import convex.core.lang.reader.antlr.ConvexParser.StringContext; -import convex.core.lang.reader.antlr.ConvexParser.SymbolContext; -import convex.core.lang.reader.antlr.ConvexParser.SyntaxContext; -import convex.core.lang.reader.antlr.ConvexParser.VectorContext; -import convex.core.util.Utils; - -public class AntlrReader { - - public static class CRListener implements ConvexListener { - ArrayList> stack=new ArrayList<>(); - - public CRListener() { - stack.add(new ArrayList<>()); - } - - public void push(ACell a) { - int n=stack.size()-1; - ArrayList top=stack.get(n); - top.add(a); - } - - public ACell pop() { - int n=stack.size()-1; - ArrayList top=stack.get(n); - int c=top.size()-1; - ACell cell=top.get(c); - top.remove(c); - return cell; - } - - - private void pushList() { - stack.add(new ArrayList<>()); - } - - public ArrayList popList() { - int n=stack.size()-1; - ArrayList top=stack.get(n); - stack.remove(n); - return top; - } - - @Override - public void visitTerminal(TerminalNode node) { - // Nothing to do - } - - @Override - public void visitErrorNode(ErrorNode node) { - throw new ParseException(node.getSourceInterval()+" "+node.getText()); - } - - @Override - public void enterEveryRule(ParserRuleContext ctx) { - // Nothing to do - } - - @Override - public void exitEveryRule(ParserRuleContext ctx) { - // Nothing to do - } - - @Override - public void enterForm(FormContext ctx) { - // Nothing to do - } - - @Override - public void exitForm(FormContext ctx) { - // Nothing to do - } - - @Override - public void enterForms(FormsContext ctx) { - // We add a new ArrayList to the stack to capture values - pushList(); - } - - @Override - public void exitForms(FormsContext ctx) { - // Nothing to do - } - - @Override - public void enterDataStructure(DataStructureContext ctx) { - // Nothing to do - } - - @Override - public void exitDataStructure(DataStructureContext ctx) { - // Nothing to do - } - - @Override - public void enterList(ListContext ctx) { - // Nothing to do - } - - @Override - public void exitList(ListContext ctx) { - ArrayList elements=popList(); - push(Lists.create(elements)); - } - - @Override - public void enterVector(VectorContext ctx) { - // Nothing to do - } - - @Override - public void exitVector(VectorContext ctx) { - ArrayList elements=popList(); - push(Vectors.create(elements)); - } - - @Override - public void enterSet(SetContext ctx) { - // Nothing to do - } - - @Override - public void exitSet(SetContext ctx) { - ArrayList elements=popList(); - push(Sets.fromCollection(elements)); - } - - @Override - public void enterMap(MapContext ctx) { - // Nothing to do - } - - @Override - public void exitMap(MapContext ctx) { - ArrayList elements=popList(); - if (Utils.isOdd(elements.size())) { - throw new ParseException("Map requires an even number form forms."); - } - push(Maps.create(elements.toArray(new ACell[elements.size()]))); - } - - @Override - public void enterLiteral(LiteralContext ctx) { - // Nothing to do - } - - @Override - public void exitLiteral(LiteralContext ctx) { - // Nothing to do - } - - @Override - public void enterLongValue(LongValueContext ctx) { - // Nothing to do - } - - @Override - public void exitLongValue(LongValueContext ctx) { - String s=ctx.getText(); - // System.out.println(s); - push( CVMLong.parse(s)); - } - - @Override - public void enterDoubleValue(DoubleValueContext ctx) { - // Nothing to do - } - - @Override - public void exitDoubleValue(DoubleValueContext ctx) { - String s=ctx.getText(); - push( CVMDouble.parse(s)); - } - - @Override - public void enterNil(NilContext ctx) { - // Nothing to do - } - - @Override - public void exitNil(NilContext ctx) { - push(null); - } - - @Override - public void enterBool(BoolContext ctx) { - // Nothing to do - } - - @Override - public void exitBool(BoolContext ctx) { - push(CVMBool.parse(ctx.getText())); - } - - @Override - public void enterCharacter(CharacterContext ctx) { - // TODO Auto-generated method stub - - } - - @Override - public void exitCharacter(CharacterContext ctx) { - String s=ctx.getText(); - CVMChar c=CVMChar.parse(s); - if (c==null) throw new ParseException("Bad character literal format: "+s); - push(c); - } - - @Override - public void enterKeyword(KeywordContext ctx) { - // Nothing to do - } - - @Override - public void exitKeyword(KeywordContext ctx) { - String s=ctx.getText(); - Keyword k=Keyword.create(s.substring(1)); - if (k==null) throw new ParseException("Bad keyword format: "+s); - push( k); - } - - @Override - public void enterSymbol(SymbolContext ctx) { - // Nothing to do - } - - @Override - public void exitSymbol(SymbolContext ctx) { - String s=ctx.getText(); - Symbol sym=Symbol.create(s); - if (sym==null) throw new ParseException("Bad keyword format: "+s); - push( sym); - } - - @Override - public void enterAddress(AddressContext ctx) { - // Nothing to do - } - - @Override - public void exitAddress(AddressContext ctx) { - String s=ctx.getText(); - push (Address.parse(s)); - } - - @Override - public void enterSyntax(SyntaxContext ctx) { - // add new list to collect [syntax, value] - pushList(); - } - - - @Override - public void exitSyntax(SyntaxContext ctx) { - ArrayList elements=popList(); - if (elements.size()!=2) throw new ParseException("Metadata requires metadata and annotated form but got:"+ elements); - AHashMap meta=ReaderUtils.interpretMetadata(elements.get(0)); - ACell value=elements.get(1); - push(Syntax.create(value, meta)); - } - - @Override - public void enterBlob(BlobContext ctx) { - // Nothing to do - - } - - @Override - public void exitBlob(BlobContext ctx) { - String s=ctx.getText(); - Blob b=Blob.fromHex(s.substring(2)); - if (b==null) throw new ParseException("Invalid Blob syntax: "+s); - push(b); - } - - @Override - public void enterQuoted(QuotedContext ctx) { - // Nothing to do - } - - @Override - public void exitQuoted(QuotedContext ctx) { - ACell form=pop(); - String qs=ctx.getStart().getText(); - Symbol qsym=ReaderUtils.getQuotingSymbol(qs); - if (qsym==null) throw new ParseException("Invalid quoting reader macro: "+qs); - push(Lists.of(qsym,form)); - } - - @Override - public void enterString(StringContext ctx) { - // Nothing to do - } - - @Override - public void exitString(StringContext ctx) { - String s=ctx.getText(); - int n=s.length(); - s=s.substring(1, n-1); // skip surrounding double quotes - s=ReaderUtils.unescapeString(s); - push(Strings.create(s)); - } - - @Override - public void enterSpecialLiteral(SpecialLiteralContext ctx) { - // Nothing to do - } - - @Override - public void exitSpecialLiteral(SpecialLiteralContext ctx) { - pop(); // pop the symbol - String s=ctx.getText(); - ACell special=ReaderUtils.specialLiteral(s); - if (special==null) throw new ParseException("Invalid special literal: "+s); - push(special); - } - - @Override - public void enterCommented(CommentedContext ctx) { - // make a dummy list, doesn't matter what goes in here - pushList(); - } - - @Override - public void exitCommented(CommentedContext ctx) { - // remove commented form - popList(); - } - - @Override - public void enterPathSymbol(PathSymbolContext ctx) { - // Nothing - } - - @Override - public void exitPathSymbol(PathSymbolContext ctx) { - String matchString=ctx.getText(); - String[] ss=matchString.split("/",-1); // negative limit keeps empty values in cases like `#0//` - int n=ss.length; - if (n<2) { - throw new ParseException("Expected followed by symbol but got: ["+ matchString+"]"); - } - - ACell lookup=(ss[0].startsWith("#"))?Address.parse(ss[0]):Symbol.create(ss[0]);; - if (lookup==null) throw new ParseException("Path must start with Addres or Symbol"); - - for (int i=1; i top=visitor.popList(); - if (top.size()!=1) { - throw new ParseException("Bad parse output: "+top); - } - - return top.get(0); - } - - public static AList readAll(String source) { - return readAll(CharStreams.fromString(source)); - } - - public static AList readAll(CharStream cs) { - ConvexLexer lexer=new ConvexLexer(cs); - lexer.removeErrorListeners(); - CommonTokenStream tokens = new CommonTokenStream(lexer); - ConvexParser parser = new ConvexParser(tokens); - parser.removeErrorListeners(); - ParseTree tree = parser.forms(); - - CRListener visitor=new CRListener(); - ParseTreeWalker.DEFAULT.walk(visitor, tree); - - ArrayList top=visitor.popList(); - return Lists.create(top); - } - -} diff --git a/convex-core/src/main/java/convex/core/lang/reader/ReaderUtils.java b/convex-core/src/main/java/convex/core/lang/reader/ReaderUtils.java deleted file mode 100644 index e5ede368b..000000000 --- a/convex-core/src/main/java/convex/core/lang/reader/ReaderUtils.java +++ /dev/null @@ -1,79 +0,0 @@ -package convex.core.lang.reader; - -import java.util.HashMap; - -import org.apache.commons.text.StringEscapeUtils; - -import convex.core.data.ACell; -import convex.core.data.AHashMap; -import convex.core.data.AMap; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.Maps; -import convex.core.data.Symbol; -import convex.core.data.Syntax; -import convex.core.data.prim.CVMChar; -import convex.core.data.prim.CVMDouble; -import convex.core.lang.Symbols; - -public class ReaderUtils { - - /** - * Converts a metadata object according to the following rule: - Map -> - * unchanged - Keyword -> {:keyword true} - Any other expression -> {:tag - * expression} - * - * @param metaNode Syntax node containing metadata - * @return Metadata map - */ - @SuppressWarnings("unchecked") - public static AHashMap interpretMetadata(ACell metaNode) { - ACell val = Syntax.unwrapAll(metaNode); - if (val instanceof AMap) return (AHashMap) val; - if (val instanceof Keyword) return Maps.of(val, Boolean.TRUE); - return Maps.of(Keywords.TAG, val); - } - - private static final HashMap specialCharacters=Maps.hashMapOf( - "newline",CVMChar.create('\n'), - "space",CVMChar.create(' '), - "tab",CVMChar.create('\t'), - "formfeed",CVMChar.create('\f'), - "backspace",CVMChar.create('\b'), - "return",CVMChar.create('\r') - ); - - public static CVMChar specialCharacter(String s) { - return specialCharacters.get(s); - } - - private static final HashMap quotingSymbols=Maps.hashMapOf( - "'",Symbols.QUOTE, - "`",Symbols.QUASIQUOTE, - "~",Symbols.UNQUOTE, - "~@",Symbols.UNQUOTE_SPLICING - ); - - public static Symbol getQuotingSymbol(String s) { - return quotingSymbols.get(s); - } - - public static String unescapeString(String s) { - return StringEscapeUtils.unescapeJava(s); - } - - public static String escapeString(String s) { - return StringEscapeUtils.escapeJava(s); - } - - private static final HashMap specialLiterals=Maps.hashMapOf( - "##NaN",CVMDouble.NaN, - "##Inf",CVMDouble.POSITIVE_INFINITY, - "##-Inf",CVMDouble.NEGATIVE_INFINITY - ); - - public static ACell specialLiteral(String s) { - return specialLiterals.get(s); - } - -} diff --git a/convex-core/src/main/java/convex/core/store/AStore.java b/convex-core/src/main/java/convex/core/store/AStore.java deleted file mode 100644 index d30d03364..000000000 --- a/convex-core/src/main/java/convex/core/store/AStore.java +++ /dev/null @@ -1,115 +0,0 @@ -package convex.core.store; - -import java.io.IOException; -import java.util.function.Consumer; - -import convex.core.data.ABlob; -import convex.core.data.ACell; -import convex.core.data.Format; -import convex.core.data.Hash; -import convex.core.data.Ref; -import convex.core.exceptions.BadFormatException; - -/** - * Abstract base class for object storage subsystems - * - * "The perfect kind of architecture decision is the one which never has to be - * made" ― Robert C. Martin - * - */ -public abstract class AStore { - - /** - * Stores a @Ref in long term storage as defined by this store implementation. - * - * Will store nested Refs if required. - * - * Does not store embedded values. If it is necessary to persist an embedded value - * deliberately in the store, use storeTopRef(...) instead. - * - * If the persisted Ref represents novelty (i.e. not previously stored) Will - * call the provided noveltyHandler. - * - * @param ref A Ref to the given object. Should be either Direct or STORED at - * minimum to present risk of MissingDataException. - * @param status Status to store at - * @param noveltyHandler Novelty Handler function for Novelty detected. May be null. - * @return The persisted Ref, of status STORED at minimum - */ - public abstract Ref storeRef(Ref ref, int status,Consumer> noveltyHandler); - - /** - * Stores a top level @Ref in long term storage as defined by this store implementation. - * - * Will store nested Refs if required. - * - * Will only store an embedded Ref if it is the top level item. - * - * If the persisted Ref represents novelty (i.e. not previously stored) Will - * call the provided noveltyHandler - * - * @param ref A Ref to the given object. Should be either Direct or STORED at - * minimum to present risk of MissingDataException. - * @param status Status to store at - * @param noveltyHandler Novelty Handler function for Novelty detected. May be null. - * @return The persisted Ref, of status STORED at minimum - */ - public abstract Ref storeTopRef(Ref ref, int status,Consumer> noveltyHandler); - - - /** - * Gets the stored Ref for a given hash value, or null if not found. - * - * If the result is non-null, the Ref will have a status equal to STORED at minimum. - * Calls to Ref.getValue() should therefore never throw MissingDataException. - * - * @param hash A hash value to look up in the persisted store - * @return The stored Ref, or null if the hash value is not persisted - */ - public abstract Ref refForHash(Hash hash); - - /** - * Gets the Root Hash from the Store. Root hash is typically used to store the Peer state - * in situations where the Peer needs to be restored from persistent storage. - * - * @return Root hash value from this store. - * @throws IOException In case of store IO error - */ - public abstract Hash getRootHash() throws IOException; - - /** - * Sets the root hash for this Store - * @param h Root Hash to set - * @throws IOException In case of store IO error - */ - public abstract void setRootHash(Hash h) throws IOException; - - /** - * Closes this store and frees associated resources - */ - public abstract void close(); - - protected final BlobCache blobCache=BlobCache.create(100000); - - /** - * Decodes a Cell from an Encoding. Looks up Cell in cache if available. Otherwise - * equivalent to Format.read(Blob). - * @param encoding Encoding of Cell - * @return Decoded Cell (may be a a null value) - * - * @throws BadFormatException If cell encoding is invalid - */ - public final ACell decode(ABlob encoding) throws BadFormatException { - ACell cached=blobCache.getCell(encoding); - if (cached!=null) return cached; - - ACell decoded=Format.read(encoding.toBlob()); - if (decoded==null) return decoded; // handle null value - - // TODO: can remove this check once happy with all tests - assert(decoded.cachedEncoding()!=null); - blobCache.putCell(decoded); - - return decoded; - } -} diff --git a/convex-core/src/main/java/convex/core/store/BlobCache.java b/convex-core/src/main/java/convex/core/store/BlobCache.java deleted file mode 100644 index 3ab5a1717..000000000 --- a/convex-core/src/main/java/convex/core/store/BlobCache.java +++ /dev/null @@ -1,66 +0,0 @@ -package convex.core.store; - -import java.lang.ref.SoftReference; - -import convex.core.data.ABlob; -import convex.core.data.ACell; - -/** - * In-memory cache for Blob decoding. Should be used in the context of a specific Store - */ -public final class BlobCache { - - private SoftReference[] cache; - private int size; - - @SuppressWarnings("unchecked") - private BlobCache(int size) { - this.size=size; - this.cache=new SoftReference[size]; - }; - - public static BlobCache create(int size) { - return new BlobCache(size); - } - - int getSize() { - return size; - } - - /** - * Gets the Cached Cell for a given Blob Encoding, or null if not cached. - * @param encoding Encoding of Cell to look up in cache - * @return Cached Cell, or null if not found - */ - public ACell getCell(ABlob encoding) { - int ix=calcIndex(encoding); - SoftReference ref=cache[ix]; - if (ref==null) return null; - ACell cell=ref.get(); - if (cell!=null) { - if (encoding.equals(cell.getEncoding())) { - return cell; - } - return null; // cached value not the same as this encoding - } - cache[ix]=null; - return null; - } - - /** - * Stores a cell in the cache - * @param cell Cell to store - */ - public void putCell(ACell cell) { - int ix=calcIndex(cell.getEncoding()); - cache[ix]=new SoftReference<>(cell); - } - - private int calcIndex(ABlob encoding) { - int hash=Long.hashCode(encoding.toLong()); - int ix=Math.floorMod(hash, size); - return ix; - } - - -} diff --git a/convex-core/src/main/java/convex/core/store/MemoryStore.java b/convex-core/src/main/java/convex/core/store/MemoryStore.java deleted file mode 100644 index 2912a2f87..000000000 --- a/convex-core/src/main/java/convex/core/store/MemoryStore.java +++ /dev/null @@ -1,108 +0,0 @@ -package convex.core.store; - -import java.io.IOException; -import java.util.HashMap; -import java.util.function.Consumer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -import convex.core.data.ACell; -import convex.core.data.Hash; -import convex.core.data.Ref; -import convex.core.util.Utils; - -/** - * Class implementing caching and storage of hashed node data - * - * Persists refs as direct refs, i.e. retains fully in memory - */ -public class MemoryStore extends AStore { - public static final MemoryStore DEFAULT = new MemoryStore(); - - private static final Logger log = LoggerFactory.getLogger(MemoryStore.class.getName()); - - /** - * Storage of persisted Refs for each hash value - */ - private final HashMap> hashRefs = new HashMap>(); - - private Hash rootHash; - - @Override - @SuppressWarnings("unchecked") - public Ref refForHash(Hash hash) { - Ref ref = (Ref) hashRefs.get(hash); - return ref; - } - - @Override - public Ref storeRef(Ref r2, int status, Consumer> noveltyHandler) { - return persistRef(r2,noveltyHandler,status,false); - } - - @Override - public Ref storeTopRef(Ref ref, int status,Consumer> noveltyHandler) { - return persistRef(ref,noveltyHandler,status,true); - } - - @SuppressWarnings("unchecked") - public Ref persistRef(Ref ref, Consumer> noveltyHandler, int requiredStatus, boolean topLevel) { - // Convert to direct Ref. Don't want to store a soft ref! - ref = ref.toDirect(); - - final T o=ref.getValue(); - if (o==null) return (Ref) Ref.NULL_VALUE; - - ACell cell = (ACell) o; - boolean embedded=cell.isEmbedded(); - - Hash hash=null; - if (!embedded) { - // check store for existing ref first. Return this is we have it - hash = ref.getHash(); - Ref existing = refForHash(hash); - if ((existing != null)) { - if (existing.getStatus()>=requiredStatus) return existing; - ref=existing; - } - } - - // need to do recursive persistence - cell = cell.updateRefs(r -> { - return r.persist(noveltyHandler); - }); - - ref=ref.withValue((T)cell); - final ACell oTemp=cell; - - if (topLevel||!embedded) { - // Persist at top level - final Hash fHash = (hash!=null)?hash:ref.getHash(); - if (log.isTraceEnabled()) { - log.trace("Persisting ref 0x"+fHash.toHexString()+" of class "+Utils.getClassName(oTemp)+" with store "+this); - } - - hashRefs.put(fHash, (Ref) ref); - if (noveltyHandler != null) noveltyHandler.accept((Ref) ref); - } - return ref.withMinimumStatus(requiredStatus); - } - - @Override - public Hash getRootHash() throws IOException { - return rootHash; - } - - @Override - public void setRootHash(Hash h) { - rootHash=h; - } - - @Override - public void close() { - hashRefs.clear(); - rootHash=null; - } -} diff --git a/convex-core/src/main/java/convex/core/store/Stores.java b/convex-core/src/main/java/convex/core/store/Stores.java deleted file mode 100644 index d0221268d..000000000 --- a/convex-core/src/main/java/convex/core/store/Stores.java +++ /dev/null @@ -1,70 +0,0 @@ -package convex.core.store; - -import etch.EtchStore; - -public class Stores { - - // Default store - private static AStore defaultStore=null; - - // Configured global store - private static AStore globalStore=null; - - // Thread local current store, in case servers want different stores - private static final ThreadLocal currentStore = new ThreadLocal<>() { - @Override - protected AStore initialValue() { - return getGlobalStore(); - } - }; - - /** - * Gets the current (thread-local) Store instance. This is initialised to be the - * global store, but can be changed with Stores.setCurrent(...) - * - * @return Store for the current thread - */ - public static AStore current() { - return Stores.currentStore.get(); - } - - /** - * Sets the current thread-local store for this thread - * - * @param store Any AStore instance - */ - public static void setCurrent(AStore store) { - currentStore.set(store); - } - - private synchronized static AStore getDefaultStore() { - if (defaultStore==null) { - defaultStore=EtchStore.createTemp("convex-db");; - } - return defaultStore; - } - - /** - * Gets the global store instance. If not previously set, a default temporary - * store will be created and used as the global store. - * - * @return Current global store - */ - public static AStore getGlobalStore() { - if (globalStore==null) { - globalStore=getDefaultStore(); - } - return globalStore; - } - - /** - * Sets the global store for this JVM. Global store is the store used for - * any new thread. - * - * @param store Store instance to use as global store - */ - public static void setGlobalStore(EtchStore store) { - if (store==null) throw new IllegalArgumentException("Cannot set global store to null)"); - globalStore=store; - } -} diff --git a/convex-core/src/main/java/convex/core/transactions/ATransaction.java b/convex-core/src/main/java/convex/core/transactions/ATransaction.java deleted file mode 100644 index 47d1b949e..000000000 --- a/convex-core/src/main/java/convex/core/transactions/ATransaction.java +++ /dev/null @@ -1,118 +0,0 @@ -package convex.core.transactions; - -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.data.Format; -import convex.core.data.type.AType; -import convex.core.data.type.Transaction; -import convex.core.lang.Context; - -/** - * Abstract base class for immutable transactions - * - * Transactions may modify the on-chain State according to the rules of the - * specific transaction type. When applied to a State, a transaction must - * produce either: a) A valid updated State b) A TransactionException - * - * Any other class of exception should be regarded as a serious failure, - * indicating a code error or system integrity issue. - * - */ -public abstract class ATransaction extends ACell { - protected final Address address; - protected final long sequence; - - protected ATransaction(Address address, long sequence) { - if (address==null) throw new ClassCastException("Null Address for transaction"); - this.address=address; - this.sequence = sequence; - } - - /** - * Writes this transaction to a byte array, including the message tag - */ - @Override - public abstract int encode(byte[] bs, int pos); - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = Format.writeVLCLong(bs,pos, address.longValue()); - pos = Format.writeVLCLong(bs,pos, sequence); - return pos; - } - - @Override - public abstract int estimatedEncodingSize(); - - /** - * Applies the functional effect of this transaction to the current state. - * - * Important points: - *
    - *
  • Assumes all relevant accounting preparation already complete, including juice reservation
  • - *
  • Performs complete state update (including any rollbacks from errors)
  • - *
  • Produces result, which may be exceptional
  • - *
  • Does not finalise memory/juice accounting (will be completed afterwards)
  • - *
- * - * @param ctx Context for which to apply this Transaction - * @return The updated chain state - */ - public abstract Context apply(Context ctx); - - /** - * Gets the *origin* Address for this transaction - * @return Address for this Transaction - */ - public Address getAddress() { - return address; - } - - public final long getSequence() { - return sequence; - } - - - - /** - * Gets the max juice allowed for this transaction - * - * @return Juice limit - */ - public abstract Long getMaxJuice(); - - @Override public final boolean isCVMValue() { - // Transactions exist outside CVM only - return false; - } - - @Override - public AType getType() { - return Transaction.INSTANCE; - } - - /** - * Updates this transaction with the specified sequence number - * @param newSequence New sequence number - * @return Updated transaction, or this transaction if the sequence number is unchanged. - */ - public abstract ATransaction withSequence(long newSequence); - - /** - * Updates this transaction with the specified address - * @param newAddress New address - * @return Updated transaction, or this transaction if unchanged. - */ - public abstract ATransaction withAddress(Address newAddress); - - @Override - public boolean isCanonical() { - return true; - } - - @Override - public ACell toCanonical() { - return this; - } - -} diff --git a/convex-core/src/main/java/convex/core/transactions/Call.java b/convex-core/src/main/java/convex/core/transactions/Call.java deleted file mode 100644 index a1453c847..000000000 --- a/convex-core/src/main/java/convex/core/transactions/Call.java +++ /dev/null @@ -1,144 +0,0 @@ -package convex.core.transactions; - -import java.nio.ByteBuffer; - -import convex.core.Constants; -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.Address; -import convex.core.data.Format; -import convex.core.data.IRefFunction; -import convex.core.data.Ref; -import convex.core.data.Symbol; -import convex.core.data.Tag; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.Context; -import convex.core.util.Utils; - -/** - * Transaction representing a Call to an Actor. - * - * The signer of the transaction will be both the *origin* and *caller* for the Actor code. - * - * This is the most efficient way to execute Actor code directly as a client, and is roughly equivalent to invoking - * (call actor offer (function-name arg1 arg2 .....)) - */ -public class Call extends ATransaction { - - protected final Address target; - protected final long offer; - protected final Symbol functionName; - protected final AVector args; - - - - protected Call(Address address, long sequence, Address target, long offer,Symbol functionName,AVector args) { - super(address,sequence); - this.target=target; - this.functionName=functionName; - this.offer=offer; - this.args=args; - } - - public static Call create(Address address, long sequence, Address target, long offer,Symbol functionName,AVector args) { - return new Call(address,sequence,target,0,functionName,args); - } - - - public static Call create(Address address, long sequence, Address target, Symbol functionName,AVector args) { - return create(address,sequence,target,0,functionName,args); - } - - @Override - public void print(StringBuilder sb) { - sb.append("{"); - sb.append(":target "); - Utils.print(sb, target); - if (offer>0) { - sb.append(" :offer "); - sb.append(offer); - } - sb.append('}'); - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++] = Tag.CALL; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = super.encodeRaw(bs,pos); // sequence - pos = Format.write(bs,pos, target); - pos=Format.writeVLCLong(bs,pos, offer); - pos=Format.write(bs,pos, functionName); - pos=Format.write(bs,pos, args); - return pos; - } - - public static ATransaction read(ByteBuffer bb) throws BadFormatException { - Address address=Address.create(Format.readVLCLong(bb)); - long sequence = Format.readVLCLong(bb); - Address target=Format.read(bb); - long offer = Format.readVLCLong(bb); - Symbol functionName=Format.read(bb); - AVector args = Format.read(bb); - return create(address,sequence, target, offer, functionName,args); - } - - @Override - public int estimatedEncodingSize() { - return 100; - } - - @Override - public Context apply(Context ctx) { - return ctx.actorCall(target, offer, functionName, args.toCellArray()); - } - - @Override - public Long getMaxJuice() { - return Constants.MAX_TRANSACTION_JUICE; - } - - @Override - public void validateCell() throws InvalidDataException { - target.validateCell(); - } - - @Override - public int getRefCount() { - return args.getRefCount(); - } - - @Override - public Ref getRef(int i) { - return args.getRef(i); - } - - @Override - public ACell updateRefs(IRefFunction func) { - AVector newArgs=args.updateRefs(func); - if (args==newArgs) return this; - return new Call(address,sequence,target,offer,functionName,newArgs); - } - - @Override - public Call withSequence(long newSequence) { - if (newSequence==this.sequence) return this; - return create(address,newSequence,target,offer,functionName,args); - } - - @Override - public Call withAddress(Address newAddress) { - if (newAddress==this.address) return this; - return create(newAddress,sequence,target,offer,functionName,args); - } - - @Override - public byte getTag() { - return Tag.CALL; - } -} diff --git a/convex-core/src/main/java/convex/core/transactions/Invoke.java b/convex-core/src/main/java/convex/core/transactions/Invoke.java deleted file mode 100644 index 3538af418..000000000 --- a/convex-core/src/main/java/convex/core/transactions/Invoke.java +++ /dev/null @@ -1,173 +0,0 @@ -package convex.core.transactions; - -import java.nio.ByteBuffer; - -import convex.core.Constants; -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.data.Format; -import convex.core.data.IRefFunction; -import convex.core.data.Ref; -import convex.core.data.Tag; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.Reader; -import convex.core.util.Utils; - -/** - * Transaction class representing the Invoke of an on-chain operation. - * - * The command provided may be specified as either: - *
    - *
  • A Form (will be compiled and executed)
  • - *
  • A pre-compiled Op (will be executed directly, cheaper)
  • - *
- * - * Peers may separately implement functionality to parse and compile a command provided as a String: this must be - * performed outside the CVM which not provide a parser internally. - */ -public class Invoke extends ATransaction { - protected final ACell command; - - protected Invoke(Address address,long sequence, ACell args) { - super(address,sequence); - this.command = args; - } - - public static Invoke create(Address address,long sequence, ACell command) { - return new Invoke(address,sequence, command); - } - - /** - * Creates an Invoke transaction - * @param address Address of origin Account - * @param sequence Sequence number - * @param command Command as a string, which will be read as Convex Lisp code - * @return New Invoke transaction instance - */ - public static Invoke create(Address address,long sequence, String command) { - return create(address,sequence, Reader.read(command)); - } - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++] = Tag.INVOKE; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = super.encodeRaw(bs,pos); // nonce, address - pos = Format.write(bs,pos, command); - return pos; - } - - /** - * Get the command for this transaction, as code. - * @return Command object. - */ - public Object getCommand() { - return command; - } - - /** - * Read a Transfer transaction from a ByteBuffer - * - * @param bb ByteBuffer containing the transaction - * @throws BadFormatException if the data is invalid - * @return The Transfer object - */ - public static Invoke read(ByteBuffer bb) throws BadFormatException { - Address address=Address.create(Format.readVLCLong(bb)); - long sequence = Format.readVLCLong(bb); - - ACell args = Format.read(bb); - return create(address,sequence, args); - } - - @SuppressWarnings("unchecked") - @Override - public Context apply(final Context context) { - Context ctx=(Context) context; - - // Run command - if (command instanceof AOp) { - ctx = ctx.run((AOp) command); - } else { - ctx = ctx.run(command); - } - return (Context) ctx; - } - - @Override - public int estimatedEncodingSize() { - // tag (1), nonce(<12) and target (33) - // plus allowance for Amount - return 1 + 12 + Format.MAX_EMBEDDED_LENGTH + Format.MAX_VLC_LONG_LENGTH; - } - - @Override - public boolean isCanonical() { - return true; - } - - @Override - public void print(StringBuilder sb) { - sb.append("{"); - sb.append(":invoke "); - Utils.print(sb, command); - sb.append('}'); - } - - @Override - public void validateCell() throws InvalidDataException { - if (command instanceof AOp) { - // OK? - ((AOp) command).validateCell(); - } else { - if (!Format.isCanonical(command)) throw new InvalidDataException("Non-canonical object as command?", this); - } - } - - @Override - public Long getMaxJuice() { - // TODO make this a field - return Constants.MAX_TRANSACTION_JUICE; - } - - @Override - public int getRefCount() { - return Utils.refCount(command); - } - - @Override - public Ref getRef(int i) { - return Utils.getRef(command, i); - } - - @Override - public Invoke updateRefs(IRefFunction func) { - ACell newCommand = Utils.updateRefs(command, func); - if (newCommand == command) return this; - return Invoke.create(address,getSequence(), newCommand); - } - - @Override - public Invoke withSequence(long newSequence) { - if (newSequence==this.sequence) return this; - return create(address,newSequence,command); - } - - @Override - public Invoke withAddress(Address newAddress) { - if (newAddress==this.address) return this; - return create(newAddress,sequence,command); - } - - @Override - public byte getTag() { - return Tag.INVOKE; - } -} diff --git a/convex-core/src/main/java/convex/core/transactions/Transfer.java b/convex-core/src/main/java/convex/core/transactions/Transfer.java deleted file mode 100644 index 74c9e9c78..000000000 --- a/convex-core/src/main/java/convex/core/transactions/Transfer.java +++ /dev/null @@ -1,147 +0,0 @@ -package convex.core.transactions; - -import java.nio.ByteBuffer; - -import convex.core.Constants; -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.data.Format; -import convex.core.data.Tag; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.Context; -import convex.core.lang.Juice; -import convex.core.lang.RT; - -/** - * Transaction class representing a coin Transfer from one account to another - */ -public class Transfer extends ATransaction { - public static final long TRANSFER_JUICE = Juice.TRANSFER; - - protected final Address target; - protected final long amount; - - protected Transfer(Address address,long nonce, Address target, long amount) { - super(address,nonce); - this.target = target; - this.amount = amount; - } - - public static Transfer create(Address address,long nonce, Address target, long amount) { - return new Transfer(address,nonce, target, amount); - } - - - @Override - public int encode(byte[] bs, int pos) { - bs[pos++]=Tag.TRANSFER; - return encodeRaw(bs,pos); - } - - @Override - public int encodeRaw(byte[] bs, int pos) { - pos = super.encodeRaw(bs,pos); // nonce, address - pos = target.encodeRaw(bs,pos); - pos = Format.writeVLCLong(bs, pos, amount); - return pos; - } - - /** - * Read a Transfer transaction from a ByteBuffer - * - * @param bb ByteBuffer containing the transaction - * @throws BadFormatException if the data is invalid - * @return The Transfer object - */ - public static Transfer read(ByteBuffer bb) throws BadFormatException { - Address address=Address.create(Format.readVLCLong(bb)); - long nonce = Format.readVLCLong(bb); - Address target = Address.readRaw(bb); - long amount = Format.readVLCLong(bb); - if (!RT.isValidAmount(amount)) throw new BadFormatException("Invalid amount: "+amount); - return create(address,nonce, target, amount); - } - - @SuppressWarnings("unchecked") - @Override - public Context apply(Context ctx) { - // consume juice, ensure we have enough to make transfer! - ctx = ctx.consumeJuice(Juice.TRANSFER); - if (!ctx.isExceptional()) { - ctx = ctx.transfer(target, amount); - } - return (Context) ctx; - } - - @Override - public int estimatedEncodingSize() { - // tag (1), nonce(<12) and target (33) - // plus allowance for Amount - return 1 + 12 + 33 + Format.MAX_VLC_LONG_LENGTH; - } - - @Override - public boolean isCanonical() { - return true; - } - - @Override - public void print(StringBuilder sb) { - sb.append("{"); - sb.append(":transfer-to "); - target.print(sb); - sb.append(','); - sb.append(":amount "+amount); - sb.append('}'); - } - - @Override - public void validateCell() throws InvalidDataException { - if ((amount<0)||(amount>Constants.MAX_SUPPLY)) throw new InvalidDataException("Invalid amount", this); - if (target == null) throw new InvalidDataException("Null Address", this); - } - - /** - * Gets the target address for this transfer - * @return Address of the destination for this transfer. - */ - public Address getTarget() { - return target; - } - - /** - * Gets the transfer amount for this transaction. - * @return Amount of transfer, as a long - */ - public long getAmount() { - return amount; - } - - @Override - public Long getMaxJuice() { - return Juice.TRANSFER; - } - - @Override - public int getRefCount() { - return 0; - } - - @Override - public Transfer withSequence(long newSequence) { - if (newSequence==this.sequence) return this; - return create(address,newSequence,target,amount); - } - - @Override - public Transfer withAddress(Address newAddress) { - if (newAddress==this.address) return this; - return create(newAddress,sequence,target,amount); - } - - @Override - public byte getTag() { - return Tag.TRANSFER; - } -} diff --git a/convex-core/src/main/java/convex/core/util/Bits.java b/convex-core/src/main/java/convex/core/util/Bits.java deleted file mode 100644 index b485580f1..000000000 --- a/convex-core/src/main/java/convex/core/util/Bits.java +++ /dev/null @@ -1,106 +0,0 @@ -package convex.core.util; - -/** - * Static utility function for bitwise functions - */ -public class Bits { - - /** - * Returns the index from the present mask for the given hex digit (0-15), or -1 - * if not found - * - * @param digit Hex digit (0-15) - * @param mask Bitmask of hex digits - * @return The index of the appropriate child for this digit, or -1 if not found - */ - public static int indexForDigit(int digit, short mask) { - // check if digit is present in mask - if ((mask & (1 << digit)) == 0) return -1; - - // get the position of this digit (which must be present due to previous check) - return positionForDigit(digit, mask); - } - - /** - * Returns the array position for a given digit given a current mask. If not - * present, this is where the new array entry must be inserted. - * - * @param digit Hex digit (0-15) - * @param mask Bitmask of hex digits - * @return Array position for the given digit in the specified mask - */ - public static int positionForDigit(int digit, short mask) { - // count present bits before this digit - return Integer.bitCount(mask & ((1 << digit) - 1)); - } - - /** - * Get the number of leading zeros in the binary representation of an int - * @param x int value to check - * @return Number of leading zeros (0-32) - */ - public static int leadingZeros(int x) { - if (x == 0) return 32; - int result = 0; - if ((x & 0xFFFF0000) == 0) { - result += 16; - } else { - x >>>= 16; - } - if ((x & 0xFF00) == 0) { - result += 8; - } else { - x >>>= 8; - } - if ((x & 0xF0) == 0) { - result += 4; - } else { - x >>>= 4; - } - if ((x & 0xC) == 0) { - result += 2; - } else { - x >>>= 2; - } - if ((x & 0x2) == 0) { - result += 1; - } else { - x >>>= 1; - } - return result; - } - - /** - * Get the number of leading zeros in the binary representation of a long - * @param x long value to check - * @return Number of leading zeros (0-64) - */ - public static int leadingZeros(long x) { - int highWord = (int) (x >>> 32); // high 4 bytes, unsigned - if (highWord != 0) return leadingZeros(highWord); - int lowWord = (int) (x); - return 32 + leadingZeros(lowWord); - } - - /** - * Gets a bit mask for the specified number of low bits in an int - * - * @param numBits Number of bits to set to 1 - * @return int containing the specified number of set low bits - */ - public static int lowBitMask(int numBits) { - return (1 << numBits) - 1; - } - - /** - * Gets the specified number of low Bits in an integer. Other bits are zeroed. - * - * @param numBits Number of bits to get - * @param val Value to extract bits from - * @return Masked in with the specified number of low bits - */ - public static int lowBits(int numBits, int val) { - return val & lowBitMask(numBits); - } - -} diff --git a/convex-core/src/main/java/convex/core/util/Counters.java b/convex-core/src/main/java/convex/core/util/Counters.java deleted file mode 100644 index e621e08de..000000000 --- a/convex-core/src/main/java/convex/core/util/Counters.java +++ /dev/null @@ -1,25 +0,0 @@ -package convex.core.util; - -/** - * Some event counters, for debugging and general metrics - */ -public class Counters { - - public static volatile long sendCount = 0; - public static volatile long beliefMerge = 0; - public static volatile long applyBlock = 0; - - public static volatile long etchRead = 0; - public static volatile long etchWrite = 0; - public static volatile long etchMiss =0; - - public String getStats() { - StringBuffer sb=new StringBuffer(); - - sb.append("Etch writes: "+etchWrite); - sb.append("Etch reads: "+etchRead); - sb.append("Etch hit(%): "+Text.toPercentString(100.0*(etchRead-etchMiss)/etchRead)); - - return sb.toString(); - } -} diff --git a/convex-core/src/main/java/convex/core/util/Economics.java b/convex-core/src/main/java/convex/core/util/Economics.java deleted file mode 100644 index 7324c4065..000000000 --- a/convex-core/src/main/java/convex/core/util/Economics.java +++ /dev/null @@ -1,53 +0,0 @@ -package convex.core.util; - -import java.math.BigInteger; - -/** - * Utility function for Convex Cryptoeconomics - */ -public class Economics { - - /** - * Computes the marginal exchange rate between assets A and B with pool quantities, - * such that a constant liquidity pool c = a * b is maintained. - * - * @param a Quantity of Asset A - * @param b Quantity of Asset B - * @return Price of A in terms of B - */ - public static double swapRate(long a, long b) { - if ((a<=0)||(b<=0)) throw new IllegalArgumentException("Pool quantities must be positive"); - return (double)b/(double)a; - } - - static final BigInteger MAX_POOL_SIZE=BigInteger.valueOf(Long.MAX_VALUE); - - - /** - * Computes the smallest price for d units of Asset A in terms of units of Asset B - * such that a constant liquidity pool c = a * b is increased - * - * @param a Quantity of Asset A in Pool - * @param b Quantity of Asset B in Pool - * @param delta Quantity of Unit A to buy (negative = sell) - * @return Price of A in terms of B - */ - public static long swapPrice(long delta,long a, long b) { - if ((a<=0)||(b<=0)) throw new IllegalArgumentException("Pool quantities must be positive"); - - BigInteger c = BigInteger.valueOf(a).multiply(BigInteger.valueOf(b)); - long newA = a-delta; - if (newA<=0) throw new IllegalArgumentException("Cannot buy entire Pool"); - - BigInteger newBigA=BigInteger.valueOf(newA); - if (newBigA.compareTo(MAX_POOL_SIZE)>=0) throw new IllegalArgumentException("Can't exceed Long pool size for A"); - - BigInteger newBigB = c.divide(newBigA); - if (newBigB.compareTo(MAX_POOL_SIZE)>=0) throw new IllegalArgumentException("Can't exceed Long pool size for B"); - - // Convert back to long, add one so pool size must strictly increase - long finalB=newBigB.longValueExact()+1; - - return finalB-b; - } -} diff --git a/convex-core/src/main/java/convex/core/util/Errors.java b/convex-core/src/main/java/convex/core/util/Errors.java deleted file mode 100644 index 8e2013770..000000000 --- a/convex-core/src/main/java/convex/core/util/Errors.java +++ /dev/null @@ -1,54 +0,0 @@ -package convex.core.util; - -import convex.core.data.ARecord; -import convex.core.data.Address; -import convex.core.data.Keyword; - -/** - * Utility class for generating appropriate error messages - * - * "I keep a list of all unresolved bugs I've seen on the forum. In some cases, - * I'm still thinking about the best design for the fix. This isn't the kind of - * software where we can leave so many unresolved bugs that we need a tracker for them." - * - * – Satoshi Nakamoto - */ -public class Errors { - - public static String immutable(Object a) { - return "Object is immutable: "+a.getClass(); - } - - public static String sizeOutOfRange(long i) { - return "Index out of range: "+i; - } - - public static String illegalPosition(long position) { - return "Illegal index position: "+position; - } - - public static String insufficientFunds(Address source, long amount) { - return "Insufficient funds in account ["+source+"] required="+amount; - } - - public static String unknownKey(Keyword key, ARecord record) { - return "Unknown key ["+key+"] for record type: "+record.getClass(); - } - - public static String badIndex(long i) { - return "Bad index: "+i; - } - - public static String badRange(long start, long length) { - return "Range out of bounds with offset="+start+" and length="+length; - } - - public static String negativeLength(long length) { - return "Negative length: "+length; - } - - public static String wrongLength(long expected, long count) { - return "Wrong length, expected="+expected+" and actual="+count; - } - -} diff --git a/convex-core/src/main/java/convex/core/util/Huge.java b/convex-core/src/main/java/convex/core/util/Huge.java deleted file mode 100644 index ed23c1bf8..000000000 --- a/convex-core/src/main/java/convex/core/util/Huge.java +++ /dev/null @@ -1,137 +0,0 @@ -package convex.core.util; - -import convex.core.exceptions.TODOException; - -/** - * A 128-bit integer - */ -public class Huge { - - // Some useful constants - public static final Huge ZERO = create(0L); - public static final Huge ONE = create(1L); - - public final long hi; - public final long lo; - - private Huge(long hi,long lo) { - this.hi=hi; - this.lo=lo; - - } - - /** - * Creates a new Huge by sign extending a long to 128 bits - * @param a Any signed 64-bit long value - * @return New Huge instance - */ - public static Huge create(long a) { - return new Huge((a>=0)?0:-1,a); - } - - /** - * Creates a new Huge by multiplying two signed longs - * @param a Any signed 64-bit long value - * @param b Any signed 64-bit long value - * @return Huge product of arguments - */ - public static Huge multiply(long a, long b) { - long hi=Math.multiplyHigh(a, b); - return new Huge(hi,a*b); - } - - /** - * Creates a new Huge by multiplying a Huge with a signed long - * @param a Any signed 128-bit Huge value - * @param b Any signed 64-bit long value - * @return Huge product of arguments - */ - public static Huge multiply(Huge a, long b) { - long carry=Math.multiplyHigh(a.lo, b); - return new Huge(carry+a.hi*b,a.lo*b); - } - - /** - * Creates a Huge by adding two signed longs - * @param a Any signed 64-bit long value - * @param b Any signed 64-bit long value - * @return Huge sum of arguments - */ - public static Huge add(long a, long b) { - long carry = UMath.unsignedAddCarry(a,b); - long signSum = ((a<0)?-1:0) + ((b<0)?-1:0); - return new Huge(carry+signSum,a+b); - } - - /** - * Creates a Huge by adding a long value to this Huge - * @param b Any signed 64-bit long value - * @return Huge sum of arguments - */ - public Huge add(long b) { - long carry = UMath.unsignedAddCarry(lo,b); - long sign = ((b<0)?-1:0); - - return new Huge(hi+sign+carry,lo+b); - } - - /** - * Performs a fused multiply and divide (a * b) / c. Handles cases where (a*b) would overflow a single 64-bit long. - * - * @param a First multiplicand - * @param b Second multiplicand - * @param c Divisor - * @return Result of operation, of null if result overflows a Long - */ - public static Long fusedMultiplyDivide(long a, long b, long c) { - throw new TODOException(); - } - - /** - * Creates a Huge by adding another Huge - * @param b Any Huge value - * @return Huge sum of arguments - */ - public Huge add(Huge b) { - long carry = UMath.unsignedAddCarry(lo,b.lo); - return new Huge(hi+b.hi+carry,lo+b.lo); - } - - @Override - public boolean equals(Object a) { - if (a instanceof Huge) return equals((Huge)a); - return false; - } - - /** - * Tests if this Huge is equal to another Huge - * @param a Another Huge instance (must not be null) - * @return true is the Huge values are equal, false otherwise - */ - public boolean equals(Huge a) { - return (lo==a.lo)&&(hi==a.hi); - } - - @Override - public String toString() { - return "#huge 0x"+Utils.toHexString(hi)+Utils.toHexString(lo); - } - - public Huge sub(Huge b) { - return add(b.negate()); - } - - /** - * Negates this Huge value - * @return Huge negation of this value - */ - public Huge negate() { - return new Huge(-hi-((lo!=0L)?1L:0L),-lo); - } - - public Huge mul(Huge b) { - throw new TODOException(); - // Broken because of carrying - // return new Huge((hi*b.lo) + (lo*b.hi) + UMath.multiplyHigh(lo, b.lo),lo*b.lo); - } -} diff --git a/convex-core/src/main/java/convex/core/util/MergeFunction.java b/convex-core/src/main/java/convex/core/util/MergeFunction.java deleted file mode 100644 index 3e4777ec1..000000000 --- a/convex-core/src/main/java/convex/core/util/MergeFunction.java +++ /dev/null @@ -1,16 +0,0 @@ -package convex.core.util; - -public abstract interface MergeFunction { - - public abstract V merge(V a, V b); - - /** - * Reverse a MergeFunction so that it can be applied with opposite ordering. - * This is useful for handling merge functions that are not commutative. - * - * @return A MergeFunction that merges the arguments in the reverse order. - */ - public default MergeFunction reverse() { - return (a, b) -> merge(b, a); - } -} diff --git a/convex-core/src/main/java/convex/core/util/Shutdown.java b/convex-core/src/main/java/convex/core/util/Shutdown.java deleted file mode 100644 index 9117d90d6..000000000 --- a/convex-core/src/main/java/convex/core/util/Shutdown.java +++ /dev/null @@ -1,77 +0,0 @@ -package convex.core.util; - -import java.util.Collection; -import java.util.IdentityHashMap; -import java.util.Map; -import java.util.TreeMap; - -/** - * So the JVM doesn't give us a nice way to run shutdown hooks in a defined order. - * - * This class enables us to do just that! - */ -public class Shutdown { - - public static final int CLIENTHTTP = 60; - public static final int SERVER = 80; - public static final int ETCH = 100; - public static final int CLI = 120; - - static { - try { - Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { - @Override - public void run() { - Shutdown.runHooks(); - } - })); - } catch(IllegalStateException e) { - // Ignore, already shutting down - } - } - - private static class Group { - private final IdentityHashMap hookSet=new IdentityHashMap<>(); - - public synchronized void addHook(Runnable r) { - hookSet.put(r, r); - } - - public synchronized void runHooks() { - Collection hooks=hookSet.keySet(); - hooks.stream().forEach(r->{ - r.run(); - }); - hookSet.clear(); - } - - } - - private static final TreeMap order=new TreeMap<>(); - - /** - * Add a Runnable shutdown hook with the given priority. Lower priority numbers will - * be executed first. - * - * @param priority Priority number for shutdown hook - * @param shutdownTask Runnable instance to execute on shutdown - */ - public static synchronized void addHook(int priority,Runnable shutdownTask) { - Group g=order.get(priority); - if (g==null) { - g=new Group(); - order.put(priority, g); - } - g.addHook(shutdownTask); - } - - /** - * Execute all hooks. Called by standard Java shutdown process. - */ - private synchronized static void runHooks() { - for (Map.Entry me: order.entrySet()) { - me.getValue().runHooks(); - } - order.clear(); - } -} diff --git a/convex-core/src/main/java/convex/core/util/Text.java b/convex-core/src/main/java/convex/core/util/Text.java deleted file mode 100644 index 51a464594..000000000 --- a/convex-core/src/main/java/convex/core/util/Text.java +++ /dev/null @@ -1,71 +0,0 @@ -package convex.core.util; - -import java.text.DecimalFormat; -import java.time.Instant; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; - -public class Text { - private static final int WHITESPACE_LENGTH = 32; - private static String WHITESPACE_32 = " "; // 32 spaces - - public static String whiteSpace(int length) { - if (length < 0) throw new IllegalArgumentException("Negative whitespace requested!"); - if (length == 0) return ""; - - if (length <= WHITESPACE_LENGTH) { - return WHITESPACE_32.substring(0, length); - } - - StringBuilder sb = new StringBuilder(length); - for (int i = WHITESPACE_LENGTH; i <= length; i += WHITESPACE_LENGTH) { - sb.append(WHITESPACE_32); - } - sb.append(whiteSpace(length & 0x1F)); - return sb.toString(); - } - - public static String leftPad(String s, int length) { - if (s == null) s = ""; - int spaces = length - s.length(); - if (spaces < 0) throw new IllegalArgumentException("String [" + s + "] too long for pad length: " + length); - return whiteSpace(spaces) + s; - } - - public static String leftPad(long value, int length) { - return leftPad(Long.toString(value), length); - } - - public static String rightPad(String s, int length) { - if (s == null) s = ""; - int spaces = length - s.length(); - if (spaces < 0) throw new IllegalArgumentException("String [" + s + "] too long for pad length: " + length); - return s + whiteSpace(spaces); - } - - public static String rightPad(long value, int length) { - return rightPad(Long.toString(value), length); - } - - static DecimalFormat balanceFormatter = new DecimalFormat("#,###"); - - public static String toFriendlyBalance(long value) { - return balanceFormatter.format(value); - } - - static DecimalFormat percentFormatter = new DecimalFormat("##.###%"); - public static String toPercentString(double value) { - return percentFormatter.format(value); - } - - public static String toFriendlyBalance(double value) { - return toFriendlyBalance((long) value); - } - - static final DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendInstant(3).toFormatter(); - - public static String dateFormat(long timestamp) { - return formatter.format(Instant.ofEpochMilli(timestamp)); - } - -} diff --git a/convex-core/src/main/java/convex/core/util/UMath.java b/convex-core/src/main/java/convex/core/util/UMath.java deleted file mode 100644 index 2ee7cac58..000000000 --- a/convex-core/src/main/java/convex/core/util/UMath.java +++ /dev/null @@ -1,35 +0,0 @@ -package convex.core.util; - -/** - * Functions for unsigned maths. - * - * It would be nice if Java included these by default. - */ -public class UMath { - - /** - * Gets the high 64 bits of an unsigned multiply - * @param a 64-bit long interpreted as unsigned value - * @param b 64-bit long interpreted as unsigned value - * @return High 64 bits of unsigned multiply - */ - public static long multiplyHigh(long a, long b) { - long r=Math.multiplyHigh(a, b); - if ((a<0)^(b<0)) r=-r; - return r; - } - - /** - * Gets the carry of an unsigned addition of two longs - * - * @param a 64-bit long interpreted as unsigned value - * @param b 64-bit long interpreted as unsigned value - * @return 1 if the addition carries, 0 otherwise - */ - public static long unsignedAddCarry(long a, long b) { - boolean sa=(a<0); // high bit of a - boolean sb=(b<0); // high bit of b - return (sa&&sb)||((sa||sb)&&(!((a+b)<0))) ? 1 : 0; - } - -} diff --git a/convex-core/src/main/java/convex/core/util/Utils.java b/convex-core/src/main/java/convex/core/util/Utils.java deleted file mode 100644 index feb987abe..000000000 --- a/convex-core/src/main/java/convex/core/util/Utils.java +++ /dev/null @@ -1,1339 +0,0 @@ -package convex.core.util; - -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.lang.reflect.Array; -import java.math.BigInteger; -import java.net.InetSocketAddress; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import convex.core.Constants; -import convex.core.State; -import convex.core.data.AArrayBlob; -import convex.core.data.ABlob; -import convex.core.data.ACell; -import convex.core.data.AObject; -import convex.core.data.ASequence; -import convex.core.data.AVector; -import convex.core.data.Blob; -import convex.core.data.IRefFunction; -import convex.core.data.Ref; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMChar; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.TODOException; -import convex.core.lang.RT; - -public class Utils { - public static final byte[] EMPTY_BYTES = new byte[0]; - - /** - * Converts an array of bytes into an unsigned BigInteger - * - * Assumes big-endian format as per new BigInteger(int, byte[]); - * - * @param data Array of bytes containing an unsigned integer (big-endian) - * @return A new non-negative BigInteger - */ - public static BigInteger toBigInteger(byte[] data) { - return new BigInteger(1, data); - } - - /** - * Converts an array of bytes into a signed BigInteger - * - * Assumes two's-complement big-endian binary representation format as per new - * BigInteger(byte[]); - * - * @param data Byte array to convert to BigInteger - * @return A signed BigInteger - */ - public static BigInteger toSignedBigInteger(byte[] data) { - return new BigInteger(data); - } - - /** - * Converts an int to a hex string e.g. "80cafe80" - * - * @param val Value to convert - * @return Lowercase hex string - */ - public static String toHexString(int val) { - StringBuffer sb = new StringBuffer(8); - for (int i = 0; i < 8; i++) { - sb.append(Utils.toHexChar((val >> ((7 - i) * 4)) & 0xf)); - } - return sb.toString(); - } - - public static String toHexString(short val) { - StringBuffer sb = new StringBuffer(4); - for (int i = 0; i < 4; i++) { - sb.append(Utils.toHexChar((val >> ((3 - i) * 4)) & 0xf)); - } - return sb.toString(); - } - - /** - * Converts a byte to a two-character hex string - * - * @param value Value to convert - * @return Lowercase hex string - */ - public static String toHexString(byte value) { - StringBuffer sb = new StringBuffer(2); - sb.append(toHexChar((((int) value) & 0xF0) >>> 4)); - sb.append(toHexChar(((int) value) & 0xF)); - return sb.toString(); - } - - /** - * Converts a long value to a 16 character hex string - * - * @param x Value to convert - * @return Hex string for the given long - */ - public static String toHexString(long x) { - StringBuffer sb = new StringBuffer(16); - for (int i = 15; i >= 0; i--) { - sb.append(toHexChar(((int) (x >> (4 * i))) & 0xF)); - } - return sb.toString(); - } - - /** - * Converts a hex string to a friendly version ( first x chars). - * SECURITY; do not use this output for any comparison. - * - * @param hexString String to show in friendly format. - * @param size Number of hex chars to output. - * @return Hex String - */ - public static String toFriendlyHexString(String hexString, int size) { - String cleanHexString = hexString.replaceAll("^0[Xx]", ""); - String result = cleanHexString.substring(0, size); - // + ".." + cleanHexString.substring(cleanHexString.length() - size); - return result; - } - /** - * Reads an int from a specified location in a byte array Assumes 4-byte - * big-endian representation - * - * @param data Byte array from which to read the 4-byte int representation - * @param offset Offset into byte array to read - * @return int value from array - */ - public static int readInt(byte[] data, int offset) { - int result = data[offset]; - for (int i = 1; i <= 3; i++) { - result = (result << 8) + (data[offset + i] & 0xFF); - } - return result; - } - - public static long readLong(byte[] data, int offset) { - long result = data[offset]; - for (int i = 1; i <= 7; i++) { - result = (result << 8) + (data[offset + i] & 0xFF); - } - return result; - } - - /** - * Reads a short from a specified location in a byte array Assumes 2-byte - * big-endian representation - * - * @param data Byte array from which to read the 2-byte short representation - * @param offset Offset into byte array to read - * @return short value from array - */ - public static short readShort(byte[] data, int offset) { - int result = ((data[offset] & 0xFF) << 8) + (data[offset + 1] & 0xFF); - return (short) result; - } - - /** - * Writes an char to a byte array in 2 byte big-endian representation - * - * @param value int value to write to the array - * @param data Byte array into which to write the given int - * @param offset Offset into the array at which the int will be written - * @return Offset after writing - */ - public static int writeChar(byte[] data, int offset,char value) { - data[offset++]=(byte)(value>>8); - data[offset++]=(byte)(value); - return offset; - } - - /** - * Writes an char to a byte array in 2 byte big-endian representation - * - * @param value int value to write to the array - * @param data Byte array into which to write the given int - * @param offset Offset into the array at which the int will be written - * @return Offset after writing - */ - public static int writeShort(byte[] data, int offset,short value) { - data[offset++]=(byte)(value>>8); - data[offset++]=(byte)(value); - return offset; - } - - /** - * Writes an int to a byte array in 4 byte big-endian representation - * - * @param value int value to write to the array - * @param data Byte array into which to write the given int - * @param offset Offset into the array at which the int will be written - * @return Offset after writing - */ - public static int writeInt(byte[] data, int offset,int value) { - for (int i = 0; i <= 3; i++) { - data[offset + i] = (byte) ((value >> (8 * (3 - i))) & 0xFF); - } - return offset+4; - } - - /** - * Writes a long to a byte array in 8 byte big-endian representation. - * - * @param value long value to write to the array - * @param data Byte array into which to write the given long - * @param offset Offset into the array at which the long will be written - * - * @throws IndexOutOfBoundsException If long reaches outside the destination - * byte array - * @return Offset after writing 8 bytes - */ - public static int writeLong(byte[] data, int offset,long value) { - for (int i = 0; i <= 7; i++) { - data[offset + i] = (byte) (value >> (8 * (7 - i))); - } - return offset+8; - } - - /** - * Reads ByteBuffer contents into a new byte array - * - * @param bb ByteBuffer - * @return New byte array - */ - public static byte[] toByteArray(ByteBuffer bb) { - int len = bb.remaining(); - byte[] bytes = new byte[len]; - bb.get(bytes); - return bytes; - } - - /** - * Reads ByteBuffer contents into a new Data object - * - * @param bb ByteBuffer - * @return Blob extracted from ByteBuffer - */ - public static AArrayBlob toData(ByteBuffer bb) { - return Blob.wrap(toByteArray(bb)); - } - - /** - * Converts an int value in the range 0..15 to a hexadecimal character - * - * @param i Value to convert - * @return Hex digit value (lowercase) - */ - public static char toHexChar(int i) { - if (i >= 0) { - if (i <= 9) return (char) (i + 48); - if (i <= 15) return (char) (i + 87); - } - throw new IllegalArgumentException("Unable to convert to single hex char: " + i); - } - - /** - * Converts a hex string to a byte array. Must contain an even number of hex - * digits, or else null will be returned - * - * @param hex String containing Hex digits - * @return byte array with the given hex value, or null if string is not valid - */ - public static byte[] hexToBytes(String hex) { - byte[] bs= hexToBytes(hex, hex.length()); - return bs; - } - - /** - * Converts a hex string to a byte array. Must contain an the expected number of - * hex digits, or else null will be returned - * - * @param hex String containing Hex digits - * @param stringLength number of hex digits in the string to use - * @return byte array with the given hex value, or null if not valud - */ - public static byte[] hexToBytes(String hex, int stringLength) { - if (hex.length() != stringLength) { - return null; - } - int N = stringLength / 2; - if (N * 2 != stringLength) { - return null; - } - byte[] result = new byte[N]; - - for (int i = 0; i < N; i++) { - char high = hex.charAt(2 * i); - char low = hex.charAt(2 * i + 1); - int lowD = Utils.hexVal(low); - if (lowD < 0) return null; - int highD = Utils.hexVal(high); - if (highD < 0) return null; - result[i] = (byte) (highD * 16 + lowD); - } - - return result; - } - - /** - * Converts a hex string to an unsigned big Integer - * - * @param hex Value to convert - * @return BigInteger - */ - public static BigInteger hexToBigInt(String hex) { - return new BigInteger(1, hexToBytes(hex)); - } - - /** - * Gets the value of a single hex car e.g. hexVal('c') => 12 - * - * @param c Character representing a hex digit - * @return int in the range 0..15 inclusive, or -1 if not a hex char - */ - public static int hexVal(char c) { - int v = (int) c; - if (v <= 102) { - if (v >= 97) return v - 87; // lowercase - if ((v >= 65) && (v <= 70)) return v - 55; // uppercase - if ((v >= 48) && (v <= 57)) return v - 48; // digit - } - return -1; - } - - /** - * Converts a byte array of length N to a hex string of length 2N - * - * @param data Array of bytes - * @return Hex String - */ - public static String toHexString(byte[] data) { - return toHexString(data, 0, data.length); - } - - /** - * Converts a slice of a byte array to a hex string of length 2N - * - * @param data Array of bytes - * @param offset Start offset to read from byte array - * @param length Length in bytes to read from byte array - * @return Hex String - */ - public static String toHexString(byte[] data, int offset, int length) { - char[] hexDigits = new char[length * 2]; - for (int i = 0; i < length; i++) { - int v = ((int) data[i + offset]) & 0xFF; - hexDigits[i * 2] = toHexChar(v >>> 4); - hexDigits[i * 2 + 1] = toHexChar(v & 0xF); - } - return new String(hexDigits); - } - - /** - * Gets the Java hashCode of any value. - * - * The hashCode of null is defined as zero - * - * @param a Any Java Object, may be null - * @return hash code - */ - public static int hashCode(Object a) { - if (a == null) return 0; - return a.hashCode(); - } - - /** - * Tests if two byte array regions are identical - * - * @param a First array - * @param aOffset Offset into first array - * @param b Second array - * @param bOffset Offset into second array - * @param length Number of bytes to compare - * @return true if array regions are equal, false otherwise - */ - public static boolean arrayEquals(byte[] a, int aOffset, byte[] b, int bOffset, int length) { - return Arrays.equals(a, aOffset, aOffset+length, b, bOffset, bOffset+length); - } - - /** - * Compares two byte arrays on an unsigned basis. Shorter arrays will be - * considered "smaller" if they match in all other positions. - * - * @param a First array - * @param aOffset Offset into first array - * @param b Second array - * @param bOffset Offset into second array - * @param maxLength The maximum size for comparison. If arrays are equal up to - * this length, will return 0 - * @return Negative if a is 'smaller', 0 if a 'equals' b, positive if a is - * 'larger'. - */ - public static int compareByteArrays(byte[] a, int aOffset, byte[] b, int bOffset, int maxLength) { - int length = Math.min(maxLength, a.length - aOffset); - length = Math.min(maxLength, b.length - bOffset); - for (int i = 0; i < length; i++) { - int ai = 0xFF & a[aOffset + i]; - int bi = 0xFF & b[bOffset + i]; - if (ai < bi) return -1; - if (ai > bi) return 1; - } - if (length < a.length) return 1; // longer a considered larger - if (length < b.length) return -1; // shorter a considered smaller - return 0; - } - - /** - * Converts an unsigned BigInteger to a hex string with the given number of - * digits Truncates any high bytes beyond the given digits. - * - * @param a Value to convert - * @param digits Number of hex digits to produce - * @return String containing the hex representation - */ - public static String toHexString(BigInteger a, int digits) { - if (a.signum() < 0) - throw new IllegalArgumentException("toHexString requires a non-negative BigInteger, got :" + a); - String s = a.toString(16); // note: only works with unsigned big integers otherwise we get "-2a" etc. - int slen = s.length(); - if (slen > digits) throw new IllegalArgumentException("toHexString number of digits exceeded, got :" + slen); - if (slen == digits) return s; - StringBuffer sb = new StringBuffer(digits); - while (slen < digits) { - sb.append('0'); - slen++; - } - sb.append(s); - return sb.toString(); - } - - /** - * Writes an unsigned big integer to a specific segment of a byte[] array. Pads - * with zeros if necessary to fill the specified length. - * - * @param a Value to write - * @param dest Destination array - * @param offset Offset into destination array - * @param length Length to write - */ - public static void writeUInt(BigInteger a, byte[] dest, int offset, int length) { - if (a.signum() < 0) throw new IllegalArgumentException("Non-negative big integer expected!"); - if ((offset + length) > dest.length) { - throw new IllegalArgumentException( - "Insufficient buffer space in byte array, available = " + (dest.length - offset)); - } - byte[] bs = a.toByteArray(); - int bl = bs.length; - if (bl == length) { - // expected case, correct number of bytes in unsigned representation - System.arraycopy(bs, 0, dest, offset, length); - } else if ((bl == (length + 1)) && (bs[0] == 0)) { - // OK because this is just an overflow of sign bit - // We just need to skip the zero bute that includes the sign - System.arraycopy(bs, 1, dest, offset, length); - } else if (bl < length) { - // rare case, our representation is too short, so need to pad - int pad = length - bl; - Arrays.fill(dest, offset, offset + pad, (byte) 0); - System.arraycopy(bs, 0, dest, offset + pad, bl); - } else { - throw new IllegalArgumentException("Insufficient buffer size, was " + length + " but needed " + bl); - } - } - - /** - * Converts a String to a byte array using UTF-8 encoding - * - * @param s Any String - * @return Byte array - */ - public static byte[] toByteArray(String s) { - return s.getBytes(StandardCharsets.UTF_8); - } - - /** - * Converts any array to an Object[] array - * - * @param anyArray Array to convert - * @return Object[] array - */ - public static Object[] toObjectArray(Object anyArray) { - if (anyArray instanceof Object[]) return (Object[]) anyArray; - int n = Array.getLength(anyArray); - Object[] result = new Object[n]; - for (int i = 0; i < n; i++) { - result[i] = Array.get(anyArray, i); - } - return result; - } - - /** - * Converts any array to an ACell[] array. Elements must be Cells. - * - * @param anyArray Array to convert - * @return ACell[] array - */ - public static ACell[] toCellArray(Object anyArray) { - int n = Array.getLength(anyArray); - ACell[] result = new ACell[n]; - for (int i = 0; i < n; i++) { - result[i] = (ACell) Array.get(anyArray, i); - } - return result; - } - - /** - * Equality method allowing for nulls - * - * @param a First value - * @param b Second value - * @return true if arguments are equal, false otherwise - */ - public static boolean equals(Object a, Object b) { - if (a == b) return true; - if (a == null) return false; // b can't be null because of above line - return a.equals(b); // fall back to Object equality - } - - /** - * Equality method allowing for nulls - * - * @param a First value - * @param b Second value - * @return true if arguments are equal, false otherwise - */ - public static boolean equals(ACell a, ACell b) { - if (a == b) return true; - if (a == null) return false; // b can't be null because of above line - return a.equals(b); // fall back to Object equality - } - - /** - * Gets a hex digit as an integer 0-15 value from a Data object - * - * @param data Blob containing byte values - * @param hexDigit Position of hex digit to extract (from start of blob) - * @return Hex digit value as an integer 0..15 inclusive - */ - public static int extractDigit(ABlob data, int hexDigit) { - return data.getHexDigit(hexDigit); - } - - /** - * Gets the class of an Object, or null if the value is null - * - * @param o Object to examine - * @return Class of the Object - */ - public static Class getClass(Object o) { - if (o == null) return null; - return o.getClass(); - } - - /** - * Gets the class name of an Object, or "null" if the value is null - * - * @param o Object to examine - * @return Class name of the Object - */ - public static String getClassName(Object o) { - Class klass = getClass(o); - return (klass == null) ? "null" : klass.getName(); - } - - /** - * Converts a long to an int, throws error if out of allowable range. - * - * @param a Value to convert - * @return int value of the long if in valid Integer range - */ - public static int checkedInt(long a) { - int i = (int) a; - if (a != i) throw new IllegalArgumentException(Errors.sizeOutOfRange(a)); - return i; - } - - /** - * Converts a long to a short, throws error if out of allowable range. - * - * @param a Value to convert - * @return short value of the long if in valid Short range - */ - public static short checkedShort(long a) { - short s = (short) a; - if (s != a) throw new IllegalArgumentException(Errors.sizeOutOfRange(a)); - return s; - } - - /** - * Converts a long to a byte, throws error if out of allowable range. - * - * @param a Value to convert - * @return byte value of the long if in valid Byte range - */ - public static byte checkedByte(long a) { - byte b = (byte) a; - if (b != a) throw new IllegalArgumentException(Errors.sizeOutOfRange(a)); - return b; - } - - /** - * Writes an unsigned BigInteger as 32 bytes into a ByteBuffer - * - * @param b A ByteBuffer with at least 32 bytes capacity - * @param v A BigInteger in the unsigned 256 bit integer range - * @return The ByteBuffer with 32 bytes written - */ - public static ByteBuffer writeUInt256(ByteBuffer b, BigInteger v) { - if (v.signum() < 0) throw new IllegalArgumentException("Non-negative integer expected"); - byte[] bs = v.toByteArray(); - byte[] buf = new byte[32]; - int blen = bs.length; // length to use - if (blen <= 32) { - System.arraycopy(bs, 0, buf, 32 - blen, blen); - } else if ((blen == 33) && (bs[0] == 0)) { - // OK since this is UInt256 range, take last 32 bytes - System.arraycopy(bs, blen - 32, buf, 0, 32); - } else { - throw new IllegalArgumentException("BigInteger too large for UInt256, length in bytes=" + blen); - } - return b.put(buf); - } - - /** - * Reads an unsigned BigInteger as 32 bytes from a ByteBuffer - * - * @param b ByteBuffer from which to extract 32 bytes - * @return A non-negative BigInteger containing the unsigned big-endian value - * from the 32 bytes read - */ - public static BigInteger readUInt256(ByteBuffer b) { - byte[] buf = new byte[32]; - b.get(buf); - return new BigInteger(1, buf); - } - - /** - * Returns the minimal number of bits to represent the signed twos complement - * long value. Return value will be at least 1, max 64 - * - * @param x Long value - * @return Number of bits required for representation, in the range 1..64 - * inclusive - */ - public static int bitLength(long x) { - long ux = (x >= 0) ? x : -x - 1; - return 1 + (64 - Bits.leadingZeros(ux)); // sign bit plus number of used bits in positive representation - } - - /** - * Converts an object to an int value, handling Strings and arbitrary numbers. - * - * @param v An object representing a valid int value - * @return The converted int value of the object - * @throws IllegalArgumentException If the argument cannot be converted to an - * int - */ - public static int toInt(Object v) { - if (v instanceof Integer) return (Integer) v; - if (v instanceof String) { - return Integer.parseInt((String) v); - } - if (v instanceof Number) { - Number number = (Number) v; - int value = (int) number.longValue(); - // following is safe, because double can represent any int - if (value != number.doubleValue()) throw new IllegalArgumentException("Cannot coerce to int without loss:"); - return value; - } - throw new IllegalArgumentException("Can't convert to int: " + v); - } - - /** - * Gets a resource as a String. - * - * @param path Path to resource, e.g "actors/token.con" - * @return String content of resource file - * @throws IOException If an IO error occurs - */ - public static String readResourceAsString(String path) throws IOException { - ClassLoader classLoader = ClassLoader.getSystemClassLoader(); - try (InputStream inputStream = classLoader.getResourceAsStream(path)) { - if (inputStream == null) throw new IOException("Resource not found: " + path); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { - return reader.lines().collect(Collectors.joining(System.lineSeparator())); - } - } - } - - /** - * Extract a number of bits (up to 32) from a big-endian byte array, shifting - * right by the specified amount. Sign extends for bits beyond range of array. - * @param bs Source byte array - * @param numBits Number of bits to extract (0-32) - * @param shift Number of bits to shift - * @return Bits returned - */ - public static int extractBits(byte[] bs, int numBits, int shift) { - if ((numBits < 0) || (numBits > 32)) throw new IllegalArgumentException("Invalid number of bits: " + numBits); - - if (numBits > 8) { - return extractBits(bs, 8, shift) | (extractBits(bs, numBits - 8, shift + 8) << 8); - } - if (shift < 0) throw new IllegalArgumentException("Negative shift: " + shift); - int bslen = bs.length; - - int bshift = shift >> 3; // shift in number of bytes - - if (bshift >= bslen) { - // beyond end of array, so sign extend last byte - return ((bs[0] >= 0) ? 0 : -1) & Bits.lowBitMask(numBits); - } - - int lowShift = (shift - (bshift << 3)); - int ix = bslen - bshift - 1; // index of low byte - - int val = bs[ix]; // low byte from array into val, sign extend - if (ix > 0) { - val = val & 0xFF; // clear top 3 bytes of val - val = val | (((int) bs[ix - 1]) << 8); // high byte, sign extended - } - val = val >> lowShift; // shift val to position low bits correctly - return val & Bits.lowBitMask(numBits); // return just the requested bits - } - - /** - * Sets a number of bits (up to 32) in a big-endian byte array, shifting by the - * specified amount Ignores bits set outside the byte array - * @param bs Target byte array - * @param numBits Number of bits to set (0-32) - * @param shift Number of bits to shift - * @param bits Bits to set - */ - public static void setBits(byte[] bs, int numBits, int shift, int bits) { - if ((numBits < 0) || (numBits > 32)) { - throw new IllegalArgumentException("Invalid number of bits: " + numBits); - } - if (numBits > 8) { - setBits(bs, 8, shift, bits); - setBits(bs, numBits - 8, shift + 8, bits >> 8); - return; - } - if (shift < 0) throw new IllegalArgumentException("Negative shift: " + shift); - int bslen = bs.length; - int bshift = shift >> 3; // shift in number of bytes - if (bshift >= bslen) return; // nothing to do, beyond end of byte array - int ix = bslen - bshift - 1; // index of low byte - - // setup val with bits to set, others zero - int lowShift = (shift - (bshift << 3)); - int lowBitMask = Bits.lowBitMask(numBits); - int val = (bits & lowBitMask) << lowShift; - - // setup keep with bits to keep in low 16 bits - int keepBitMask = ~(lowBitMask << lowShift); // bits to keep from original array - int keep = (bs[ix] & 0xFF); - if (ix > 0) { - keep = keep | (((bs[ix - 1]) & 0xFF) << 8); - } - keep = keep & keepBitMask; - - val = val | keep; - bs[ix] = (byte) (val & 0xFF); - if (ix > 0) { - bs[ix - 1] = (byte) ((val >> 8) & 0xFF); - } - } - - /** - * Reads data from the Byte Buffer buffer, up to the limit. - * @param bb ByteBuffer to read from - * @return Blob containing bytes read from buffer - */ - public static AArrayBlob readBufferData(ByteBuffer bb) { - bb.position(0); - int len = bb.remaining(); - byte[] bytes = new byte[len]; - bb.get(bytes); - return Blob.wrap(bytes); - } - - /** - * Prints an Object in readable String representation - * @param v Object to print - * @return String representation of value - */ - public static String print(Object v) { - StringBuilder sb=new StringBuilder(); - print(sb,v); - return sb.toString(); - } - - /** - * Prints an Object in readable String representation - * @param sb StringBuilder to append to - * @param v Object to print - */ - public static void print(StringBuilder sb,Object v) { - if (v == null) { - sb.append("nil"); - } else if (v instanceof AObject) { - ((AObject)v).print(sb); - } else if (v instanceof Boolean || v instanceof Number){ - sb.append(v.toString()); - } else if (v instanceof String) { - sb.append('"'); - sb.append((String)v); - sb.append('"'); - } else if (v instanceof Instant) { - sb.append(((Instant)v).toEpochMilli()); - } else if (v instanceof Character) { - CVMChar.create((long)v).print(sb); - } else { - throw new TODOException("Can't print: " + Utils.getClass(v)); - } - } - - /** - * Converts a Object to an InetSocketAddress - * - * @param o An Object to convert to a socket address. May be a String or existing InetSocketAddress - * @return A valid InetSocketAddress, or null if not in valid format - */ - public static InetSocketAddress toInetSocketAddress(Object o) { - if (o instanceof InetSocketAddress) { - return (InetSocketAddress) o; - } else if (o instanceof String) { - return toInetSocketAddress((String)o); - } else if (o instanceof URL) { - return toInetSocketAddress((URL)o); - } else { - return null; - } - } - - /** - * Converts a String to an InetSocketAddress - * - * @param s A string in the format of a valid URL or "myhost.com:17888" - * @return A valid InetSocketAddress, or null if not in valid format - */ - public static InetSocketAddress toInetSocketAddress(String s) { - if (s==null) return null; - try { - // Try URL parsing first - URL url=new URL(s); - return toInetSocketAddress(url); - } catch (MalformedURLException ex) { - // Try to parse as host:port - int colon = s.lastIndexOf(':'); - if (colon < 0) return null; - try { - String hostName = s.substring(0, colon); // up to last colon - int port = Utils.toInt(s.substring(colon + 1)); // after last colon - InetSocketAddress addr = new InetSocketAddress(hostName, port); - return addr; - } catch (Exception e) { - return null; - } - } - } - - /** - * Converts a URL to an InetSocketAddress. Will assume default port if not specified. - * - * @param url A valid URL - * @return A valid InetSocketAddress for the URL - */ - public static InetSocketAddress toInetSocketAddress(URL url) { - String host=url.getHost(); - int port=url.getPort(); - if (port<0) port=Constants.DEFAULT_PEER_PORT; - return new InetSocketAddress(host,port); - } - - /** - * Filters the array, returning an array containing only the elements where the - * predicate returns true. May return the same array if all elements are - * included. - * - * @param arr Array to filter - * @param predicate Predicate to test array elements - * @return Filtered array. - */ - public static T[] filterArray(T[] arr, Predicate predicate) { - if (arr.length <= 32) return filterSmallArray(arr, predicate); - throw new TODOException("Filter large arrays"); - } - - /** - * Return a list of values, sorted according to the score computed using the - * provided function, in ascending order. Ignores elements where score is null - * (will not be included in the resulting list) - * - * @param scorer a Function mapping collection elements to Long values - * @param coll Collection of values to compare - * @return The sorted collection values as an ArrayList, in ascending score - * order. - */ - public static ArrayList sortListBy(Function scorer, Collection coll) { - // TODO can probably improve efficiency - ArrayList result = new ArrayList<>(coll.size()); - HashMap scores = new HashMap<>(coll.size()); - for (T c : coll) { - Long score = scorer.apply(c); - if (score == null) continue; - scores.put(c, score); - result.add(c); - } - result.sort(new Comparator<>() { - @Override - public int compare(T a, T b) { - long comp = scores.get(a) - scores.get(b); - return Long.signum(comp); - } - }); - return result; - } - - /** - * Filters the array, returning an array containing only the elements where the - * predicate returns true. May return the same array if all elements are - * included. - * - * Array must have a maximum of 32 elements - * - * @param arr - * @param predicate - * @return - */ - private static T[] filterSmallArray(T[] arr, Predicate predicate) { - int mask = 0; - int n = arr.length; - for (int i = 0; i < n; i++) { - if (predicate.test(arr[i])) mask |= (1 << i); - } - return filterSmallArray(arr, mask); - } - - @SuppressWarnings("unchecked") - public static T[] filterSmallArray(T[] arr, int mask) { - int n = arr.length; - if (n > 32) throw new IllegalArgumentException("Array too long to filter: " + n); - int fullMask = (1 << n) - 1; - if (mask == fullMask) return arr; - int nn = Integer.bitCount(mask); - T[] result = (T[]) Array.newInstance(arr.getClass().getComponentType(), nn); - if (nn == 0) return result; - int ix = 0; - for (int i = 0; i < n; i++) { - if ((mask & (1 << i)) != 0) { - result[ix++] = arr[i]; - } - } - assert (ix == nn); - return result; - } - - /** - * Computes a bit mask of up to 16 bits by scanning a full array for which - * elements are included in the subset, comparing using object identity - * - * Subset must be an ordered subset of of the full array - * - * @param set Array of elements - * @param subset Array of element subset (must be identical) - * @return Bit mask as a short - */ - public static short computeMask(T[] set, T[] subset) { - int n = set.length; - if (n > 16) throw new IllegalArgumentException("Max length of 16 for mask computation, got: " + n); - int mask = 0; - int ix = 0; - int subsetLength = subset.length; - for (int i = 0; i < n; i++) { - if (ix == subsetLength) break; // no more items to find - if (set[i] == subset[ix]) { - mask |= (1 << i); - ix++; - } - } - if (ix != subsetLength) throw new IllegalArgumentException("Subset not all found"); - return (short) mask; - } - - /** - * Hack to convert a checked exception into an unchecked exception. - * - * @param Type of exception to return - * @param t Any Throwable instance - * @return Throwable instance - * @throws T In all cases - */ - @SuppressWarnings("unchecked") - public static T sneakyThrow(Throwable t) throws T { - throw (T) t; - } - - @SuppressWarnings("unchecked") - public static T[] copyOfRangeExcludeNulls(T[] entries, int offset, int length) { - int newLen = length; - for (int i = 0; i < length; i++) { - if (entries[offset + i] == null) newLen--; - } - if (newLen < length) { - T[] result = (T[]) Array.newInstance(entries.getClass().getComponentType(), newLen); - int ix = 0; - for (int i = 0; i < length; i++) { - T v = entries[offset + i]; - if (v != null) { - result[ix++] = v; - } - } - assert (ix == newLen); - return result; - } else { - return Arrays.copyOfRange(entries, offset, offset + length); - } - } - - /** - * Reverse an array in place - * @param arr Array to reverse - */ - public static void reverse(T[] arr) { - reverse(arr, arr.length); - } - - /** - * Reverse the first n elements of an array in place - * @param arr Array to reverse - * @param n Number of elements to reverse - */ - public static void reverse(T[] arr, int n) { - for (int i = 0; i < (n / 2); i++) { - T val = arr[i]; - arr[i] = arr[n - i - 1]; - arr[n - i - 1] = val; - } - } - - /** - * Reads the full contents of an input stream into a new byte array. - * - * @param is An arbitrary InputStream - * @return A byte array containing the full contents of the given InputStream - * @throws IOException If IO error occurs - */ - public static byte[] readBytes(InputStream is) throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - byte[] buf = new byte[1024]; - int bytesRead; - while ((bytesRead = is.read(buf)) >= 0) { - bos.write(buf, 0, bytesRead); - } - return bos.toByteArray(); - } - - public static boolean isOdd(long x) { - return (x & 1L) != 0; - } - - /** - * Displays a String representing the given Object, printing null as "nil" - * - * SECURITY: should *not* be used in Actor code, use RT.str(...) instead. - * - * @param o Object to convert - * @return String representation of object - */ - public static String toString(Object o) { - if (o == null) return "nil"; - return o.toString(); - } - - /** - * Removes all spaces from a String - * @param s String to strip - * @return String without spaces - */ - public static String stripWhiteSpace(String s) { - return s.replaceAll("\\s+", ""); - } - - /** - * Gets the number of Refs directly contained in a Cell (will be zero if the - * Cell is not a Ref container) - * - * @param a Cell to check (may be null) - * @return Number of Refs in the object. - */ - public static int refCount(ACell a) { - if (a==null) return 0; - return a.getRefCount(); - } - - /** - * Counts the total number of Refs contained in a data object recursively. Will - * count duplicate children multiple times. - * - * @param a Object to count Refs in - * @return Total number of Refs found - */ - public static long totalRefCount(Object a) { - if (!(a instanceof ACell)) return 0; - - ACell ra = (ACell) a; - long[] count = new long[] { 0L }; - - ACell ra2; - ra2 = ra.updateRefs(r -> { - count[0] += 1 + totalRefCount(r.getValue()); - - return r; - }); - assert (ra == ra2); // check we didn't change anything! - return count[0]; - } - - public static Ref getRef(ACell o, int i) { - if (o ==null) throw new IllegalArgumentException("Bad ref index: " + i+ " called on null"); - return o.getRef(i); - } - - @SuppressWarnings("unchecked") - public static T updateRefs(T o, IRefFunction func) { - if (o==null) return o; - return (T) o.updateRefs(func); - } - - public static int bitCount(short mask) { - return Integer.bitCount(mask & 0xFFFF); - } - - /** - * Runs test repeatedly, until it returns true or the timeout has elapsed - * - * @param timeoutMillis Timeout interval - * @param test Test to run until true - * @return True if the operation timed out, false otherwise - */ - public static boolean timeout(int timeoutMillis, Supplier test) { - long start = getTimeMillis(); - long end=start+timeoutMillis; - long now = start; - - // loop until either test succeeds (return false) or the timeout happens (return true) - while (true) { - if (test.get()) return false; - - // test failed, so sleep - try { - // compute sleep time - long nextInterval=(long) ((now - start) * 0.3 + 1); - long sleepTime=Math.min(nextInterval, end-now); - if (sleepTime<0L) return true; - Thread.sleep(sleepTime); - } catch (InterruptedException e) { - // ignore? Probably shouldn't happen though - // But should set interrupt flag as below; - Thread.currentThread().interrupt(); - } - now = getTimeMillis(); - } - } - - private static long lastTimestamp = Instant.now().toEpochMilli(); - - /** - * Gets the current system timestamp. Guaranteed monotonic within this JVM. - * - * Should be used for timestamps that need to be persisted or communicated - * Should not be used for timing - use Utils.getTimeMillis() instead - * - * - * @return Long representation of Timestamp - */ - public static long getCurrentTimestamp() { - // Use Instant milliseconds - long ts = Instant.now().toEpochMilli(); - if (ts > lastTimestamp) { - lastTimestamp = ts; - return ts; - } else { - return lastTimestamp; - } - } - - private static final long startupTimestamp=getCurrentTimestamp(); - private static final long startupNanos=System.nanoTime(); - - /** - * Gets the a millisecond accurate time suitable for use in timing. - * - * Should not be used for timestamps - * - * - * @return long - */ - public static long getTimeMillis() { - // Use nanoTime() for precision and guaranteed monotonicity - long elapsedMillis = (System.nanoTime()-startupNanos)/1000000; - return startupTimestamp+elapsedMillis; - } - - /** - * Test if the first hex digits of two bytes match - * - * @param a Any byte value - * @param b Any byte value - * @return true if the first hex digit (high nibble) of the two bytes is equal, - * false otherwise. - */ - public static boolean firstDigitMatch(byte a, byte b) { - return (a & 0xF0) == (b & 0xF0); - } - - /** - * Leftmost Binary Search. - * - * Generic method to search for an exact or approximate (leftmost) value. - * - * Examples: - * Given a vector [1, 2, 3] and target 2: returns 2. - * Given a vector [1, 2, 3] and target 5: returns 3. - * Given a vector [1, 2, 3] and target 0: returns null. - * - * @param L Items. - * @param value Function to get the value for comparison with target. - * @param comparator How to compare value with target. - * @param target Value being searched for. - * @param Type of the elements in L. - * @param Type of the target value. - * @return Target, or leftmost value, or null if there isn't a match. - */ - public static T binarySearchLeftmost(ASequence L, Function value, Comparator comparator, U target) { - long min = 0; - long max = L.count(); - - while (min < max) { - long midpoint = (min + max) / 2; - - if (comparator.compare(value.apply(L.get(midpoint)), target) < 0) - min = midpoint + 1; - else - max = midpoint; - } - - // Match can be exact or approximate. - // In case there isn't an exact match, - // a leftmost search returns a rank (min) - // which is used to get the leftmost value. - if (min < L.count() && comparator.compare(value.apply(L.get(min)), target) == 0) { - return L.get(min); - } else { - if (min - 1 == -1) - return null; - - return L.get(min - 1); - } - - } - - @SuppressWarnings("unchecked") - public static CompletableFuture> completeAll(java.util.List> futures) { - CompletableFuture[] fs = futures.toArray(new CompletableFuture[futures.size()]); - - return CompletableFuture.allOf(fs).thenApply(e -> futures.stream() - .map(CompletableFuture::join) - .collect(Collectors.toList()) - ); - } - - public static State stateAsOf(AVector states, CVMLong timestamp) { - return binarySearchLeftmost(states, State::getTimeStamp, Comparator.comparingLong(CVMLong::longValue), timestamp); - } - - public static AVector statesAsOfRange(AVector states, CVMLong timestamp, long interval, int count) { - AVector v = Vectors.empty(); - - for (int i = 0; i < count; i++) { - v = v.conj(stateAsOf(states, timestamp)); - - timestamp = CVMLong.create(timestamp.longValue() + interval); - } - - return v; - } - - public static boolean bool(Object a) { - if (a==null) return false; - if (a instanceof ACell) return (RT.bool((ACell)a)); - if (a instanceof Boolean) return ((Boolean)a); - return true; // consider other values truthy - } - - @SafeVarargs - public static List listOf(T... values) { - return Arrays.asList(values); - } - - private static final ExecutorService executor=Executors.newCachedThreadPool(); - - /** - * Executes functions on a thread pool for each element of a collection - * @param Result type of function - * @param Argument type - * @param func Function to run - * @param items Collection of items to run futures on - * @return List of futures for each item - */ - public static ArrayList> futureMap(Function func, Collection items) { - ArrayList> futures=new ArrayList<>(items.size()); - for (T item: items) { - futures.add(executor.submit(()->func.apply(item))); - } - return futures; - } - - -} diff --git a/convex-core/src/main/java/etch/Etch.java b/convex-core/src/main/java/etch/Etch.java deleted file mode 100644 index 1f7ab2d7c..000000000 --- a/convex-core/src/main/java/etch/Etch.java +++ /dev/null @@ -1,909 +0,0 @@ -package etch; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.FileChannel.MapMode; -import java.nio.channels.FileLock; -import java.util.ArrayList; -import java.util.Arrays; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import convex.core.Constants; -import convex.core.data.AArrayBlob; -import convex.core.data.ACell; -import convex.core.data.Blob; -import convex.core.data.Hash; -import convex.core.data.Ref; -import convex.core.data.RefSoft; -import convex.core.util.Counters; -import convex.core.util.Shutdown; -import convex.core.util.Utils; - -/** - * A stupid, fast database for immutable data you want carved in stone. - * - * We solve the cache invalidation problem, quite effectively, by never changing anything. Once a value - * is written for a given key, it cannot be changed. Etch is indifferent to the exact meaning of keys, - * but they must have a fixed length of 32 bytes (256 bits). - * - * It is intended that keys are pseudo-random hash values, which will result in desirable distributions - * of data for the radix tree structure. - * - * Radix tree index blocks are 256-way arrays of 8 byte pointers. - * - * To avoid creating too many index blocks when collisions occur, a chained entry list inside is created - * in unused space in index blocks. Once there is no more space, chains are collapsed to a new index block. - * - * Pointers in index blocks are of 4 possible types, determined by the two high bits (MSBs): - * - 00 high bits: pointer to data - * - 01 high bits: pointer to next index node - * - 10 high bits: start of chained entry list - * - 11 high bits: continuation of chained entry list - * - * Data is stored as: - * - 32 bytes key - * - X bytes monotonic label of which - * - 1 byte status - * - 8 bytes Memory Size (TODO: might be negative for unknown?) - * - 2 bytes data length N (a short) - * - N byes actual data - */ -public class Etch { - // structural constants for data block - private static final int KEY_SIZE=32; - private static final int LABEL_SIZE=1+8; // Flags (byte) plus Memory Size (long) - private static final int LENGTH_SIZE=2; - private static final int POINTER_SIZE=8; - - /** - * Index block is fixed size with 256 entries - */ - private static final int INDEX_BLOCK_SIZE=POINTER_SIZE*256; - - // constants for memory mapping buffers into manageable regions - private static final int MAX_REGION_SIZE=1<<30; // 1GB seems reasonable - private static final int REGION_MARGIN=65536; // 64k margin for writes past end of current buffer - - /** - * Magic number for Etch files, must be first 2 bytes - */ - private static final byte[] MAGIC_NUMBER=Utils.hexToBytes("e7c6"); - - private static final int SIZE_HEADER_MAGIC=2; - private static final int SIZE_HEADER_FILESIZE=8; - private static final int SIZE_HEADER_ROOT=32; - - /** - * Length of header, including: - * - Magic number "e7c6" (2 bytes) - * - File size (8 bytes) - * - Root hash (32 bytes) - * - * "The Ultimate Answer to Life, The Universe and Everything is... 42!" - * - Douglas Adams, The Hitchhiker's Guide to the Galaxy - */ - private static final int SIZE_HEADER=SIZE_HEADER_MAGIC+SIZE_HEADER_FILESIZE+SIZE_HEADER_ROOT; - - protected static final long OFFSET_FILE_SIZE = SIZE_HEADER_MAGIC; - protected static final long OFFSET_ROOT_HASH = SIZE_HEADER_MAGIC+SIZE_HEADER_FILESIZE; - - /** - * Start position of first index block - * This is immediately after a long data length pointer at the start of the file - */ - private static final long INDEX_START=SIZE_HEADER; - - private static final long TYPE_MASK= 0xC000000000000000L; - private static final long PTR_PLAIN=0x0000000000000000L; // direct pointer to data - private static final long PTR_INDEX=0x4000000000000000L; // pointer to index block - private static final long PTR_START=0x8000000000000000L; // start of chained entries - private static final long PTR_CHAIN=0xC000000000000000L; // chained entries after start - - private static final Logger log=LoggerFactory.getLogger(Etch.class.getName()); - - /** - * Temporary byte array for writer. Must not be used by readers. - */ - private final ThreadLocal tempArray=new ThreadLocal<>() { - @Override - public byte[] initialValue() { - return new byte[INDEX_BLOCK_SIZE]; - } - }; - - /** - * Internal pointer to end of database - */ - private static long tempIndex=0; - - private final File file; - private final RandomAccessFile data; - - /** - * List of MappedByteBuffers for each region of the database file. - */ - private final ArrayList regionMap=new ArrayList<>(); - - private long dataLength=0; - - private boolean BUILD_CHAINS=true; - private EtchStore store; - - private Etch(File dataFile) throws IOException { - // Ensure we have a RandomAccessFile that exists - this.file=dataFile; - if (!dataFile.exists()) dataFile.createNewFile(); - this.data=new RandomAccessFile(dataFile,"rw"); - - // Try to exclusively lock the Etch database file - FileChannel fileChannel=this.data.getChannel(); - FileLock lock=fileChannel.tryLock(); - if (lock==null) { - log.error("Unable to obtain lock on file: {}",dataFile); - throw new IOException("File lock failed"); - } - - // at this point, we have an exclusive lock on the database file. - - if (dataFile.length()==0) { - // Need to populate new file, with data length long and initial index block - MappedByteBuffer mbb=seekMap(0); - mbb.put(MAGIC_NUMBER); - - // write zeros (temp is newly empty) for file size and root. Will fix later - int headerZeros=SIZE_HEADER_FILESIZE+SIZE_HEADER_ROOT; - byte[] temp=new byte[headerZeros]; - mbb.put(temp,0,headerZeros); - dataLength=SIZE_HEADER; // advance past initial long - - // add an index block - long indexStart=appendNewIndexBlock(); - assert(indexStart==INDEX_START); - - // ensure data length is initially correct - mbb=seekMap(SIZE_HEADER_MAGIC); - mbb.putLong(dataLength); - } else { - // existing file, so need to read the length pointer - MappedByteBuffer mbb=seekMap(0); - byte[] check=new byte[2]; - mbb.get(check); - if(!Arrays.equals(MAGIC_NUMBER, check)) { - throw new IOException("Bad magic number! Probably not an Etch file: "+dataFile); - } - - long length = mbb.getLong(); - dataLength=length; - } - - // shutdown hook to close file / release lock - convex.core.util.Shutdown.addHook(Shutdown.ETCH,new Runnable() { - public void run() { - close(); - } - }); - } - - /** - * Create an Etch instance using a temporary file. - * @return The new Etch instance - * @throws IOException If an IO error occurs - */ - public static Etch createTempEtch() throws IOException { - Etch newEtch = createTempEtch("etch-"+tempIndex); - tempIndex++; - return newEtch; - } - - /** - * Create an Etch instance using a temporary file with a specific file prefix. - * @param prefix temporary file prefix to use - * @return The new Etch instance - * @throws IOException If an IO error occurs - */ - public static Etch createTempEtch(String prefix) throws IOException { - File data = File.createTempFile(prefix+"-", null); - if (Constants.ETCH_DELETE_TEMP_ON_EXIT) data.deleteOnExit(); - return new Etch(data); - } - - /** - * Create an Etch instance using the specified file - * @param file File with which to create Etch instance - * @return The new Etch instance - * @throws IOException If an IO error occurs - */ - public static Etch create(File file) throws IOException { - Etch etch= new Etch(file); - log.debug("Etch created on file: {} with data length: {}"+file,etch.dataLength); - return etch; - } - - /** - * Gets the MappedByteBuffer for a given position, seeking to the specified location. - * Type flags are ignored if included in the position pointer. - * - * @param position Target position for the MappedByteBuffer - * @return MappedByteBuffer instance with correct position. - * @throws IOException - */ - private MappedByteBuffer seekMap(long position) throws IOException { - position=slotPointer(position); // ensure we don't have any pesky type bits - - if ((position<0)||(position>dataLength)) { - throw new Error("Seek out of range in Etch file: position="+Utils.toHexString(position)+ " dataLength="+Utils.toHexString(dataLength)+" file="+file.getName()); - } - int mapIndex=Utils.checkedInt(position/MAX_REGION_SIZE); // 1GB chunks - - MappedByteBuffer mbb=(MappedByteBuffer) getBuffer(mapIndex).duplicate(); - mbb.position(Utils.checkedInt(position-MAX_REGION_SIZE*(long)mapIndex)); - return mbb; - } - - private MappedByteBuffer getBuffer(int regionIndex) throws IOException { - // Get current mapped region, or null if out of range - int regionMapSize=regionMap.size(); - MappedByteBuffer mbb=(regionIndex write(AArrayBlob key, Ref value) throws IOException { - Counters.etchWrite++; - return write(key,0,value,INDEX_START); - } - - private Ref write(AArrayBlob key, int keyOffset, Ref value, long indexPosition) throws IOException { - if (keyOffset>=KEY_SIZE) { - throw new Error("Offset exceeded for key: "+key); - } - - final int digit=key.byteAt(keyOffset)&0xFF; - long slotValue=readSlot(indexPosition,digit); - long type=slotType(slotValue); - - if (slotValue==0L) { - // empty location, so simply write new value - return writeNewData(indexPosition,digit,key,value,PTR_PLAIN); - - } else if (type==PTR_INDEX) { - // recursively check next level of index - long newIndexPosition=slotPointer(slotValue); // clear high bits - return write(key,keyOffset+1,value,newIndexPosition); - - } else if (type==PTR_PLAIN) { - // existing data pointer (non-zero) - // check if we have the same value first, otherwise need to resolve conflict - // This should have the current (potential collision) key in tempArray - if (checkMatchingKey(key,slotValue)) { - return updateInPlace(slotValue,value); - } - byte[] temp=tempArray.get(); - - // we need to check the next slot position to see if we can extend to a chain - int nextDigit=digit+1; - long nextSlotValue=readSlot(indexPosition,nextDigit); - - // if next slot is empty, we can make a chain! - if (BUILD_CHAINS&&(nextSlotValue==0L)) { - // update current slot to be the start of a chain - writeSlot(indexPosition,digit,slotValue|PTR_START); - - // write new data pointer to next slot - long newDataPointer=appendData(key,value); - writeSlot(indexPosition,nextDigit,newDataPointer|PTR_CHAIN); - - return value; - } - - if (keyOffset>=KEY_SIZE-1) { - throw new Error("Unexpected collision at max offset for key: "+key+" with existing key: "+Blob.wrap(temp,0,KEY_SIZE)); - } - - // have collision, so create new index node including the existing pointer - int nextDigitOfCollided=temp[keyOffset+1]&0xFF; - long newIndexPosition=appendLeafIndex(nextDigitOfCollided,slotValue); - - // put index pointer into this index block, setting flags for index node - writeSlot(indexPosition,digit,newIndexPosition|PTR_INDEX); - - // recursively write this key - return write(key,keyOffset+1,value,newIndexPosition); - } else if (type==PTR_START) { - // first check if the start pointer is the right value. if so, bail out with nothing to do - if (checkMatchingKey(key, slotValue)) { - return updateInPlace(slotValue,value); - } - - // now scan slots, looking for either the right value or an empty space - int i=1; - while (i<256) { - slotValue=readSlot(indexPosition,digit+i); - - // if we reach an empty location simply write new value as a chain continuation (PTR_CHAIN) - if (slotValue==0L) { - return writeNewData(indexPosition,digit+i,key,value,PTR_CHAIN); - } - - // if we are not in a chain, we have reached the maximum chain length. Exit loop and compress. - if (slotType(slotValue)!=PTR_CHAIN) break; - - // if we found the key itself, return since already stored. - if (checkMatchingKey(key, slotValue)) { - return updateInPlace(slotValue,value); - } - - i++; - } - - // we now need to collapse the chain, since it cannot be extended. - // System.out.println("Compressing chain, offset="+keyOffset+" chain length="+i+" with key "+key+ " indexDat= "+readBlob(indexPosition,2048)); - - // first we build a new index block, containing our new data - long newDataPointer=appendData(key,value); - long newIndexPos=appendLeafIndex(key.byteAt(keyOffset+1),newDataPointer); - - // for each element in chain, move existing data to new index block. i is the length of chain - for (int j=0; j=KEY_SIZE) { - Blob bx=readBlob(indexPosition,2048); - Blob bx2=readBlob(currentSlot,34); - Blob bx3=readBlob(dp,34); - throw new Error("Overflowing key size - key collision? index="+Utils.toHexString(indexPosition)+" dataPointer="+Utils.toHexString(dp)+" Key: "+readBlob(dp,32)); - } - - // expand to a new index block for collision - long newIndexPosition=appendNewIndexBlock(); - writeExistingData(newIndexPosition,keyOffset+1,dp); - writeExistingData(newIndexPosition,keyOffset+1,currentSlot); - writeSlot(indexPosition,digit,newIndexPosition|PTR_INDEX); - } else { - throw new Error("Unexpected type: "+type); - } - } - - /** - * Reads a blob of the specified length from storage - * @param pointer - * @param length - * @return - * @throws IOException - */ - private Blob readBlob(long pointer, int length) throws IOException { - MappedByteBuffer mbb=seekMap(pointer); - byte[] bs=new byte[length]; - mbb.get(bs); - return Blob.wrap(bs); - } - - /** - * Gets the type of a slot, given the slot value - * @param slotValue - * @return - */ - private long slotType(long slotValue) { - return slotValue&TYPE_MASK; - } - - /** - * Utility function to truncate file. Won't work if mapped byte buffers are active? - * @throws FileNotFoundException - * @throws IOException - */ - protected void truncateFile() throws FileNotFoundException, IOException { - try (FileOutputStream fos=new FileOutputStream(file, true)) { - FileChannel outChan = fos.getChannel() ; - outChan.truncate(dataLength); - } - } - - /** - * Close all files resources with this Etch store, including writing the final - * data length. - */ - synchronized void close() { - if (!(data.getChannel().isOpen())) return; // already closed - try { - // write final data length - MappedByteBuffer mbb=seekMap(OFFSET_FILE_SIZE); - mbb.putLong(dataLength); - mbb=null; - - // Force writes to disk. Probably useful. - for (MappedByteBuffer m: regionMap) { - m.force(); - } - regionMap.clear(); - System.gc(); - - data.close(); - - log.debug("Etch closed on file: "+data+" with data length: "+dataLength); - } catch (IOException e) { - log.error("Error closing Etch file: "+file); - e.printStackTrace(); - } - } - - /** - * Gets the raw pointer for, given the slot value (clears high bits) - * @param slotValue - * @return - */ - private long slotPointer(long slotValue) { - return slotValue&~TYPE_MASK; - } - - /** - * Checks if the key matches the data at the specified pointer position - * @param key - * @param dataPointer Pointer to data. Type bits in MSBs will be ignored. - * @return - * @throws IOException - */ - private boolean checkMatchingKey(AArrayBlob key, long dataPointer) throws IOException { - long dataPosition=dataPointer&~TYPE_MASK; - MappedByteBuffer mbb=seekMap(dataPosition); - byte[] temp=tempArray.get(); - mbb.get(temp, 0, KEY_SIZE); - if (key.equalsBytes(temp,0)) { - // key already in store - return true; - } - return false; - } - - /** - * Appends a leaf index block including exactly one data pointer, at the specified digit position - * @param digit Digit position for the data pointer to be stored at (0..255, high bits ignored) - * @param dataPointer Single data pointer to include in new index block - * @return the position of the new index block - * @throws IOException - */ - private long appendLeafIndex(int digit, long dataPointer) throws IOException { - long position=dataLength; - byte[] temp=tempArray.get(); - Arrays.fill(temp, (byte)0x00); - int ix=POINTER_SIZE*(digit&0xFF); - Utils.writeLong(temp, ix,dataPointer); // single node - MappedByteBuffer mbb=seekMap(position); - mbb.put(temp); // write full index block - dataLength=position+INDEX_BLOCK_SIZE; - return position; - } - - /** - * Reads a Blob from the database, returning null if not found - * @param key Key to read from Store - * @return Blob containing the data, or null if not found - * @throws IOException If an IO error occurs - */ - public Ref read(AArrayBlob key) throws IOException { - Counters.etchRead++; - - long pointer=seekPosition(key); - if (pointer<0) { - Counters.etchMiss++; - return null; // not found - } - - // seek to correct position, skipping over key - MappedByteBuffer mbb=seekMap(pointer+KEY_SIZE); - - // get flags byte - byte flagByte=mbb.get(); - - // Get memory size - long memorySize=mbb.getLong(); - - // get Data length - short length=mbb.getShort(); - byte[] bs=new byte[length]; - mbb.get(bs); - Blob encoding= Blob.wrap(bs); - try { - Hash hash=Hash.wrap(key); - ACell cell=store.decode(encoding); - cell.getEncoding().attachContentHash(hash); - - if (memorySize>0) { - // need to attach memory size for cell - cell.attachMemorySize(memorySize); - } - - Ref ref=RefSoft.create(cell, (int)flagByte); - cell.attachRef(ref); - - return ref; - } catch (Exception e) { - throw new Error("Failed to read data in etch store: "+encoding.toHexString()+" flags = "+Utils.toHexString(flagByte)+" length ="+length+" pointer = "+Utils.toHexString(pointer)+ " memorySize="+memorySize,e); - } - } - - /** - * Flushes any changes to persistent storage. - * @throws IOException If an IO error occurs - */ - public synchronized void flush() throws IOException { - for (MappedByteBuffer mbb: regionMap) { - if (mbb!=null) mbb.force(); - } - data.getChannel().force(false); - } - - /** - * Gets the position of a value in the data file from the index - * @param key Key value - * @return data file offset or -1 if not found - * @throws IOException - */ - private long seekPosition(AArrayBlob key) throws IOException { - return seekPosition(key,0,INDEX_START); - } - - /** - * Gets the slot value at the specified digit position in an index block. - * @param indexPosition Position of index block - * @param digit Digit of value 0..255 (high bits will be ignored) - * @return Pointer value (including type bits in MSBs) - * @throws IOException - */ - private long readSlot(long indexPosition, int digit) throws IOException { - long pointerIndex=indexPosition+POINTER_SIZE*(digit&0xFF); - MappedByteBuffer mbb=seekMap(pointerIndex); - long pointer=mbb.getLong(); - return pointer; - } - - /** - * Creates and writes a new data pointer at the specified position, storing the key/value - * and applying the specified type to the pointer stored in the slot - * - * @param position Position to write the data pointer - * @param key Key for the data - * @param value Value of the data - * @return - * @throws IOException - */ - private Ref writeNewData(long indexPosition, int digit, AArrayBlob key, Ref value, long type) throws IOException { - long newDataPointer=appendData(key,value)|type; - writeSlot(indexPosition, digit, newDataPointer); - return value; - } - - /** - * Updates a Ref in place at the specified position. Assumes data not changed. - * @param position Data position in storage file - * @param ref - * @return - * @throws IOException - */ - private Ref updateInPlace(long position, Ref ref) throws IOException { - // Seek to status location - MappedByteBuffer mbb=seekMap(position+KEY_SIZE); - - // Get current stored values - int currentFlags=mbb.get(); - int newFlags=Ref.mergeFlags(currentFlags,ref.getFlags()); // idempotent flag merge - - long currentSize=mbb.getLong(); - - if (currentFlags==newFlags) return ref; - - // We have a status change, need to increase status of store - mbb=seekMap(position+KEY_SIZE); - mbb.put((byte)newFlags); - - // maybe update size, if not already persisted - if ((currentSize==0L)&&((newFlags&Ref.STATUS_MASK)>=Ref.PERSISTED)) { - mbb.putLong(ref.getValue().getMemorySize()); - } - - return ref.withFlags(newFlags); // reflect merged flags - } - - /** - * Writes a slot value to an index block. - * - * @param indexPosition - * @param digit Digit radix position in index block (0..255), high bits are ignored - * @param slotValue - * @throws IOException - */ - private void writeSlot(long indexPosition, int digit, long slotValue) throws IOException { - long position=indexPosition+(digit&0xFF)*POINTER_SIZE; - MappedByteBuffer mbb=seekMap(position); - mbb.putLong(slotValue); - } - - /** - * Gets the position of a data block from the given offset into the key - * @param key Key value - * @param offset Offset in number of bytes into key value for next step of search - * @param indexPosition offset of the current index block - * @return data position for data block or -1 if not found - * @throws IOException - */ - private long seekPosition(AArrayBlob key, int offset, long indexPosition) throws IOException { - if (offset>=KEY_SIZE) { - throw new Error("Offset exceeded for key: "+key); - } - - int digit=key.byteAt(offset)&0xFF; - long slotValue=readSlot(indexPosition,digit); - long type=(slotValue&TYPE_MASK); - if (slotValue==0) { - // Empty slot i.e. not found - return -1; - } else if (type==PTR_INDEX) { - // recursively check next index node - long newIndexPosition=slotPointer(slotValue); - return seekPosition(key,offset+1,newIndexPosition); - } else if (type==PTR_PLAIN) { - if (checkMatchingKey(key,slotValue)) return slotValue; - return -1; - } else if (type==PTR_CHAIN) { - // continuation of chain from some previous index, therefore key can't be present - return -1; - } else if (type==PTR_START) { - synchronized (this) { - // start of chain, so scan chain of entries - int i=0; - while (i<256) { - long ptr=slotValue&(~TYPE_MASK); - if (checkMatchingKey(key,ptr)) return ptr; - - i++; // advance to next position - slotValue=readSlot(indexPosition,digit+i); - type=(slotValue&TYPE_MASK); - if (!(type==PTR_CHAIN)) return -1; // reached end of chain - } - } - return -1; - } else { - throw new Error("Shouldn't be possible!"); - } - } - - /** - * Append a new index block to the store file. The new Index block will be initially empty, - * i.e. filled completely with zeros. - * @return The location of the newly added index block. - * @throws IOException - */ - private long appendNewIndexBlock() throws IOException { - long position=dataLength; - byte[] temp=tempArray.get(); - MappedByteBuffer mbb=seekMap(position); - Arrays.fill(temp,(byte)0); - mbb.put(temp); - dataLength=position+INDEX_BLOCK_SIZE; - return position; - } - - /** - * Appends a new key / value data block. Returns a pointer to the data, with cleared type bits (PTR_PLAIN) - * - * @param key The key to include in the data block - * @param a the Blob representing the new data value - * @return The position of the new data block - * @throws IOException - */ - private long appendData(AArrayBlob key,Ref value) throws IOException { - assert(key.count()==KEY_SIZE); - - // Get relevant values for writing - // probably need to call these first, might move mbb position? - ACell cell=value.getValue(); - Blob encoding=cell.getEncoding(); - int status=value.getStatus(); - - long memorySize=0L; - if (status>=Ref.PERSISTED) { - memorySize=cell.getMemorySize(); - } - - // position ready for append - final long position=dataLength; - MappedByteBuffer mbb=seekMap(position); - - // append key - mbb.put(key.getInternalArray(),key.getInternalOffset(),KEY_SIZE); - - // append flags (1 byte) - int flags=value.flagsWithStatus(Math.max(value.getStatus(),Ref.STORED)); - mbb.put((byte)(flags)); // currently all flags fit in one byte - - // append Memory Size (8 bytes). Initialised to 0L if STORED only. - mbb.putLong(memorySize); - - // append blob length - short length=Utils.checkedShort(encoding.count()); - if (length==0) { - // Blob b=cell.createEncoding(); - throw new Error("Etch trying to write zero length encoding for: "+Utils.getClassName(cell)); - } - mbb.putShort(length); - - // append blob value - mbb.put(encoding.getInternalArray(),encoding.getInternalOffset(),length); - - // update total data length - dataLength=mbb.position(); - - - - if (dataLength!=position+KEY_SIZE+LABEL_SIZE+LENGTH_SIZE+length) { - System.out.println("PANIC!"); - } - - // return file position for added data - return position; - } - - public File getFile() { - return file; - } - - public synchronized Hash getRootHash() throws IOException { - MappedByteBuffer mbb=seekMap(OFFSET_ROOT_HASH); - byte[] bs=new byte[Hash.LENGTH]; - mbb.get(bs); - return Hash.wrap(bs); - } - - public synchronized void setRootHash(Hash h) throws IOException { - MappedByteBuffer mbb=seekMap(OFFSET_ROOT_HASH); - byte[] bs=h.getBytes(); - assert(bs.length==Hash.LENGTH); - mbb.put(bs); - } - - public void setStore(EtchStore etchStore) { - this.store=etchStore; - } -} diff --git a/convex-core/src/main/java/etch/EtchStore.java b/convex-core/src/main/java/etch/EtchStore.java deleted file mode 100644 index 3795e29a7..000000000 --- a/convex-core/src/main/java/etch/EtchStore.java +++ /dev/null @@ -1,218 +0,0 @@ -package etch; - -import java.io.File; -import java.io.IOException; -import java.util.function.Consumer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import convex.core.data.ACell; -import convex.core.data.Hash; -import convex.core.data.IRefFunction; -import convex.core.data.Ref; -import convex.core.store.AStore; -import convex.core.util.Utils; - -/** - * Class implementing on-disk memory-mapped storage of Convex data. - * - * - * "There are only two hard things in Computer Science: cache invalidation and - * naming things." - Phil Karlton - * - * Objects are keyed by cryptographic hash. That solves naming. Objects are - * immutable. That solves cache invalidation. - * - * Garbage collection is left as an exercise for the reader. - */ -public class EtchStore extends AStore { - private static final Logger log = LoggerFactory.getLogger(EtchStore.class.getName()); - - /** - * Etch Storage of persisted Refs for each hash value - */ - private Etch etch; - - public EtchStore(Etch etch) { - this.etch = etch; - etch.setStore(this); - } - - /** - * Creates an EtchStore using a specified file. - * - * @param file File to use for storage. Will be created it it does not already - * exist. - * @return EtchStore instance - * @throws IOException If an IO error occurs - */ - public static EtchStore create(File file) throws IOException { - Etch etch = Etch.create(file); - return new EtchStore(etch); - } - - /** - * Create an Etch store using a new temporary file with the given prefix - * - * @param prefix String prefix for temporary file - * @return New EtchStore instance - */ - public static EtchStore createTemp(String prefix) { - try { - Etch etch = Etch.createTempEtch(prefix); - return new EtchStore(etch); - } catch (IOException e) { - throw Utils.sneakyThrow(e); - } - } - - /** - * Create an Etch store using a new temporary file with a generated prefix - * - * @return New EtchStore instance - */ - public static EtchStore createTemp() { - try { - Etch etch = Etch.createTempEtch(); - return new EtchStore(etch); - } catch (IOException e) { - throw Utils.sneakyThrow(e); - } - } - - @SuppressWarnings("unchecked") - @Override - public Ref refForHash(Hash hash) { - try { - Ref existing = etch.read(hash); - return (Ref) existing; - } catch (IOException e) { - throw new Error("IO exception from Etch", e); - } - } - - @Override - public Ref storeRef(Ref ref, int status, Consumer> noveltyHandler) { - return storeRef(ref, noveltyHandler, status, false); - } - - @Override - public Ref storeTopRef(Ref ref, int status, Consumer> noveltyHandler) { - return storeRef(ref, noveltyHandler, status, true); - } - - @SuppressWarnings("unchecked") - public Ref storeRef(Ref ref, Consumer> noveltyHandler, int requiredStatus, - boolean topLevel) { - // first check if the Ref is already persisted to required level - if (ref.getStatus() >= requiredStatus) return ref; - - final ACell cell = ref.getValue(); - // Quick handling for null - if (cell == null) return (Ref) Ref.NULL_VALUE; - - // check store for existing ref first. - boolean embedded = cell.isEmbedded(); - Hash hash = null; - // if not embedded, worth checking store first for existing value - if (!embedded) { - hash = ref.getHash(); - Ref existing = refForHash(hash); - if (existing != null) { - // Return existing ref if status is sufficient - if (existing.getStatus() >= requiredStatus) { - cell.attachRef(existing); - return existing; - } - } - } - - // beyond STORED level, need to recursively persist child refs if they exist - if ((requiredStatus > Ref.STORED)&&(cell.getRefCount()>0)) { - IRefFunction func = r -> { - return storeRef((Ref) r, noveltyHandler, requiredStatus, false); - }; - - // need to do recursive persistence - // TODO: maybe switch to a queue? Mitigate risk of stack overflow? - ACell newObject = cell.updateRefs(func); - - // perhaps need to update Ref - if (cell != newObject) ref = ref.withValue((T) newObject); - } - - if (topLevel || !embedded) { - // Do actual write to store - final Hash fHash = (hash != null) ? hash : ref.getHash(); - if (log.isTraceEnabled()) { - log.trace( "Etch persisting at status=" + requiredStatus + " hash = 0x" - + fHash.toHexString() + " ref of class " + Utils.getClassName(cell) + " with store " + this); - } - - Ref result; - try { - // ensure status is set when we write to store - ref = ref.withMinimumStatus(requiredStatus); - result = etch.write(fHash, (Ref) ref); - } catch (IOException e) { - throw Utils.sneakyThrow(e); - } - - // call novelty handler if newly persisted - if (noveltyHandler != null) noveltyHandler.accept(result); - return (Ref) result; - } else { - // no need to write, just tag updated status - return ref.withMinimumStatus(requiredStatus); - } - } - - @Override - public String toString() { - return "EtchStore at: " + etch.getFile().getName(); - } - - /** - * Gets the database file name for this EtchStore - * - * @return File name as a String - */ - public String getFileName() { - return etch.getFile().toString(); - } - - public void close() { - etch.close(); - } - - /** - * Ensure the store is fully persisted to disk - * @throws IOException If an IO error occurs - */ - public void flush() throws IOException { - etch.flush(); - } - - public File getFile() { - return etch.getFile(); - } - - @Override - public Hash getRootHash() throws IOException { - return etch.getRootHash(); - } - - @Override - public void setRootHash(Hash h) throws IOException { - etch.setRootHash(h); - } - - /** - * Gets the underlying Etch instance - * @return Etch instance - */ - public Etch getEtch() { - return etch; - } -} diff --git a/convex-core/src/test/cvx/test/asset/box.cvx b/convex-core/src/test/cvx/test/asset/box.cvx deleted file mode 100644 index 997359186..000000000 --- a/convex-core/src/test/cvx/test/asset/box.cvx +++ /dev/null @@ -1,301 +0,0 @@ -;; -;; -;; Testing the `asset.box` actor. -;; -;; - -;;;;;;;;;; Setup - - -;; Deploying test version of `asset.box` instead of importing stable version. -;; -($.file/read "src/main/cvx/asset/box/actor.cvx") - - -(def box.actor - (deploy (cons 'do - (next $/*result*)))) - - -($.file/read "src/main/cvx/asset/box.cvx") - - -(def box - (deploy (concat (cons 'do - (next $/*result*)) - `((def box.actor - ~box.actor))))) - - -;; Requiring test suites for `convex.asset` since this library implement that interface. -;; -($.file/read "src/test/cvx/test/convex/asset.cvx") - -(def asset.test - (deploy (cons 'do - $/*result*))) - - -(asset.test/setup) - - -;; Other imports - -(import convex.fungible :as fungible) - -(def T - $.test) - - -;; Setup. Print test results at the end -;; -($.stream/out! (str $.term/clear.screen - "Testing `asset.box`")) - - -;; Default account is an actor, key is set to transform it into a user account. -;; -(set-key $.account/fake-key) - - -;;;;;;;;;; Helpers - - -(defmacro create-box+ - - ^{:doc {:description "Creates `n-box` boxes defined under `total`, each being also defined under `b-X` where X is a number starting at 0." - :signature [{:params [n-box]}]}} - - [n-box] - - (loop [acc [] - n 0] - (if (< n - n-box) - (recur (conj acc - (box/create)) - (inc n)) - `(do - (def total - ~acc) - ~(cons 'do - (loop [acc [] - n 0] - (if (< n - n-box) - (recur (conj acc - `(def ~(symbol (str "b-" - n)) - (total ~n))) - (inc n)) - acc))))))) - - -;;;;;;;;;; Test suites - - -(defn suite.asset - - ^{:doc {:description "Ensures `convex.asset` interface is respected."}} - - [] - - (T/group '((T/path.conj 'asset) - - (create-box+ 4) - - (T/trx '(= (set total) - (asset/balance box.actor)) - {:description "Box actor owns all its boxes."}) - - (T/trx '(address? (def user-1 - ($.account/zombie))) - {:description "User 1 created."}) - - (T/trx '(address? (def user-2 - ($.account/zombie))) - {:description "User 2 created."}) - - - (T/trx '(= #{b-0 - b-1 - b-2} - (asset/transfer user-1 - [box.actor - #{b-0 - b-1 - b-2}])) - {:description "Transfer of 3 boxes to user 1."}) - - (T/trx '(= #{b-3} - (asset/transfer user-2 - [box.actor - #{b-3}])) - {:description "Transfer of last box to user 2."}) - - (asset.test/suite.main box.actor - user-1 - user-2)))) - - - -(defn suite.content - - ^{:doc {:description "Inserting assets in boxes and removing them."}} - - [] - - (T/group '((T/path.conj 'content) - - (create-box+ 4) ;; defines `total` containing all boxes and `b-X` for each numbered box - - (T/trx '(asset/owns? *address* - [box.actor - (set total)]) - {:description "Initially, creator owns all boxes."}) - - (T/trx '(= {0 {box.actor #{b-1 - b-2}} - 1 {} - 2 {} - 3 {}} - (box/insert b-0 - [box.actor - #{b-1 - b-2}])) - {:description "Put b-1 and b-2 in b-0 since boxes are assets themselves."}) - - - (T/trx '(not (asset/owns? *address* - #{b-1 - b-2})) - {:description "Loose ownership of inserted assets."}) - - (T/trx '(asset/owns? box.actor - [box.actor - #{b-1 - b-2}]) - {:description "Box actor takes ownership of inserted assets."}) - - (T/trx '(asset/owns? *address* - [box.actor - #{b-0 - b-3}]) - {:description "Creator still owns non-inserted assets."}) - - (T/trx '(= #{b-0 - b-3} - (asset/balance box.actor - *address*)) - {:description "Don't own inserted boxes anymore."}) - - (T/trx '(= #{b-1 - b-2} - (asset/balance box.actor - box.actor)) - {:description "Box actor owns inserted boxes."}) - - (T/trx '(= #{b-1 - b-2} - (box/remove b-0 - [box.actor - #{b-1 - b-2}])) - {:description "Remove boxes from b-0."}) - - (T/trx '(= #{b-0 - b-1 - b-2 - b-3} - (asset/balance box.actor - *address*)) - {:description "Removed boxes owned again."}) - - (T/fail.code #{:TRUST} - '(box/insert b-0 - [box.actor - #{b-0}]) - {:description "Cannot insert a box into itself."}) - - ;; Using a fungible token - - (T/trx '(address? (def foocoin - (deploy (fungible/build-token {:supply 1000000})))) - {:description "Fungible token created."}) - - (T/trx '(= {0 {} - 1 {foocoin 1000} - 2 {} - 3 {}} - (box/insert b-1 - [foocoin - 1000])) - {:description "Put 1000 tokens into b-1."}) - - (T/trx '(= {0 {} - 1 {foocoin 1000} - 2 {foocoin 2000} - 3 {}} - (box/insert b-2 - [foocoin - 2000])) - {:description "Put 2000 tokens into b-2."}) - - (T/trx '(= 3000 - (asset/balance foocoin - box.actor)) - {:description "Box actor holds 3000 tokens after insertions."}) - - (T/fail.code #{:ASSERT} - '(box/remove b-1 - [foocoin - 1001]) - {:description "Cannot remove too much from a box."}) - - (T/fail.code #{:ASSERT} - '(box/remove b-1 - [foocoin - -1]) - {:description "Cannot remove negative amount from a box."}) - - (def expected-balance - (+ (- 1000000 - 3000) - 500)) - - (T/trx '(= expected-balance - (box/remove b-1 - [foocoin - 500])) - {:description "Remove 500 foocoin from b-1."}) - - (T/trx '(= expected-balance - (asset/balance foocoin - *address*)) - {:description "Regain foocoins after removing them from box actor."}) - - (T/trx '(= 2500 - (asset/balance foocoin - box.actor)) - {:description "Box balance in foocoin adjusted after removing some."})))) - - -;;; - - -(defn suite.main - - ^{:doc {:description "Main suite gathering all other suites."}} - - [] - - (T/group '((T/path.conj 'asset.box) - (suite.asset) - (suite.content)))) - - -;;; - -;($.repl/start) -(suite.main) -(T/print "asset.box") diff --git a/convex-core/src/test/cvx/test/asset/nft/simple.cvx b/convex-core/src/test/cvx/test/asset/nft/simple.cvx deleted file mode 100644 index 9fffbe231..000000000 --- a/convex-core/src/test/cvx/test/asset/nft/simple.cvx +++ /dev/null @@ -1,278 +0,0 @@ -;; -;; -;; Testing `asset.simple-nft`. -;; -;; - - -;;;;;;;;;; Setup - -($.stream/out! (str $.term/clear.screen - "Testing `asset.nft.simple`")) - - -;; Deploying test version of `asset.simple-nft` instead of importing stable version. -;; -($.file/read "src/main/cvx/asset/nft/simple.cvx") - - -(eval `(def nft - (deploy (quote ~(cons 'do - (next $/*result*)))))) - -;; Requiring test suites for `convex.asset` since this library implement that interface. -;; -($.file/read "src/test/cvx/test/convex/asset.cvx") - - -(def asset.test - (deploy (cons 'do - $/*result*))) - - -(asset.test/setup) - - -;; Other libraries -;; -(def T - $.test) - - -;; Default account is an actor, key is set to transform it into a user account. -;; -(set-key $.account/fake-key) - - -;;;;;;;;;; Test suites - - -(defn suite.asset - - ^{:doc {:decription "Ensures the `convex.asset` interface is respected."}} - - [] - - (T/group '((T/path.conj 'asset) - - ;; Quantities - - (T/trx '(= #{} - (asset/quantity-zero nft))) - - (T/trx '(= #{1 2 3 4} - (asset/quantity-add nft - #{1 2} - #{3 4}))) - (T/trx '(= #{1 2 3 4} - (asset/quantity-add nft - #{1 2 3} - #{2 3 4}))) - - (T/trx '(= #{1 2} - (asset/quantity-sub nft - #{1 2 3} - #{3 4 5}))) - (T/trx '(= #{1 2} - (asset/quantity-add nft - #{1 2} - nil))) - (T/trx '(= #{1 2} - (asset/quantity-add nft - nil - #{1 2}))) - - (T/trx '(= #{1 2} - (asset/quantity-sub nft - #{1 2} - nil))) - - (T/trx '(= #{} - (asset/quantity-sub nft - nil - #{1 2}))) - - (T/trx '(asset/quantity-contains? nft - #{1 2 3} - #{2 3})) - - (T/trx '(not (asset/quantity-contains? nft - #{1 2 3} - #{3 4}))) - - (T/trx '(not (asset/quantity-contains? nft - #{1 2 3} - #{4 5 6}))) - - ;; Transfer and ownership - - (T/trx '(vector? (def total - (loop [acc []] - (if (= (count acc) - 4) - acc - (recur (conj acc - (call nft - (create)))))))) - {:description "4 NFTs created."}) - - (T/trx '(= (set total) - (asset/balance nft - *address*)) - {:description "Creator balance contain all NFTs."}) - - (T/trx '(asset/owns? *address* - [nft - (set total)]) - {:description "Initially, creator owns all NFTs."}) - - (T/trx '(address? (def user-1 - ($.account/zombie))) - {:description "User 1 deployed."}) - - (T/trx '(address? (def user-2 - ($.account/zombie))) - {:description "User 2 deployed."}) - - (def balance-creator - #{(total 0)}) - - (def balance-user-1 - (set (next total))) - - (T/trx '(= balance-user-1 - (asset/transfer user-1 - [nft - balance-user-1])) - {:description "Transfer almost all NFTs to user 1."}) - - (T/trx '(= balance-user-1 - (asset/balance nft - user-1)) - {:description "Balance of user 1 updated."}) - - (T/trx '(= balance-creator - (asset/balance nft - *address*)) - {:description "Balance of created updated."}) - - (T/trx '(not (asset/owns? *address* - [nft - balance-user-1])) - {:description "After transfer, creator do not own user 1 NFTs."}) - - (T/trx '(asset/owns? user-1 - [nft - balance-user-1]) - {:description "After transfer, user 1 owns expected NFTs."}) - - (T/trx '(asset/owns? *address* - [nft - balance-creator]) - {:description "Creator still owns 1 NFT."}) - - (T/trx '(= #{} - (asset/balance nft - user-2)) - {:description "User 2 balance is still empty."}) - - (def balance-user-2 - balance-creator) - - (T/trx '(= balance-user-2 - (asset/transfer user-2 - [nft - balance-user-2])) - {:description "Transfer remaining NFT to user 2."}) - - (T/trx '(= balance-user-2 - (asset/balance nft - user-2)) - {:description "Balance of user 2 updated."}) - - (T/trx '(= #{} - (asset/balance nft - *address*)) - {:description "Balance of creator is empty."}) - - (T/trx '(asset/owns? user-2 - [nft - balance-user-2]) - {:description "User 2 owns remaining NFT."}) - - - (T/trx '(not (asset/owns? *address* - [nft - balance-user-2])) - {:description "Creator lost ownership of its last NFT."}) - - (T/trx '(= balance-user-1 - (asset/balance nft - user-1)) - {:description "Balance of user 1 unchanged."}) - - (asset.test/suite.main nft - user-1 - user-2)))) - - - -(defn suite.burn - - ^{:doc {:description "Burning NFTs."}} - - [] - - (T/group '((T/path.conj 'burn) - - (T/fail.code #{:TRUST} - '(call nft - (burn #{-1})) - {:description "Cannot burn inexistant NFT."}) - - (T/trx '(long? (def piece - (call nft - (create)))) - {:description "NFT created."}) - - (T/trx '(= #{piece} - (call nft - (burn #{piece}))) - {:description "NFT burned."}) - - (T/fail.code #{:TRUST} - '(call nft - (burn #{piece})) - {:description "Impossible to double-burn."}) - - (T/trx '(= #{} - (asset/balance nft - *address*)) - {:description "Balance is empty after burning."}) - - (T/trx '(not (asset/owns? *address* - [nft - #{piece} ])) - {:description "Cannot own burned NFT."})))) - - -;;; - - -(defn suite.main - - ^{:doc {:description "Main suite gathering all other suites."}} - - [] - - (T/group '((T/path.conj 'asset.simple-nft) - (suite.asset) - (suite.burn)))) - - -;;; - - -(T/report.clear) -(suite.main) -(T/print "suite.main") diff --git a/convex-core/src/test/cvx/test/asset/nft/tokens.cvx b/convex-core/src/test/cvx/test/asset/nft/tokens.cvx deleted file mode 100644 index 9385203e7..000000000 --- a/convex-core/src/test/cvx/test/asset/nft/tokens.cvx +++ /dev/null @@ -1,1042 +0,0 @@ -;; -;; -;; Testing the `asset.nft-tokens`. -;; -;; - - -;;;;;;;;;; Setup - - -($.stream/out! (str $.term/clear.screen - "Testing `asset.nft.tokens`")) - - -;; Deploying test version of `asset.simple-nft` instead of importing stable version. -;; -($.file/read "src/main/cvx/asset/nft/tokens.cvx") - - -(eval `(def nft - (deploy (quote ~(cons 'do - (next $/*result*)))))) - - -($.file/exec "src/test/cvx/test/convex/asset/quantity/set-long.cvx") - - - -;; Importing stable versions -;; -(import convex.asset :as asset) - -(def T - $.test) - - -;; Default account is an actor, key is set to transform it into a user account. -;; -(set-key $.account/fake-key) - - -;;;;;;;;;; Reusable - - -(defn create.check - - ^{:doc {:description "Checks generic facts about `token`, a newly created token."}} - - [prepare-trx-pop] - - (T/group '((T/path.conj 'create.check) - - (T/trx '(asset/owns? *address* - [nft - #{token}]) - {:description "Creater owns token."}) - - (T/trx '(= #{token} - (asset/balance nft - *address*)) - {:description "Token is in creator balance."}) - - (T/trx '(= *address* - (call nft - (get-token-creator token))) - {:description "Creator is retrieved."}) - - (T/trx '(= *address* - (call nft - (get-token-owner token))) - {:description "Initially, creator is owner."}) - - (T/trx '(= data - (call nft - (get-token-data token))) - {:description "Token data is retrieved."}) - - (T/trx '(call nft - (check-trusted? *address* - :destroy - token)) - {:description "Can destroy token."}) - - (T/trx '(call nft - (check-trusted? *address* - :transfer - token)) - {:description "Can transfer token."}) - - (T/trx '(call nft - (check-trusted? *address* - :update - token)) - {:description "Can update token."}) - - (T/trx '(call nft - (check-trusted? *address* - [:update :name] - token)) - {:description "Can update token name."}) - - (def data-new - (assoc data - :foo - :bar)) - - (T/trx '(= data-new - (call nft - (merge-token-data token - {:foo :bar}))) - {:description "Can merge data to token."}) - - (T/trx '(= data-new - (call nft - (get-token-data token))) - {:description "Data merged permanently."}) - - (T/trx '(= 42 - (call nft - (set-token-data token - 42))) - {:description "Can replace token data."}) - - (T/trx '(= 42 - (call nft - (get-token-data token))) - {:description "Data replaced permanently."}) - - (T/trx '(nil? (call nft - (check-transfer *address* - nil - token))) - {:description "No transfer issued."})) - prepare-trx-pop)) - - - -(defn destroy.check - - ^{:doc {:description "Destroys token interned as `token` and checks generic facts."}} - - [prepare-trx-pop] - - (T/group '((T/path.conj 'destroy.check) - - (T/trx '(= "No right to transfer token -1" - (call nft - (check-transfer *address* - nil - -1))) - {:description "Cannot check transfer on inexistent token."}) - - (T/trx '(call nft - (destroy-token token)) - {:description "Token destroyed."}) - - (T/fail.code #{:ASSERT} - '(call nft - (destroy-token -1)) - {:description "Cannot destroy inexistent token."}) - - (T/trx '(not (asset/owns? *address* - [nft - token])) - {:description "Cannot own destroyed token."}) - - (T/trx '(= #{} - (asset/balance nft - *address*)) - {:description "Balance is empty after destroying token."}) - - (T/trx '(nil? (get nft/token-records - token)) - {:description "Token record destroyed."})) - prepare-trx-pop)) - - - -(defn separate-offer-accept - - ^{:doc {:description "Tests transfer scenario where the offer is accepted separatelu from emission."}} - - [receiver] - - (T/group `((T/path.conj 'separate-offer-accept) - - (def receiver - ~receiver) - - (T/trx '(zero? (def token - (call nft - (create-token {:name "Token"} - nil)))) - {:description "Token created."}) - - (T/trx '(= {*address* {receiver #{token}}} - (asset/offer receiver - [nft - token])) - {:description "Send offer."}) - - (T/trx '(asset/owns? *address* - [nft - token]) - {:description "Still owner, offer not yet accepted."}) - - (T/trx '(not (asset/owns? receiver - [nft - token])) - {:description "Receiver not yet owner, has not accepted yet."}) - - (T/trx '(= [nft - #{token}] - (eval-as receiver - `(~asset/accept ~*address* - [~nft - ~token]))) - {:description "Receiver accepts offer."}) - - (T/trx '(not (asset/owns? *address* - [nft - token])) - {:description "Lost ownership after offer got accepted."}) - - (T/trx '(asset/owns? receiver - [nft - token]) - {:description "Receiver gained ownership after accepting."})))) - - - -(defn transfer.check - - ^{:doc {:description "Ensures basic facts are respected after a preliminary transfer between `sender` and `receiver`."}} - - - ([token] - - (transfer.check token - *address* - *address* - receiver)) - - - ([token creator sender receiver] - - (T/group `((T/path.conj 'transfer.check) - - (do - (def creator - ~creator) - (def receiver - ~receiver) - (def sender - ~sender) - (def token - ~token)) - - (T/trx `(not (asset/owns? ~sender - ~[nft - token])) - {:description "Lost ownership after transfer."}) - - (T/trx '(asset/owns? receiver - [nft - token]) - {:description "Receiver is now the owner."}) - - (T/trx '(not (subset? #{token} - (asset/balance nft - sender))) - {:description "Balance reflects lost ownership."}) - - (T/trx '(subset? #{token} - (asset/balance nft - receiver)) - {:description "Balance of receiver reflects ownership."}) - - (T/trx '(= creator - (call nft - (get-token-creator token))) - {:description "Creator did not change during transfer."}) - - (T/trx '(= receiver - (call nft - (get-token-owner token))) - {:description "Token owner adjusted."}) - - (T/trx '(not (call nft - (check-trusted? sender - :destroy - token))) - {:description "Lost ability to destroy."}) - - (T/trx '(not (call nft - (check-trusted? sender - :transfer - token))) - {:description "Lost ability to transfer."}) - - (T/trx '(not (call nft - (check-trusted? sender - :update - token))) - {:description "Lost ability to update."}) - - (T/trx '(not (call nft - (check-trusted? sender - [:update :name] - token))) - {:description "Lost ability to update name."}) - - (T/trx '(call nft - (check-trusted? receiver - :destroy - token)) - {:description "Receiver gained ability to destroy."}) - - (T/trx '(call nft - (check-trusted? receiver - :transfer - token)) - {:description "Receiver gained ability to transfer."}) - - (T/trx '(call nft - (check-trusted? receiver - :update - token)) - {:description "Receiver gained ability to update."}) - - (T/trx '(call nft - (check-trusted? receiver - [:update :name] - token)) - {:description "Receiver gained ability to update name."}))))) - - - -(defn transfer.do - - ^{:doc {:description "Performs a transfer to internet `receiver`."}} - - [load] - - (T/trx `(= [nft - ~(if (set? load) - load - #{load})] - (asset/transfer receiver - [nft - ~load])) - {:description "Token(s) transferred."})) - - - -(defn transfer.mono - - ^{:doc {:description "Tests scenarios of a single token transfer."}} - - [path-item make-account] - - (T/group `((T/path.conj (quote ~path-item)) - - (def receiver - ~make-account) - - (T/trx '(zero? (def token - (call nft - (create-token {:name "Token"} - nil)))) - {:description "First token created."}) - - (transfer.do token) - - (transfer.check token)))) - - -;;;;;;;;;; Test suites - - -(defn suite.class - - ^{:doc {:description "Tests creating a token with a class that logs all events."}} - - [] - - (T/group '((T/path.conj 'class) - - (def receiver - ($.account/zombie)) - - (def uri-logger - "http://www.logger.com") - - (def logger - (deploy - `(do - - (def nft - ~nft) - - (def token-history - {}) - - (defn check-trusted? - ^{:callable? true} - [addr policy-key id] - (if (= policy-key - :destroy) - (= (call nft - (get-token-creator id)) - addr) - (= (call nft - (get-token-owner id)) - addr))) - - (defn create-token - ^{:callable? true} - [caller id initial-data] - (def token-history - (assoc token-history - id - [{:caller caller - :data initial-data - :event :create-token}]))) - - (defn destroy-token - ^{:callable? true} - [caller id] - (def token-history - (dissoc token-history - id))) - - (defn get-token-history - ^{:callable? true} - [id] - (get token-history - id)) - - (defn get-uri - ^{:callable? true} - [id] - ~uri-logger) - - (defn merge-token-data - ^{:callable? true} - [caller id data] - (def token-history - (assoc token-history - id - (conj (get token-history - id) - {:caller caller - :data data - :event :merge-token-data})))) - - (defn set-token-data - ^{:callable? true} - [caller id data] - (def token-history - (assoc token-history - id - (conj (get token-history - id) - {:caller caller - :data data - :event :set-token-data})))) - - - (defn perform-transfer - ^{:callable? true} - [caller sender receiver id-set] - (def token-history - (reduce (fn [history id] - (assoc history id - (conj (get history - id) - {:caller caller - :event :transfer - :sender sender - :receiver receiver}))) - token-history - id-set)))))) - - (def data - {:name "Token" - :uri "https://www.mysite.com"}) - - (T/trx '(zero? (def token - (call nft - (create-token data - logger)))) - {:description "Token created."}) - - (T/trx '(= {:class logger - :creator *address* - :data data - :owner *address*} - (get nft/token-records - token)) - {:description "Accurate token record."}) - - (T/trx '(= logger - (call nft - (get-token-class token))) - {:description "Class reported."}) - - (create.check (fn [] - `(def history - ~(call logger - (get-token-history token))))) - - (T/trx `(= ~[{:caller *address* - :data data - :event :create-token} - {:caller *address* - :data {:foo :bar} ;; Done by `create.check` - :event :merge-token-data} - {:caller *address* - :data 42 ;; Done by `create.check` - :event :set-token-data}] - ~history) - {:description "History tracked by logger."}) - - (T/trx '(= [nft - #{token}] - (asset/transfer receiver - [nft - token])) - {:description "Token transferred."}) - - (destroy.check (fn [] - `(def history-2 - ~(call logger - (get-token-history token))))) - - (T/trx '(nil? history-2) - {:description "History removed at destruction."})))) - - - -(defn suite.class.restrict - - ^{:doc {:description "Tests creating a token with a class that limits the number of token transfer to 2."}} - - [] - - (T/group '((T/path.conj 'class.restrict) - - (def receiver - ($.account/zombie)) - - (def transfer-max-twice - (deploy - `(do - - (def nft - ~nft) - - (def transfer-count - {}) - - (defn check-transfer - ^{:callable? true} - [caller sender receiver id-set] - (reduce (fn [_ id] - (when (>= (get transfer-count - id) - 2) - (reduced (str "Token " id " has already been transferred twice")))) - nil - id-set)) - - (defn check-trusted? - ^{:callable? true} - [addr policy-key id] - (if (contains-key? #{:destroy - :transfer} - policy-key) - (= (call nft - (get-token-creator id)) - addr) - (= (call nft - (get-token-owner id)) - addr))) - - (defn create-token - ^{:callable? true} - [caller id initial-data] - (def transfer-count - (assoc transfer-count - id - 0))) - - (defn destroy-token - ^{:callable? true} - [caller id] - (def transfer-count - (dissoc transfer-count - id))) - - (defn perform-transfer - ^{:callable? true} - [caller sender receiver id-set] - (def transfer-count - (reduce (fn [tc id] - (assoc tc - id - (inc (get tc - id)))) - transfer-count - id-set)))))) - - (def data - {:name "Token" - :uri "https://www.mysite.com"}) - - (T/trx '(zero? (def token - (call nft - (create-token data - transfer-max-twice)))) - {:description "Token created."}) - - (T/trx '(= {:class transfer-max-twice - :creator *address* - :data data - :owner *address*} - (get nft/token-records - token)) - {:description "Accurate token record."}) - - (T/trx '(= transfer-max-twice - (call nft - (get-token-class token))) - {:description "Class reported."}) - - (create.check nil) - - (T/trx '(= [nft - #{token}] - (asset/transfer receiver - [nft - token])) - {:description "First transfer."}) - - (T/trx '(= [nft - #{token}] - (asset/transfer *address* - [nft - token])) - {:description "First transfer."}) - - - (T/fail.code #{:ASSERT} - '(= [nft - #{token}] - (asset/transfer receiver - [nft - token])) - {:description "Fail, already transferred twice."})))) - - - -(defn suite.policy - - ^{:doc {:description "Ensures token policies are respected."}} - - [] - - (T/group '((T/path.conj 'policy) - - (def receiver - ($.account/zombie)) - - (def someone - ($.account/zombie)) - - (T/trx '(zero? (def token - (call nft - (create-token {:name "Token" - :status false} - {:destroy :creator - :transfer :owner - :update :none - [:update :status] someone})))) - {:description "Created token with specific policies."}) - - (T/trx '(= [nft - #{token}] - (asset/transfer receiver - [nft - token])) - {:description "Token transferred."}) - - (T/trx '(call nft - (check-trusted? *address* - :destroy - token)) - {:description "Can still destroy in spite of losing ownership."}) - - (T/trx '(not (call nft - (check-trusted? receiver - :destroy - token))) - {:description "Receiver cannot destroy in spite of ownership."}) - - (T/trx '(not (call nft - (check-trusted? *address* - :transfer - token))) - {:description "Cannot transfer since not the owner."}) - - (T/trx '(call nft - (check-trusted? receiver - :transfer - token)) - {:description "New owner can transfer."}) - - (T/trx '(not (call nft - (check-trusted? *address* - :update - token))) - {:description "Cannot update."}) - - (T/trx '(not (call nft - (check-trusted? receiver - :update - token))) - {:description "Receiver cannot update either."}) - - (T/trx '(not (call nft - (check-trusted? *address* - [:update :name] - token))) - {:description "Cannot update name."}) - - (T/trx '(not (call nft - (check-trusted? receiver - [:update :name] - token))) - {:description "Receiver cannot update name either."}) - - (T/trx '(not (call nft - (check-trusted? *address* - [:update :status] - token))) - {:description "Cannot update status."}) - - (T/trx '(not (call nft - (check-trusted? receiver - [:update :status] - token))) - {:description "Receiver cannot update status either."}) - - (T/trx '(call nft - (check-trusted? someone - [:update :status] - token)) - {:description "Someone else can update status as decided."}) - - (T/trx '(= {:name "Token" - :status true} - (eval-as someone - `(call ~nft - (merge-token-data ~token - {:status true})))) - {:description "Status changed."}) - - (T/trx '(= {:name "Token" - :status true} - (call nft - (get-token-data token))) - {:description "Status permanently updated."}) - - - (T/trx '(call nft - (destroy-token token)) - {:description "Token destroyed."}) - - (T/trx '(not (asset/owns? receiver - [nft - token])) - {:description "Receiver cannot own destroyed token."}) - - (T/trx '(not (asset/owns? *address* - [nft - token])) - {:description "Original owner cannot own destroyed token eiter."}) - - (T/trx '(empty? (asset/balance nft - *address*)) - {:description "Empty balance."}) - - (T/trx '(empty? (asset/balance nft - receiver)) - {:description "Receiver has empty balance after destruction."})))) - - - -(defn suite.self - - ^{:doc {:description "Ensures basic facts about a token, without any transfer or more sophisticated operations."}} - - [] - - (T/group '((T/path.conj 'self) - - (def uri - "https://www.mysite.com") - - (def data - {:name "Token-1" - :uri uri}) - - (T/trx '(zero? (def token - (call nft - (create-token data - nil)))) - {:description "Token created."}) - - (T/trx '(= {:creator *address* - :data data - :owner *address*} - (get nft/token-records - token)) - {:description "Accurate token record."}) - - (create.check nil) - - (T/trx '(nil? (call nft - (get-token-class token))) - {:description "No class associated with token."}) - - (T/trx '(= uri - (call nft - (get-uri token))) - {:description "URI of token is retrieved."}) - - (destroy.check nil)))) - - - -(defn suite.separate-offer-accept - - ^{:doc {:description "Tests transferring a token while separating offer and accepting the offer."}} - - [] - - (separate-offer-accept ($.account/zombie))) - - - -(defn suite.separate-offer-accept.actor - - ^{:doc {:description "Like `suite.separate-offer-accept` but with an actor."}} - - [] - - (separate-offer-accept (deploy `(set-controller ~*address*)))) - - - -(defn suite.transfer - - ^{:doc {:description "Applies `transfer.mono` to a user account."}} - - [] - - (transfer.mono 'transfer - '($.account/zombie))) - - - -(defn suite.transfer.actor - - ^{:doc {:description "Applies `transfer.mono` to an actor."}} - - [] - - (transfer.mono 'transfer.actor - `(deploy - '(defn receive-asset - ^{:callable? true} - [offer _data] - (~asset/accept *caller* - offer))))) - - - -(defn suite.transfer.multi - - ^{:doc {:description "Tests transferring several tokens."}} - - [] - - (T/group '((T/path.conj 'transfer.multi) - - (def receiver - ($.account/zombie)) - - (T/trx '(zero? (def token-1 - (call nft - (create-token {:name "Token 1" - :status false} - nil)))) - {:description "Token 1 created."}) - - - (T/trx '(= 1 - (def token-2 - (call nft - (create-token {:name "Token 2" - :status false} - nil)))) - {:description "Token 2 created."}) - - (T/trx '(= 2 - (def token-3 - (call nft - (create-token {:name "Token 3" - :status false} - nil)))) - {:description "Token 3 created."}) - - (T/trx '(= [nft - #{token-1 - token-2 - token-3}] - (asset/transfer receiver - [nft - #{token-1 - token-2 - token-3}])) - {:description "Partial transfer."}) - - (transfer.check token-1) - - (transfer.check token-2) - - (transfer.check token-3)))) - - - -(defn suite.transfer.multi.partial - - ^{:doc {:description "Like `suite.transfer.multi` but not all tokens are accepted."}} - - [] - - (T/group '((T/path.conj 'transfer.multi.partial) - - (def receiver - (deploy - '(do - - (import convex.asset :as asset) - - (defn receive-asset - ^{:callable? true} - [[addr id-set] data] - (asset/accept *caller* - [addr - (let [id-min (apply min - (vec id-set))] - #{id-min - (inc id-min)})]))))) - - (T/trx '(zero? (def token-1 - (call nft - (create-token {:name "Token 1" - :status false} - nil)))) - {:description "Token 1 created."}) - - - (T/trx '(= 1 - (def token-2 - (call nft - (create-token {:name "Token 2" - :status false} - nil)))) - {:description "Token 2 created."}) - - (T/trx '(= 2 - (def token-3 - (call nft - (create-token {:name "Token 3" - :status false} - nil)))) - {:description "Token 3 created."}) - - (T/trx '(= [nft - #{token-1 - token-2}] - (asset/transfer receiver - [nft - #{token-1 - token-2 - token-3}])) - {:description "Partial transfer."}) - - (transfer.check token-1) - - (transfer.check token-2) - - (transfer.check token-3 - *address* - receiver - *address*)))) - - -;;; - - -(defn suite.main - - ^{:doc {:description "Main suite gathering other suites."}} - - [] - - (T/group '((T/path.conj 'asset.nft-tokens) - (suite.class) - (suite.class.restrict) - (suite.policy) - (suite.quantity nft) ;; From `asset/quantity/set-long` test file. - (suite.self) - (suite.separate-offer-accept) - (suite.separate-offer-accept.actor) - (suite.transfer) - (suite.transfer.actor) - (suite.transfer) - (suite.transfer.multi) - (suite.transfer.multi.partial)))) - - -;;; - - -(T/report.clear) -(suite.main) -(T/print "asset.nft-tokens") -nil diff --git a/convex-core/src/test/cvx/test/convex/asset.cvx b/convex-core/src/test/cvx/test/convex/asset.cvx deleted file mode 100644 index f26ea088c..000000000 --- a/convex-core/src/test/cvx/test/convex/asset.cvx +++ /dev/null @@ -1,167 +0,0 @@ -;; -;; -;; Test suites meant to be executed in implementations of the `convex.asset` interface. -;; -;; Meant to be deployed as a library. -;; -;; - - -;;;;;;;;;; Setup - - -(import convex.run.test :as T) -(import convex.run.trx :as $.trx) - - -(defn setup - - ^{:doc {:description "Must be called before using any of test suites from this library."}} - - [] - - ($.trx/prepend '(import convex.asset :as asset))) - - -;;;;;;;;;; Test suites meant to be used for accounts that implements the `convex.asset` interface - - -(defn suite.balance - - ^{:doc {:description "Used internally for ensuring that balance is not nil nor quantity zero."}} - - [path quantity-zero balance] - - (T/group `((T/path.conj ['convex.asset.test - 'suite.balance - ~path]) - - (T/trx '(not (nil? ~balance)) - {:description "Balance is not nil."}) - - (T/trx '(not (= ~quantity-zero - ~balance)) - {:description "Balance is not empty"})))) - - - -(defn suite.user - - ^{:doc {:description "Used internally for testing user balance and ownership."}} - - [path token user known-balance] - - (T/group `((T/path.conj ['convex.asset.test - 'suite.user - ~path]) - - (T/trx '(= ~known-balance - (asset/balance ~token - ~user)) - {:description "Balance is as expected."}) - - (T/trx '(= ~known-balance - (asset/quantity-add ~token - ~known-balance - nil) - (asset/quantity-add ~token - nil - ~known-balance)) - {:description "Adding nothing to a quantity does not change the quantity."}) - - (T/trx '(= (asset/quantity-zero ~token) - (asset/quantity-sub ~token - ~known-balance - ~known-balance)) - {:description "Subtracting a quantity from itself equals the empty value."}) - - (T/trx '(asset/owns? ~user - [~token - ~known-balance]) - {:description "User owns what is reported by `balance`."}) - - (T/trx '(asset/owns? ~user - [~token - nil]) - {:description "User owns at least \"nothing\"."})))) - - -;;; - - -(defn suite.main - - ^{:doc {:description ["Main suite which gathers all other suites from this library." - "Together, both given user owns total supply."]}} - - [token user-1 user-2] - - (T/group `((T/path.conj '[convex.asset.test - suite.main]) - (def tester - ($.account/zombie)) - - (def quantity-zero - (asset/quantity-zero ~token)) - - (T/trx '(= quantity-zero - (asset/balance ~token - tester)) - {:description "Initially, tester balance is empty."}) - - (def balance-1 - (asset/balance ~token - ~user-1)) - - (~suite.balance "user-1" - quantity-zero - balance-1) - - (def balance-2 - (asset/balance ~token - ~user-2)) - - (~suite.balance "user-2" - quantity-zero - balance-2) - - (def supply - (asset/quantity-add ~token - balance-1 - balance-2)) - - (~suite.balance "supply" - quantity-zero - supply) - - (~suite.user "user-1" - ~token - ~user-1 - balance-1) - - (~suite.user "user-2" - ~token - ~user-2 - balance-2) - - (eval-as ~user-1 - (list asset/transfer - tester - [~token - balance-1])) - - (T/trx '(= quantity-zero - (asset/balance ~token - ~user-1)) - {:description "Balance of user 1 has been emptied."}) - - (eval-as ~user-2 - (list asset/transfer - tester - [~token - balance-2])) - - (T/trx '(= supply - (asset/balance ~token - tester)) - {:description "Tester has whole supply."})))) diff --git a/convex-core/src/test/cvx/test/convex/asset/quantity/set-long.cvx b/convex-core/src/test/cvx/test/convex/asset/quantity/set-long.cvx deleted file mode 100644 index 6998f0f71..000000000 --- a/convex-core/src/test/cvx/test/convex/asset/quantity/set-long.cvx +++ /dev/null @@ -1,59 +0,0 @@ -(defn suite.quantity - - ^{:doc {:description ["Asset quantity tests for when quantities are long, which is quite common." - "Assumes `convex.asset` is imported as `asset`."] - :signature [{:params [token]}]}} - - [token] - - (T/group `((T/path.conj "convex/asset/quantity/set-long") - - (def token - ~token) - - (T/trx '(= #{} - (asset/quantity-zero token))) - - (T/trx '(= #{1 2 3 4} - (asset/quantity-add token - #{1 2} - #{3 4}))) - (T/trx '(= #{1 2 3 4} - (asset/quantity-add token - #{1 2 3} - #{2 3 4}))) - - (T/trx '(= #{1 2} - (asset/quantity-sub token - #{1 2 3} - #{3 4 5}))) - (T/trx '(= #{1 2} - (asset/quantity-add token - #{1 2} - nil))) - (T/trx '(= #{1 2} - (asset/quantity-add token - nil - #{1 2}))) - - (T/trx '(= #{1 2} - (asset/quantity-sub token - #{1 2} - nil))) - - (T/trx '(= #{} - (asset/quantity-sub token - nil - #{1 2}))) - - (T/trx '(asset/quantity-contains? token - #{1 2 3} - #{2 3})) - - (T/trx '(not (asset/quantity-contains? token - #{1 2 3} - #{3 4}))) - - (T/trx '(not (asset/quantity-contains? token - #{1 2 3} - #{4 5 6})))))) diff --git a/convex-core/src/test/cvx/test/convex/fungible.cvx b/convex-core/src/test/cvx/test/convex/fungible.cvx deleted file mode 100644 index dba458538..000000000 --- a/convex-core/src/test/cvx/test/convex/fungible.cvx +++ /dev/null @@ -1,509 +0,0 @@ -;; -;; -;; Testing `convex.fungible`. -;; -;; - - -;;;;;;;;;; Setup - - -($.stream/out! (str $.term/clear.screen - "Testing `convex.fungible`")) - - -;; Deploying test version of `convex.fungible` instead of importing stable version. -;; -($.file/read "src/main/cvx/convex/fungible.cvx") - - -(eval `(def fungible - (deploy (quote ~(cons 'do - (next $/*result*)))))) - -($.file/exec "src/test/cvx/test/convex/fungible/generic.cvx") - - -;; Requiring test suites for `convex.asset` since fungible tokens implement that interface. -;; -($.file/read "src/test/cvx/test/convex/asset.cvx") - - -(def asset.test - (deploy (cons 'do - $/*result*))) - - -(asset.test/setup) - - -;; Other imports. -;; -(import convex.asset :as asset) - - -(def T - $.test) - - -;; Default account is an actor, key is set to transform it into a user account. -;; -(set-key $.account/fake-key) - - -;;;;;;;;;; Test suites - - -(defn suite.asset - - ^{:doc {:description "Testing that the library follows the `convex.asset` interface."}} - - [] - - (T/group `((T/path.conj 'suite.asset) - - (def supply - 1000000) - - (T/trx '(address? (def token - (deploy (fungible/build-token {:supply supply})))) - {:description "Token deployed"}) - - (suite.fungible token - supply - *address*) - - ;; Transfers - - (def receiver - ($.account/zombie)) - - (T/trx '(= 500 - (asset/offer receiver - [token - 500])) - {:description "Offer receiver some tokens."}) - - (T/trx '(= 500 - (asset/get-offer token - *address* - receiver)) - {:description "First offer has been issued."}) - - (T/trx '(= 1000 - (asset/offer receiver - [token - 1000])) - {:description "Updating offer."}) - - (T/trx '(= 1000 - (asset/get-offer token - *address* - receiver)) - {:description "Offer has been updated."}) - - (T/trx '(= 250 - (eval-as receiver - `(~asset/accept ~*address* - [~token - 250]))) - {:description "Partly accepting an offer, returns current balance."}) - - (T/trx '(= 750 - (asset/get-offer token - *address* - receiver)) - {:description "Remaining offer is still valid."}) - - (T/trx '(= 1000 - (eval-as receiver - `(~asset/accept ~*address* - [~token - 750]))) - {:description "Accepting remaining offer, returns current balance."}) - - (T/trx '(= 2000 - (asset/transfer receiver - [token - 1000])) - {:description "Transfer some tokens to receiver (arity 2), returns current balance."}) - - (T/trx '(= {token 3000} - (asset/transfer receiver - {token 1000})) - {:description "Transfer some tokens to receiver (arity 3), returns current balance."}) - - (T/trx '(= (- supply - 3000) - (asset/balance token - *address*)) - {:description "Villain received tokens."}) - - (T/trx '(= 3000 - (asset/balance token - receiver)) - {:description "Balance updated after transferring to receiver."}) - - - ;; Ownership - - (def balance-receiver - (asset/balance token - receiver)) - - (T/trx '(asset/owns? receiver - [token - (dec balance-receiver)]) - {:description "Villain owns at least less than its balance."}) - - (T/trx '(asset/owns? receiver - [token - balance-receiver]) - {:description "Villain owns its balance."}) - - (T/trx '(not (asset/owns? receiver - [token - (inc balance-receiver)])) - {:description "Villain cannot own more than its balance."}) - - - ;; Token arithmetics - - (T/trx '(= 0 - (asset/quantity-zero token)) - {:description "Empty balance is 0."}) - - (T/trx '(= 110 - (asset/quantity-add token - 100 - 10)) - {:description "Adding tokens."}) - - (T/trx '(= 100 - (asset/quantity-add token - 100 - nil)) - {:description "Adding nil."}) - - (T/trx '(= 100 - (asset/quantity-add token - nil - 100)) - {:description "Adding to nil."}) - - (T/trx '(= 90 - (asset/quantity-sub token - 100 - 10)) - {:description "Subtracting tokens."}) - - (T/trx '(= 100 - (asset/quantity-sub token - 100 - nil)) - {:description "Subtracting nil."}) - - (T/trx '(zero? (asset/quantity-sub token - nil - 100)) - {:description "Subtracting from nil."}) - - (T/trx '(= 0 - (asset/quantity-sub token - 10 - 1000)) - {:description "Subtracting does not go below 0."}) - - - ;; Token comparisons - - (T/trx '(asset/quantity-contains? [token - 110] - [token - 100])) - - (T/trx '(asset/quantity-contains? [token - 110] - nil)) - - (T/trx '(asset/quantity-contains? nil - nil)) - - (T/trx '(asset/quantity-contains? token - 110 - 100)) - - (T/trx '(asset/quantity-contains? token - 110 - nil)) - - (T/trx '(asset/quantity-contains? token - nil - nil)) - - (T/trx '(not (asset/quantity-contains? [token - 100] - [token - 110]))) - - (T/trx '(not (asset/quantity-contains? nil - [token - 110]))) - - (T/trx '(not (asset/quantity-contains? token - 100 - 110))) - - (T/trx '(not (asset/quantity-contains? token - nil - 110)))))) - - - -(defn suite.build-token - - ^{:doc {:description "Builds a token and ensures good fungability behavior."}} - - [] - - (T/group '((T/path.conj 'suite.build-token) - - (T/trx '(address? (def token - (deploy (fungible/build-token {})))) - {:description "Token deployed"}) - - (def receiver - ($.account/zombie)) - - (def supply - (fungible/balance token - *address*)) - - (def amount - (long (/ supply - 10))) - - (T/trx '(= amount - (fungible/transfer token - receiver - amount)) - {:description "Transferring tokens to receiver."}) - - (T/trx '(= (- supply - amount) - (fungible/balance token - *address*)) - {:description "Balance of sender updated after transfer."}) - - (T/trx '(= amount - (fungible/balance token - receiver)) - {:description "Balance of receiver updated after transfer."}) - - (T/fail.code #{:ASSERT} - '(fungible/transfer token - receiver - -1) - {:description "Cannot transfer negative amount."}) - - (T/fail.code #{:ASSERT} - '(fungible/transfer token - receiver - (inc (fungible/balance token - *address*))) - {:description "Cannot transfer more than hold."}) - - (T/group '((T/trx '(= supply - (fungible/transfer token - receiver - (fungible/balance token - *address*))) - {:description "Transferring all tokens to receiver."}) - - (T/trx '(= 0 - (fungible/balance token - *address*)) - {:description "Sender has transferred all owned tokens."}) - - (T/trx '(= supply - (fungible/balance token - receiver)) - {:description "Receiver holds whole supply."}) - ))))) - - - -(defn suite.mint - - ^{:doc {:description "Builds a mintable token and ensures minting rules are enforced."}} - - [] - - (T/group `((T/path.conj 'suite.mint) - - (def receiver - ($.account/zombie)) - - (def supply.init - 100) - - (def supply.max - 1000) - - (def supply.mintable - (- supply.max - supply.init)) - - (T/trx `(boolean (def token - (deploy '(do - ~(fungible/build-token {:supply supply.init}) - ~(fungible/add-mint {:max-supply supply.max}))))) - {:description "Token deployed."}) - - (suite.fungible token - supply.init - *address*) - - ;; Minting - - (T/fail.code #{:ASSERT} - '(fungible/mint token (inc supply.mintable)) - {:description "Cannot mint more than max supply."}) - - (T/trx '(= supply.max - (fungible/mint token - supply.mintable)) - {:description "Minting and augmenting total supply."}) - - (T/trx '(= supply.max - (fungible/balance token - *address*)) - {:description "Minted tokens added to balance."}) - - (T/group `((T/path.conj :negative-minting) - - (T/trx '(= supply.init - (fungible/mint token - (- supply.mintable))) - {:description "Negative minting and reducing total supply."}) - - (T/trx '(= supply.init - (fungible/balance token - *address*)) - {:description "Balance adjusted after negative minting."}) - - (T/trx '(= 0 - (fungible/mint token - (- supply.init))) - {:description "Negative mint down to 0."}) - - (T/fail.code #{:ASSERT} - '(fungible/mint token - -1) - {:description "Cannot negative mint more than currently hold."}))) - - (T/group `((T/path.conj :burning) - - (T/trx '(= supply.init - (fungible/burn token - supply.mintable)) - {:description "Burning and reducing total supply."}) - - (T/trx '(= supply.init - (fungible/balance token - *address*)) - {:description "Balance adjusted after burning."}) - - (T/trx '(= 0 - (fungible/burn token - supply.init)) - {:description "Burning down to 0."}) - - (T/fail.code #{:ASSERT} - '(fungible/burn token - 1) - {:description "Cannot burn more than currently hold."}))) - (def supply.receiver - (long (/ supply.max - 5))) - - (T/trx '(= supply.receiver - (fungible/transfer token - receiver - supply.receiver)) - {:description "Transferring part of supply to receiver."}) - - (T/trx '(= supply.receiver - (fungible/balance token - receiver)) - {:description "Balance of receiver updated after transfer."}) - - (T/trx '(= (- supply.max - supply.receiver) - (fungible/burn token - supply.receiver)) - {:description "Can still burn hold tokens."}) - - (T/fail.code #{:ASSERT} - '(fungible/burn token - (inc (fungible/balance token - *address*))) - {:description "Cannot burn more than currently holding."}) - - (T/fail.code #{:ASSERT} - '(fungible/mint token - (inc supply.receiver)) - {:description "After transfer, still Cannot mint more than max supply."}) - - (T/trx '(= supply.receiver - (query (fungible/burn token - (fungible/balance token - *address*)))) - {:description "Can burn all currently hold tokens."}) - - (T/fail.code #{:TRUST} - '(eval-as receiver - `(~fungible/mint ~token - 1)) - {:description "Non-owner cannot mint anything."}) - - (T/fail.code #{:TRUST} - '(eval-as receiver - `(~fungible/mint ~token - ~(inc supply.max))) - {:description "Trust verified before amount during minting."}) - - (T/fail.code #{:TRUST} - `(eval-as receiver - `(~fungible/burn ~token - 1)) - {:description "Non-owner cannot burn anything."}) - - (T/fail.code #{:TRUST} - '(eval-as receiver - `(~fungible/burn ~token - ~(inc supply.max))) - {:description "Trust verified before amount during minting."})))) - - - -;;; - - -(defn suite.main - - ^{:doc {:description "Main suite gathering all other test suites."}} - - [] - - (T/group '((T/path.conj 'convex.fungible) - (suite.asset) - (suite.build-token) - (suite.mint)))) - - -;;; - - -(suite.main) -(T/print "convex.fungible") diff --git a/convex-core/src/test/cvx/test/convex/fungible/generic.cvx b/convex-core/src/test/cvx/test/convex/fungible/generic.cvx deleted file mode 100644 index 45809417c..000000000 --- a/convex-core/src/test/cvx/test/convex/fungible/generic.cvx +++ /dev/null @@ -1,46 +0,0 @@ -(defn suite.fungible - - ^{:doc {:description ["Reusable fungability tests with the `convex.asset` interface." - "Requires asset test suites account interned as `asset.test`."]}} - - [token supply user] - - (T/group `((T/path.conj 'suite.fungible) - - (T/trx '(= ~supply - (asset/balance ~token - ~user)) - {:description "Owns total supply."}) - - (T/trx '(= 0 - (asset/balance ~token - (deploy nil))) - {:description "Newly created account has no token."}) - - (T/trx '(do - (asset/transfer ~user - [~token - (asset/balance ~token - ~user)]) - (= ~supply - (asset/balance ~token - ~user))) - {:description "Self-transfer does not affect balance."}) - - (T/trx '(do - (asset/transfer ~user - [~token - nil]) - (= ~supply - (asset/balance ~token - ~user))) - {:description "Self-transfer of nothing does not affect balance."}) - - (asset.test/suite.main ~token - ~user - (let [user-2 ($.account/zombie)] - (asset/transfer user-2 - [~token - (long (/ ~supply - 3))]) - user-2))))) diff --git a/convex-core/src/test/cvx/test/convex/play.cvx b/convex-core/src/test/cvx/test/convex/play.cvx deleted file mode 100644 index 0c8dc6cef..000000000 --- a/convex-core/src/test/cvx/test/convex/play.cvx +++ /dev/null @@ -1,97 +0,0 @@ -;; -;; -;; Testing `convex.play`. -;; -;; - - -;;;;;;;;;; Setup - - -($.stream/out! (str $.term/clear.screen - "Testing `convex.play`")) - - -($.file/read "src/main/cvx/convex/play.cvx") - - -(def play - (deploy (cons 'do - $/*result*))) - - -(def T - $.test) - - -;;;;;;;;;; Test suites - - -(defn suite.main - - ^{:doc {:description "Only test suite."}} - - [] - - (T/group '((T/path.conj 'convex.play) - - (def env - (deploy - `(do - - (def *log* - []) - - (defn command - ^{:callable? true} - [trx] - (when-not (= trx - 42) - (def *log* - (conj *log* - trx)))) - - (defn start - ^{:callable? true} - [] - (def *log* - [:join]))))) - - - (T/group '((T/path.conj "Actor returns nil") - - (play/start env) - 1 - 2 - 3 - 42 ;; Supposed to stop at 42 - - (T/trx '(= "Stop." - $/*result*) - {:description "Stops."}) - - (T/trx '(= [:join 1 2 3] - env/*log*) - {:description "Commands executed properly."}))) - - (T/group '((T/path.conj "Stops on user request") - - (play/start env) - :a - :b - stop - - (T/trx '(= "Stop." - $/*result*) - {:description "Stops."}) - - (T/trx '(= [:join :a :b] - env/*log*) - {:description "Commands executed properly."})))))) - - -;;; - - -(suite.main) -(T/print "convex.play") diff --git a/convex-core/src/test/cvx/test/convex/registry.cvx b/convex-core/src/test/cvx/test/convex/registry.cvx deleted file mode 100644 index 4d6c4b3d8..000000000 --- a/convex-core/src/test/cvx/test/convex/registry.cvx +++ /dev/null @@ -1,153 +0,0 @@ -;; -;; -;; Testing `convex.registry`. -;; -;; - - -;;;;;;;;;; Setup - - -($.file/read "src/main/cvx/convex/registry.cvx") - - -(def registry - (deploy (concat ['do] - $/*result* - `[(do - (def cns-database - {}) - (def trust - ~(call *registry* - (cns-resolve 'convex.trust))))]))) - - - -(def T - $.test) - - -;; Easier to interact with the Trust library if operating as a user account. -;; -(set-key $.account/fake-key) - - -;;;;;;;;;; Test suites - - -(defn suite.main - - ^{:doc {:description "Only test suite."}} - - [] - - (T/group '((T/path.conj 'convex.registry) - - (T/trx '(nil? (call registry - (cns-resolve 'some.name))) - {:description "Not found."}) - - (T/trx '(address? (def addr - (deploy `(~registry/meta.set 42)))) - {:description "Actor deployed with metadata."}) - - (T/trx '(= 42 - (registry/meta.get addr)) - {:description "Actor metadata retrieved."}) - - (T/trx `(= [addr - *address*] - (get (call registry - (cns-update 'some.name - addr)) - 'some.name)) - {:description "Registering address."}) - - (T/trx '(= addr - (call registry - (cns-resolve 'some.name))) - {:description "Address registered."}) - - (def addr-2 - ($.account/zombie)) - - - (T/trx '(= [addr-2 - *address*] - (get (call registry - (cns-update 'some.name - addr-2)) - 'some.name)) - {:description "Controller can update name."}) - - (def villain - ($.account/zombie)) - - (T/fail.code #{:TRUST} - `(eval-as villain - '(call ~registry - (cns-update 'some.name - *address*))) - {:description "Cannot update existing name unless controller."}) - - (T/trx '(= addr-2 - (call registry - (cns-resolve 'some.name))) - - {:description "Original mapping left intact."}) - - (T/trx '(= [addr-2 - villain] - (get (call registry - (cns-control 'some.name - villain)) - 'some.name)) - {:description "Control transfered."}) - - (T/fail.code #{:TRUST} - '(call registry - (cns-update 'some.name - addr)) - {:description "Cannot update after transferring control."}) - - (T/fail.code #{:TRUST} - '(call registry - (cns-control 'some.name - *address*)) - {:description "Cannot transfer control when control is lost."}) - - (T/trx '(= [addr - villain] - (get (eval-as villain - `(call ~registry - (cns-update 'some.name - ~addr))) - 'some.name)) - {:description "Villain can update after gaining control."}) - - (T/trx '(= addr - (call registry - (cns-resolve 'some.name))) - {:description "Villain updated."}) - - (T/trx '(= [addr-2 - villain] - (get (eval-as villain - `(call ~registry - (cns-update 'another.name - ~addr-2))) - 'another.name)) - {:description "Villain can create new mapping."}) - - (T/trx '(= addr-2 - (call registry - (cns-resolve 'another.name))) - {:description "Villain created new mapping."})))) - - - -;;; - - -(suite.main) -(T/print "convex.registry") diff --git a/convex-core/src/test/cvx/test/convex/trust.cvx b/convex-core/src/test/cvx/test/convex/trust.cvx deleted file mode 100644 index 210e3ccda..000000000 --- a/convex-core/src/test/cvx/test/convex/trust.cvx +++ /dev/null @@ -1,390 +0,0 @@ -;; -;; -;; Testing `convex.trust`. -;; -;; - - -($.stream/out! (str $.term/clear.screen - "Testing `convex.trust`")) - - -;;;;;;;;;; Setup - - -(def T - $.test) - - -($.file/read "src/main/cvx/convex/trust.cvx") - - -(eval `(def trust - (deploy (quote ~(cons 'do - (next $/*result*)))))) - - -;; Default account is an actor, key is set to transform it into a user account. -;; -(set-key $.account/fake-key) - - -;;;;;;;;;; Test suites - - -(defn suite.blacklist - - ^{:doc {:description "Creates a blacklist and ensures access is managed accordingly."}} - - [] - - (T/group '((T/path.conj 'black-list) - - (T/trx '(address? (def bl - (deploy (trust/build-blacklist nil)))) - {:description "Blacklist deployed."}) - - (T/trx '(not (trust/trusted? bl - *address*)) - {:description "Controller is not trusted by default when no list is provided."}) - - (T/fail.code #{:CAST} - '(trust/trusted? bl - nil) - {:description "Cannot check nil."}) - - (T/fail.code #{:CAST} - '(trust/trusted? bl - []) - {:description "Cannot trust something that is not an address."}) - - (T/fail.code #{:CAST} - '(trust/trusted? nil - *address*) - {:description "List cannot be nil."}) - - (T/fail.code #{:CAST} - '(trust/trusted? [] - *address*) - {:description "List must be an address, obviously."}) - - (T/trx '(address? (def user-1 - ($.account/zombie))) - {:description "User 1 deployed."}) - - (T/trx '(trust/trusted? bl - user-1) - {:description "Cannot trust new account."}) - - (T/trx '(= #{*address* - user-1} - (call bl - (set-trusted user-1 - false))) - {:description "Removing trust for new account."}) - - (T/trx '(not (trust/trusted? bl - user-1)) - {:description "Excluded account is not trusted."}) - - (T/trx '(= #{*address*} - (call bl - (set-trusted user-1 - true))) - {:description "Restoring trust in new account."}) - - (T/trx '(trust/trusted? bl - user-1) - {:description "New account is trusted again."}) - - (T/trx '(address? (def user-2 - ($.account/zombie))) - {:description "User 2 deployed."}) - - (T/trx '(= #{*address* - user-2} - (call bl - (set-trusted user-2 - false))) - {:description "Removing trust in user 2."}) - - (T/trx '(eval-as user-1 - `(~trust/trusted? ~bl - ~user-1)) - {:description "Non-controller can check monitor for trusted address."}) - - (T/trx '(not (eval-as user-1 - `(~trust/trusted? ~bl - ~user-2))) - {:description "Non-controller can check monitor for untrusted address."}) - - (T/fail.code #{:TRUST} - '(eval-as user-1 - `(call ~bl - (set-trusted ~user-1 - false))) - {:description "Non-controller trying to remove trust for itself fails."}) - - (T/fail.code #{:TRUST} - '(eval-as user-1 - `(call ~bl - (set-trusted ~user-1 - true))) - {:description "Non-controller cannot grant trust in itself."}) - - (T/fail.code #{:TRUST} - '(eval-as user-1 - `(call ~bl - (set-trusted ~user-2 - true))) - {:description "Non-controller cannot grant trust to another account."}) - - (T/fail.code #{:TRUST} - '(eval-as user-1 - `(call ~bl - (set-trusted ~user-2 - false))) - {:description "Non-controller trying to remove trust for another account fails."}) - - (T/fail.code #{:TRUST} - '(eval-as user-1 - `(call ~bl - (set-trusted ~*address* - true))) - {:description "Non-controller cannot grant trust to trusted account."}) - - (T/fail.code #{:TRUST} - '(eval-as user-1 - `(call ~bl - (set-trusted ~*address* - false))) - {:description "Non-controller trying to remove trust for trusted account fails."})))) - - - -(defn suite.self-trust - - ^{:doc {:description "Basic truths about trust."}} - - [] - - (T/group '((T/path.conj 'self-trust) - - (T/trx '(trust/trusted? *address* - *address*) - {:description "Self trust."}) - - (T/trx '(not (trust/trusted? *address* - nil)) - {:description "Never trust \"nothing\"."}) - - (T/trx '(not (trust/trusted? *address* - :foo)) - {:description "Cannot trust something that is not an address."}) - - (T/trx '(and (nil? (account #666666)) - (not (trust/trusted? *address* - #666666))) - {:description "Cannot trust an inexistent address."})))) - - - -(defn suite.upgrade - - ^{:doc {:description "Adding and removing upgradability from an account (ie. ability to eval arbitrary code)."}} - - [] - - (T/group '((T/path.conj 'upgrade) - - (T/trx '(address? (def target - (deploy `(do - (def bar - 1) - ~(trust/add-trusted-upgrade nil))))) - {:description "Upgradable whiteliste deployed."}) - - (T/trx '(do - (call target - (upgrade '(def foo - 42))) - (= 42 - target/foo)) - {:description "Eval arbitrary code in list actor."}) - - (T/trx '(address? (def not-root - ($.account/zombie))) - {:description "Accoutn deployed."}) - - (T/fail.code #{:TRUST} - '(eval-as not-root - `(call ~target - (upgrade '(def foo - 100)))) - {:description "Non-root account cannot upgrade."}) - - (T/trx '(nil? (trust/remove-upgradability! target)) - {:description "Upgrade utilities removed (returns nil)."}) - - (T/fail.code #{:STATE} - '(call target - (updade '(def foo - 1001))) - {:description "Impossible to upgrade."}) - - (T/trx '(= 1 - target/bar) - {:description "Rest of environment is preserved after loosing upgradability."})))) - - - -(defn suite.whitelist - - ^{:doc {:description "Builds whitelist and ensures access is managed accordingly."}} - - [] - - (T/group '((T/path.conj 'white-list) - - (T/trx '(address? (def wl - (deploy (trust/build-whitelist nil)))) - {:description "Whitelist deployed."}) - - (T/trx '(trust/trusted? wl - *address*) - {:description "Controller is trusted by default when no list is provided."}) - - (T/fail.code #{:CAST} - '(trust/trusted? wl - nil) - {:description "Cannot check nil."}) - - (T/fail.code #{:CAST} - '(trust/trusted? wl - []) - {:description "Cannot trust something that is not an address."}) - - (T/fail.code #{:CAST} - '(trust/trusted? nil - *address*) - {:description "List cannot be nil."}) - - (T/fail.code #{:CAST} - '(trust/trusted? [] - *address*) - {:description "List must be an address, obviously."}) - - (T/trx '(address? (def user-1 - ($.account/zombie))) - {:description "User 1 deployed."}) - - (T/trx '(not (trust/trusted? wl - user-1)) - {:description "Cannot trust new account."}) - - (T/trx '(= #{*address* - user-1} - (call wl - (set-trusted user-1 - true))) - {:description "Trusting new account."}) - - (T/trx '(trust/trusted? wl - user-1) - {:description "New account is trusted."}) - - (T/trx '(= #{*address*} - (call wl - (set-trusted user-1 - false))) - {:description "Removing trust in new account."}) - - (T/trx '(not (trust/trusted? wl - user-1)) - {:description "New account is not trusted anymore."}) - - (T/trx '(address? (def user-2 - ($.account/zombie))) - {:description "User 2 deployed."}) - - (T/trx '(= #{*address* - user-2} - (call wl - (set-trusted user-2 - true))) - {:description "Trusting user 2."}) - - (T/trx '(not (eval-as user-1 - `(~trust/trusted? ~wl - ~user-1))) - {:description "Non-controller can check monitor for untrusted address."}) - - (T/trx '(eval-as user-1 - `(~trust/trusted? ~wl - ~user-2)) - {:description "Non-controller can check monitor for trusted address."}) - - (T/fail.code #{:TRUST} - '(eval-as user-1 - `(call ~wl - (set-trusted ~user-1 - true))) - {:description "Non-controller cannot grant trust to itself."}) - - (T/fail.code #{:TRUST} - '(eval-as user-1 - `(call ~wl - (set-trusted ~user-1 - false))) - {:description "Non-controller trying to remove trust for itself fails."}) - - (T/fail.code #{:TRUST} - '(eval-as user-1 - `(call ~wl - (set-trusted ~user-2 - true))) - {:description "Non-controller cannot grant trust to another account."}) - - (T/fail.code #{:TRUST} - '(eval-as user-1 - `(call ~wl - (set-trusted ~user-2 - false))) - {:description "Non-controller trying to remove trust for another account fails."}) - - (T/fail.code #{:TRUST} - '(eval-as user-1 - `(call ~wl - (set-trusted ~*address* - true))) - {:description "Non-controller cannot grant trust to trusted account."}) - - (T/fail.code #{:TRUST} - '(eval-as user-1 - `(call ~wl - (set-trusted ~*address* - false))) - {:description "Non-controller trying to remove trust for trusted account fails."})))) - - -;;; - - -(defn suite.main - - ^{:doc {:description "Main suite gathering all other suites."}} - - [] - - (T/group '((T/path.conj 'convex.trust) - (suite.blacklist) - (suite.self-trust) - (suite.upgrade) - (suite.whitelist)))) - - -;;; - - -(suite.main) -(T/print "convex.trust") diff --git a/convex-core/src/test/cvx/test/convex/trusted-oracle.cvx b/convex-core/src/test/cvx/test/convex/trusted-oracle.cvx deleted file mode 100644 index 5c85ea5e7..000000000 --- a/convex-core/src/test/cvx/test/convex/trusted-oracle.cvx +++ /dev/null @@ -1,109 +0,0 @@ -;; -;; -;; Testing `convex.trusted-oracle`. -;; -;; - - -;;;;;;;;;; Setup:w - - -($.file/read "src/main/cvx/convex/trusted-oracle/actor.cvx") - - -(def default-actor - (deploy (cons 'do - (next $/*result*)))) - - -($.file/read "src/main/cvx/convex/trusted-oracle.cvx") - - -(def oracle - (deploy (concat (cons 'do - (next $/*result*)) - `((def default-actor - ~default-actor))))) - - -(def T - $.test) - - -;;;;;;;;;; Test suites - - -(defn suite.main - - ^{:doc {:description "Only test suite."}} - - [] - - (T/group '((T/path.conj 'convex.trusted-oracle) - - (T/trx '(oracle/register :foo - {:a :b - :trust #{*address*}}) - {:description "Registering oracle."}) - - (T/trx '(= false - (oracle/register :foo - {:bar :baz})) - {:description "Cannot register same key twice."}) - - (T/trx '(= {:a :b - :trust #{*address*}} - (oracle/data :foo)) - {:description "Overwrite did not work."}) - - (T/trx '(= false - (oracle/finalized? :foo)) - {:description "Not yet finalized."}) - - (T/trx '(nil? (oracle/read :foo)) - {:description "Nothing to read, not yet finalized."}) - - (def villain - ($.account/zombie)) - - (T/fail.code #{:TRUST} - '(eval-as villain - `(~oracle/provide :foo - :bar)) - {:description "Villain is not trusted for providing a value."}) - - (T/trx '(nil? (oracle/read :foo)) - {:description "Vilain did not manage to provide a value."}) - - (T/trx '(= :baz - (oracle/provide :foo - :baz)) - {:description "Trusted account provides value."}) - - (T/trx '(oracle/finalized? :foo) - {:description "Key is finalized."}) - - (T/trx '(= :baz - (oracle/read :foo)) - {:description "Finalized key is read."}) - - (T/trx '(= :baz - (oracle/provide :foo - 42)) - {:description "Cannot overwrite value."}) - - (T/trx '(= :baz - (oracle/read :foo)) - {:description "Value was not overwritten."}) - - (T/fail.code #{:STATE} - '(oracle/provide :bar - 42) - {:description "Cannot provide a result for an inexistent key."})))) - - -;;; - - -(suite.main) -(T/print "convex.trusted-oracle") diff --git a/convex-core/src/test/cvx/test/torus/exchange.cvx b/convex-core/src/test/cvx/test/torus/exchange.cvx deleted file mode 100644 index aa8cc3276..000000000 --- a/convex-core/src/test/cvx/test/torus/exchange.cvx +++ /dev/null @@ -1,499 +0,0 @@ -;; -;; -;; Testing `torus.exchange`. -;; -;; - - -;;;;;;;;;; Setup - - -($.file/read "src/main/cvx/torus/exchange.cvx") - - -(def torus - (deploy (cons 'do - (next $/*result*)))) - - -(def T - $.test) - - -(import convex.asset :as asset) -(import convex.fungible :as fungible) - - -($.stream/out! (str $.term/clear.screen - "Testing `torus.exchange`")) - - -;; Generic fungiblity suites. -;; -($.file/exec "src/test/cvx/test/convex/fungible/generic.cvx") - - -;; Requiring test suites for `convex.asset` as required for generic fungibility tests. -;; -($.file/read "src/test/cvx/test/convex/asset.cvx") - - -(def asset.test - (deploy (cons 'do - $/*result*))) - - -(asset.test/setup) - - -;; Easier to buy/sell as a user account, no need to implement `receive-coin`. -;; -(set-key $.account/fake-key) - - -;;;;;;;;;; Deploying currencies - 1e6 each with 2 decimal places - - -(def GBP.token - (deploy (fungible/build-token {:supply 1000000000}))) - - -(def USD.token - (deploy (fungible/build-token {:supply 1000000000}))) - - -(def USD.market - (call torus - (create-market USD.token))) - - -;; Only market for USD at the moment. - - -;;;;;;;;;; Test suites - - -(defn suite.api - - ^{:doc {:description "Testing core API functions."}} - - [] - - (T/group '((T/path.conj 'api) - - (T/trx '(address? (def GBP.market - (call torus - (create-market GBP.token)))) - {:description "Create market for GBP."}) - - (T/trx '(= GBP.market - (torus/get-market GBP.token)) - {:description "Retrieve market for GBP."}) - - (T/trx '(= USD.market - (torus/get-market USD.token)) - {:description "Retrieve market for USD."}) - - (T/trx '(nil? (torus/price GBP.token)) - {:description "No price for GBP yet, market exists but no liquidity."}) - - (T/trx '(nil? (torus/price USD.token)) - {:description "No price for USD yet, market exists but no liquidity."}) - - ;; Liquidity - - (T/trx '(<= (- 1.0 - 0.00001) - (/ (sqrt (* 5000000.0 - 1000000000000.0)) - (torus/add-liquidity GBP.token - 5000000 - 1000000000000)) - (+ 1.0 - 0.00001)) - {:description "Initial deposit of 50K GBP token liquidity and checking result."}) - - (T/trx '(<= (- 1.0 - 0.00001) - (/ (sqrt (* 10000000.0 - 1000000000000.0)) - (torus/add-liquidity USD.token - 10000000 - 1000000000000)) - (+ 1.0 - 0.00001)) - {:description "Initial deposit of 100K USD token liquidity and checking result."}) - - ;; Prices - - (T/trx '(= 200000.0 - (torus/price GBP.token)) - {:description "50 GBP token for 1e12 CVX Gold = 2e5 CVX / Penny sterling."}) - - (T/trx '(= 100000.0 - (torus/price USD.token)) - {:description "100K USD token for 1e12 CVX Gold = 1e5 CVX / US Cent."}) - - (T/trx '(= 1.0 - (torus/price GBP.token - GBP.token)) - {:description "Rate GBP / GBP."}) - - (T/trx '(= 2.0 - (torus/price GBP.token - USD.token)) - {:description "Rate GBP / USD."}) - - (T/trx '(= 0.5 - (torus/price USD.token - GBP.token)) - {:description "Rate USD / GBP."}) - - ;; Marginal buy trades for 1 GBP / 1 USD - - (T/trx '(= 101 - (torus/buy GBP.token - 100 - GBP.token))) - - (T/trx '(= 101 - (torus/buy-quote GBP.token - 100 - GBP.token))) - - (T/trx '(= 51 - (torus/buy USD.token - 100 - GBP.token))) - - (T/trx '(= 51 - (torus/buy-quote USD.token - 100 - GBP.token))) - - (T/trx '(= 201 - (torus/buy GBP.token - 100 - USD.token))) - - (T/trx '(= 201 - (torus/buy-quote GBP.token - 100 - USD.token))) - - ;; Marginal sell trades for 1 GBD / 1 USD - - (T/trx '(= 99 - (torus/sell GBP.token - 100 - GBP.token))) - - (T/trx '(= 99 - (torus/sell-quote GBP.token - 100 - GBP.token))) - - (T/trx '(= 49 - (torus/sell USD.token - 100 - GBP.token))) - - (T/trx '(= 49 - (torus/sell-quote USD.token - 100 - GBP.token))) - - (T/trx '(= 199 - (torus/sell GBP.token - 100 - USD.token))) - - (T/trx '(= 199 - (torus/sell-quote GBP.token - 100 - USD.token))) - - ;; Failures - ;; TODO. Review, errors in original Java tests were actually failing only because of arity exceptions. - - (T/fail.code #{:LIQUIDITY} - '(torus/buy USD.token - 1000000000000000 - USD.token) - {:description "Trade too big, not enough liquidity."})))) - - - -(defn suite.deployed-currencies - - ^{:doc {:description "Testing already deployed currencies."}} - - [] - - (T/group '((T/path.conj 'deployed-currencies) - - (import currency.GBP :as GBP.default) - (import currency.USD :as USD.default) - (import torus.exchange :as torus-old) - - ;; TODO. Missing `double?` in core. See https://github.com/Convex-Dev/convex/issues/92 - ;; - (T/trx '(number? (torus-old/price GBP.default - USD.default)) - {:description "Price of USD per GBP token."})))) - - - -(defn suite.initial-token-market - - ^{:doc {:description "Testing common operations (adding liquidity, buy, sell, ...) on a new token market."}} - - [] - - (T/group '((T/path.conj 'initial-token-market) - - (T/trx '(nil? (call USD.market - (price))) - {:description "No price since zero liquidity."}) - - (T/trx '(= 20000000 - (asset/offer USD.market - [USD.token - 20000000])) - {:description "Initial token offering for market (200K USD)."}) - - (T/trx '(= 20000000 - (asset/get-offer USD.token - *address* - USD.market)) - {:description "Accept initial offer."}) - - ;; Adding liquidity. - - (T/trx '(do - (def shares.initial - (call USD.market - 1000000000000 - (add-liquidity 10000000))) - true) - {:description "Initial deposit of 100K USD for 1000 CVX Gold."}) - - (T/trx '(= 10000000 - (asset/balance USD.token - USD.market)) - {:description "Market has balance of 100K USD."}) - - (T/trx '(= 1000000000000 - (balance USD.market)) - {:description "Market has CVX balance of 1000 Gold."}) - - (T/trx '(= shares.initial - (asset/balance USD.market - *address*)) - {:description "Initial pool shares, accessible as a fungible asset balance."}) - - (T/trx '(= 10000000 - (asset/get-offer USD.token - *address* - USD.market)) - {:description "Consumed half the full offer of tokens."}) - - (T/trx '(= 100000.0 - (call USD.market - (price))) - {:description "Price should be 100000 CVX / US Cent."}) - - ;; More liquidity. - - (T/trx '(do - (def shares.new - (call USD.market - 1000000000000 - (add-liquidity 10000000))) - true) - {:description "Initial deposit of 100K USD for 1000 CVX Gold."}) - - (T/trx '(= 20000000 - (asset/balance USD.token - USD.market)) - {:description "Market has balance of 100K USD."}) - - (T/trx '(= (+ shares.initial - shares.new) - (asset/balance USD.market - *address*)) - {:description "New pool shares, accessible as a fungible asset balance."}) - - (T/trx '(= 100000.0 - (call USD.market - (price))) - {:description "Price remains unchanged with new pool."}) - - (T/trx '(zero? (asset/get-offer USD.token - *address* - USD.market)) - {:description "Whole offer consumed."}) - - ;; Withdraw half of liquidity - - (def balance-before-withdrawal - *balance*) - - (T/trx '(= shares.new - (call USD.market - (withdraw-liquidity shares.new))) - {:description "Withdrawing half of liquidify."}) - - (T/trx '(= shares.initial - (asset/balance USD.market - *address*)) - {:description "Remaining liquidity."}) - - (T/trx '(= 10000000 - (asset/balance USD.token - USD.market))) - - (T/trx '(= 990000000 - (asset/balance USD.token - *address*))) - - (T/trx '(> *balance* - balance-before-withdrawal)) - - (T/trx '(= 1000000000000 - (balance USD.market)) - {:description "CVX balance of market back to start."}) - - ;; Generic fungible tests on shares - - (suite.fungible USD.market - shares.initial - *address*) - - ;; Buy half of all tokens (50K USD) - - (T/trx '(long? (def cvx.paid - (call USD.market - *balance* - (buy-tokens 5000000)))) - {:description "Buy half of all tokens."}) - - (T/trx '(> cvx.paid - 1000000000000) - {:description "Should cost more than pool CVX balance after fee."}) - - (T/trx '(< cvx.paid - 1100000000000) - {:description "Should cost less than 10% fee."}) - - (T/trx '(= 5000000 - (asset/balance USD.token - USD.market)) - {:description "Half tokens remaining in the market."}) - - (T/trx '(= 995000000 - (asset/balance USD.token - *address*))) - - (T/trx '(= shares.initial - (asset/balance USD.market - *address*))) - - ;; Sell back tokens (50K USD) - - (T/trx '(= 5000000 - (asset/offer USD.market - [USD.token - 5000000])) - {:description "Sell back tokens."}) - - (T/trx '(long? (def cvx.gained - (call USD.market - (sell-tokens 5000000))))) - - (T/trx '(> cvx.gained - 900000000000) - {:description "Should gain most of money bach."}) - - (T/trx '(< cvx.gained - cvx.paid) - {:description "Gain less than cost because of fees."}) - - (T/trx '(= 10000000 - (asset/balance USD.token - USD.market))) - - (T/trx '(= 990000000 - (asset/balance USD.token - *address*))) - - (T/trx '(= shares.initial - (asset/balance USD.market - *address*))) - - ;; Withdraw all liquidity - - (T/trx '(long? (def shares.remaining - (asset/balance USD.market - *address*)))) - - (T/trx '(> shares.remaining - 0)) - - (T/trx '(= shares.remaining - (torus/withdraw-liquidity USD.token - shares.remaining)) - {:description "Withdraw all remaining shares."}) - - (T/trx '(zero? (asset/balance USD.market - *address*)) - {:description "No shares left."}) - - (T/trx '(zero? (asset/balance USD.token - USD.market)) - {:description "No USD tokens left in liquidity pool."}) - - (T/trx '(zero? (balance USD.market)) - {:description "No CVX left in liquidity pool."})))) - - - -(defn suite.missing-market - - ^{:doc {:description "Testing facts about markets which do not exist or have no liquidity."}} - - [] - - (T/group '((T/path.conj 'missing-market) - - (T/trx '(nil? (torus/get-market GBP.token)) - {:description "There is no market for GBP yet."}) - - (T/trx '(nil? (torus/price #4242424242)) - {:description "No price for a market that does not exist."}) - - (T/trx '(nil? (torus/price GBP.token)) - {:description "No price for a market that does not exist, even if token exists."})))) - - -;;; - - -(defn suite.main - - ^{:doc {:description "Main suite gathering all other suites."}} - - [] - - (T/group '((T/path.conj 'torus.exchange) - (suite.api) - (suite.deployed-currencies) - (suite.initial-token-market) - (suite.missing-market)))) - - -;;;;;;;;;; - - -(suite.main) -(T/print "torus.exchange") diff --git a/convex-core/src/test/java/convex/actors/ActorsTest.java b/convex-core/src/test/java/convex/actors/ActorsTest.java deleted file mode 100644 index d9fc62895..000000000 --- a/convex-core/src/test/java/convex/actors/ActorsTest.java +++ /dev/null @@ -1,256 +0,0 @@ -package convex.actors; - -import static convex.core.lang.TestState.eval; -import static convex.core.lang.TestState.evalB; -import static convex.core.lang.TestState.evalL; -import static convex.core.lang.TestState.step; -import static convex.test.Assertions.assertArityError; -import static convex.test.Assertions.assertAssertError; -import static convex.test.Assertions.assertCVMEquals; -import static convex.test.Assertions.assertCastError; -import static convex.test.Assertions.assertFundsError; -import static convex.test.Assertions.assertStateError; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; - -import org.junit.jupiter.api.Test; - -import convex.core.data.Address; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.init.InitTest; -import convex.core.lang.Context; -import convex.core.lang.Core; -import convex.core.lang.Symbols; -import convex.core.lang.TestState; -import convex.core.util.Utils; - -public class ActorsTest { - - @Test public void testDeployAndCall() { - Context ctx=TestState.step("(def caddr (deploy '(let [n 10] (defn getter ^{:callable? true} [] n) (defn hidden [] nil) (defn plus ^{:callable? true} [x] (+ n x)))))"); - - assertEquals(Address.class,ctx.getResult().getClass()); - - assertEquals(10L,evalL(ctx,"(call caddr (getter))")); - assertEquals(14L,evalL(ctx,"(call caddr (plus 4))")); - - assertFalse(evalB(ctx,"(callable? caddr 'foo)")); - assertTrue(evalB(ctx,"(callable? caddr 'getter)")); - - assertStateError(step(ctx,"(call caddr (bad-symbol 2))")); - assertStateError(step(ctx,"(call caddr (hidden 2))")); - assertArityError(step(ctx,"(call caddr (getter 2))")); - assertArityError(step(ctx,"(call caddr (plus))")); - assertFundsError(step(ctx,"(call caddr 1000000000000000000 (plus))")); - assertCastError(step(ctx,"(call caddr (plus :foo))")); - } - - @Test public void testSimpleDeploys() { - assertTrue(evalB("(address? (deploy 1))")); - } - - @Test public void testDeployFailures() { - assertArityError(step("(deploy)")); - assertArityError(step("(deploy 1 2)")); - - assertArityError(step("(deploy '(if))")); - } - - @Test public void testUserAsActor() { - Context ctx=step("(do (defn foo ^{:callable? true} [] *caller*) (defn bar [] nil) (def z 1))"); - assertEquals(InitTest.HERO,eval(ctx,"(call *address* (foo))")); - assertStateError(step(ctx,"(call *address* (non-existent-function))")); - assertStateError(step(ctx,"(call *address* (bar))")); - assertStateError(step(ctx,"(call *address* (z))")); - } - - @Test public void testNotActor() { - assertFalse(evalB("(actor? *address*)")); - assertFalse(evalB("(callable? *address* 'foo)")); - assertStateError(TestState.step("(call *address* (not-a-function))")); - } - - @Test public void testMinimalContract() { - Context ctx=TestState.step("(def caddr (deploy '(do)))"); - Address a=(Address) ctx.getResult(); - assertNotNull(a); - - assertFalse(evalB(ctx,"(callable? caddr 'foo)")); - - assertEquals(Core.COUNT,ctx.lookup(Symbols.COUNT).getValue()); - assertNull(ctx.getAccountStatus(a).getEnvironmentValue(Symbols.FOO)); - } - - @Test public void testTokenContract() throws IOException { - String VILLAIN=InitTest.VILLAIN.toHexString(); - String HERO=InitTest.HERO.toHexString(); - - // setup address for this scene - Context ctx=TestState.step("(do (def HERO (address \""+HERO+"\")) (def VILLAIN (address \""+VILLAIN+"\")))"); - - // Technique of constructing a contract using a String - String contractString=Utils.readResourceAsString("contracts/token.con"); - ctx=TestState.step(ctx,"(def my-token (deploy ("+contractString+" 101 1000 HERO)))"); // contract initialisation args - - assertEquals(1000L,evalL(ctx,"(call my-token (balance *address*))")); - assertEquals(0L,evalL(ctx,"(call my-token (balance VILLAIN))")); - ctx=TestState.step(ctx,"(call my-token (transfer VILLAIN 10))"); - ctx=TestState.step(ctx,"(call my-token (transfer HERO 100))"); // should have no effect - final Context fctx=ctx; // save context for later tests - - assertEquals(990L,evalL(fctx,"(call my-token (balance *address*))")); - assertEquals(10L,evalL(fctx,"(call my-token (balance VILLAIN))")); - - assertEquals(1000L,evalL(fctx,"(call my-token (total-supply))")); - - assertTrue(evalB(fctx,"(actor? my-token)")); - assertFalse(evalB(fctx,"(actor? HERO)")); - assertFalse(evalB(fctx,"(actor? :foo)")); - - // some tests for contract safety - assertAssertError(TestState.step(fctx,"(call my-token (transfer VILLAIN 1000))")); - assertAssertError(TestState.step(fctx,"(call my-token (transfer VILLAIN -1))")); - assertAssertError(TestState.step(fctx,"(call my-token (transfer nil 10))")); - assertStateError(TestState.step(fctx,"(call my-token (bad-function))")); - } - - - @Test public void testHelloContract() throws IOException { - Context ctx=TestState.step("(do )"); - - // Technique for deploying contract with a quoted form - String contractString=Utils.readResourceAsString("contracts/hello.con"); - ctx=TestState.step(ctx,"(def hello (deploy (quote "+contractString+")))"); - - ctx=TestState.step(ctx,"(call hello (greet \"Nikki\"))"); - assertEquals("Hello Nikki",ctx.getResult().toString()); - - ctx=TestState.step(ctx,"(call hello (greet \"Nikki\"))"); - assertEquals("Welcome back Nikki",ctx.getResult().toString()); - - ctx=TestState.step(ctx,"(call hello (greet \"Alice\"))"); - assertEquals("Hello Alice",ctx.getResult().toString()); - - } - - @Test public void testFundingContract() throws IOException { - Context ctx=TestState.step("(do )"); - - Address addr=ctx.getAddress(); - - String contractString=Utils.readResourceAsString("contracts/funding.con"); - ctx=TestState.step(ctx,"(def funcon (deploy '"+contractString+"))"); - assertFalse(ctx.isExceptional()); - Address caddr=(Address) ctx.getResult(); - long initialBalance=ctx.getBalance(addr); - - { - // just test return of the correct *offer* value - ctx=TestState.step(ctx,"(call funcon 1234 (echo-offer))"); - assertCVMEquals(1234,ctx.getResult()); - assertEquals(initialBalance,ctx.getBalance(addr)); - assertEquals(TestState.TOTAL_FUNDS,ctx.getState().computeTotalFunds()); - } - - { - // test accepting half of funds - final Context rctx=TestState.step(ctx,"(call funcon 1000 (accept-quarter))"); - assertCVMEquals(250,rctx.getResult()); - assertEquals(250,rctx.getBalance(caddr)); - - assertEquals(initialBalance-250,rctx.getBalance(addr)); - assertEquals(TestState.TOTAL_FUNDS,rctx.getState().computeTotalFunds()); - } - - { - // test accepting all funds - final Context rctx=TestState.step(ctx,"(call funcon 1237 (accept-all))"); - assertCVMEquals(1237,rctx.getResult()); - assertEquals(1237,rctx.getBalance(caddr)); - - assertEquals(initialBalance-1237,rctx.getBalance(addr)); - assertEquals(TestState.TOTAL_FUNDS,rctx.getState().computeTotalFunds()); - } - - { - // test accepting zero funds - final Context rctx=TestState.step(ctx,"(call funcon 1237 (accept-zero))"); - assertCVMEquals(0,rctx.getResult()); - assertEquals(0,rctx.getBalance(caddr)); - - assertEquals(initialBalance,rctx.getBalance(addr)); - assertEquals(TestState.TOTAL_FUNDS,rctx.getState().computeTotalFunds()); - } - - { - // test contract that accepts funds then rolls back - final Context rctx=TestState.step(ctx,"(call funcon 1237 (accept-rollback))"); - assertEquals(Keywords.FOO,rctx.getResult()); - assertEquals(0,rctx.getBalance(caddr)); - assertEquals(0,rctx.getOffer()); - - assertEquals(initialBalance,rctx.getBalance(addr)); - assertEquals(TestState.TOTAL_FUNDS,rctx.getState().computeTotalFunds()); - } - - { - // test contract that accepts funds repeatedly - final Context rctx=TestState.step(ctx,"(call funcon 1337 (accept-repeat))"); - assertCVMEquals(0,rctx.getResult()); // final offer echoed back - assertEquals(1337,rctx.getBalance(caddr)); - - assertEquals(initialBalance-1337,rctx.getBalance(addr)); - assertEquals(TestState.TOTAL_FUNDS,rctx.getState().computeTotalFunds()); - } - - { - // test contract that forwards funds to self - final Context rctx=TestState.step(ctx,"(call funcon 1337 (accept-forward))"); - assertCVMEquals(1337,rctx.getResult()); // result of forward to accept-all - assertEquals(1337,rctx.getBalance(caddr)); - - assertEquals(initialBalance-1337,rctx.getBalance(addr)); - assertEquals(TestState.TOTAL_FUNDS,rctx.getState().computeTotalFunds()); - } - - // test *offer* restored after send - assertEquals(0,evalL(ctx,"(do (call funcon 1237 (accept-zero)) *offer*)")); - - // test *offer* in contract with no send - assertEquals(0,evalL(ctx,"(call funcon (echo-offer))")); - } - - - - @Test public void testExceptionContract() throws IOException { - Context ctx=TestState.step("(do )"); - - String contractString=Utils.readResourceAsString("contracts/exceptional.con"); - ctx=TestState.step(ctx,"(def ex (deploy '"+contractString+"))"); - - ctx=TestState.step(ctx,"(call ex (halt-fn \"Jenny\"))"); - assertEquals("Jenny",ctx.getResult().toString()); - - // calling this will break the fragile definition, but then rollback to restore it - ctx=TestState.step(ctx,"(call ex (rollback-fn \"Alice\"))"); - assertEquals("Alice",ctx.getResult().toString()); - - ctx=TestState.step(ctx,"(call ex (get-fragile))"); - assertEquals(Keyword.create("ok"),ctx.getResult()); - - // Calling this should break the fragile definition permanently - ctx=TestState.step(ctx,"(call ex (break-fn \"Lana\"))"); - assertEquals("Lana",ctx.getResult().toString()); - - ctx=TestState.step(ctx,"(call ex (get-fragile))"); - assertEquals(Keyword.create("broken"),ctx.getResult()); - - } - -} diff --git a/convex-core/src/test/java/convex/actors/PredictionMarketTest.java b/convex-core/src/test/java/convex/actors/PredictionMarketTest.java deleted file mode 100644 index 45178215c..000000000 --- a/convex-core/src/test/java/convex/actors/PredictionMarketTest.java +++ /dev/null @@ -1,168 +0,0 @@ -package convex.actors; - -import static convex.test.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -import java.io.IOException; - -import org.junit.jupiter.api.Test; - -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.data.Maps; -import convex.core.lang.ACVMTest; -import convex.core.lang.Context; -import convex.core.lang.RT; -import convex.core.lang.TestState; -import convex.core.util.Utils; - -public class PredictionMarketTest extends ACVMTest { - - protected PredictionMarketTest() { - super(TestState.STATE); - } - - @SuppressWarnings("rawtypes") - private T evalCall(Context ctx,Address addr, long offer, String name, Object... args) { - Context rctx=doCall(ctx,addr, offer, name, args); - return RT.jvm(rctx.getResult()); - } - - @SuppressWarnings("unchecked") - private Context doCall(Context ctx,Address addr, long offer, String name, Object... args) { - int n=args.length; - ACell[] cvmArgs=new ACell[n]; - for (int i=0; i rctx=ctx.actorCall(addr, offer, name, cvmArgs); - return (Context) rctx; - } - - @SuppressWarnings("rawtypes") - @Test - public void testPredictionContract() throws IOException { - String contractString = Utils.readResourceAsString("lab/prediction-market.cvx"); - - // Run code to initialise actor with [oracle oracle-key outcomes] - Context ctx = context(); - ctx=step(contractString); - assertNotError(ctx); - ctx=step(ctx,"(deploy (build-prediction-market *address* :bar #{true,false}))"); - assertNotError(ctx); - - Address addr = (Address) ctx.getResult(); - assertNotNull(addr); - ctx = step(ctx, "(def caddr " + addr + ")"); - assertFalse(ctx.isExceptional()); - - // tests of bonding curve function with empty stakes - assertEquals(0.0, (double)evalCall(ctx,addr, 0L, "bond", Maps.empty()), 0.01); - - // bonding curve point with one staked outcome - assertEquals(10.0, evalCall(ctx,addr, 0L, "bond", Maps.of(true, 10L)), 0.01); - - // two staked outcomes - assertEquals(5.0, evalCall(ctx,addr, 0L, "bond", Maps.of(true, 3L, false, 4L)), 0.01); - - long initalBal=ctx.getBalance(HERO); - { // stake on, stake off..... - // first we stake on the 'true' outcome - Context rctx1 = doCall(ctx,addr, 10L, "stake", true, 10L); - assertCVMEquals(10L, rctx1.getResult()); - assertEquals(10L, initalBal- rctx1.getBalance(HERO)); - assertEquals(1.0, evalD(rctx1, "(call caddr (price true))")); // should be exact price 100% - assertEquals(0.0, evalD(rctx1, "(call caddr (price false))")); // should be exact price 0% - - // stake on other outcome. Note that we offer too much funds, but this won't be - // accepted so no issue. - Context rctx2 = doCall(rctx1,addr, 10L, "stake", false, 10L); - assertCVMEquals(4L, rctx2.getResult()); - assertEquals(14L, initalBal - rctx2.getBalance(HERO)); - assertEquals(TestState.TOTAL_FUNDS, rctx2.getState().computeTotalFunds()); - - // halve stakes - Context rctx3 = doCall(rctx2,addr, 10L, "stake", false, 5L); - rctx3 = doCall(rctx3,addr, 10L, "stake", true, 5L); - assertEquals(7L, initalBal - rctx3.getBalance(HERO)); - assertEquals(0.5, evalD(rctx3, "(call caddr (price true))"), 0.1); // approx price given rounding - - // zero one stake - Context rctx4 = doCall(rctx3,addr, 10L, "stake", false, 0L); - assertCVMEquals(-2L, rctx4.getResult()); // refund of 2 - assertEquals(5L, initalBal - rctx4.getBalance(HERO)); - - // Exit market - Context rctx5 =doCall(rctx4,addr, 10L, "stake", true, 0L); - assertCVMEquals(-5L, rctx5.getResult()); // refund of 5 - assertEquals(0L, initalBal - rctx5.getBalance(HERO)); - assertEquals(TestState.TOTAL_FUNDS, rctx2.getState().computeTotalFunds()); - } - - { // underfunded stake request - Context rctx1 = doCall(ctx,addr, 5L, "stake", true, 10L); - assertStateError(rctx1); // TODO: what is right error type? - assertEquals(0L, initalBal - rctx1.getBalance(HERO)); - } - - { // negative stake request - Context rctx1 = doCall(ctx,addr, 5L, "stake", true, -10L); - assertAssertError(rctx1); // TODO: what is right error type? - assertEquals(0L, initalBal - rctx1.getBalance(HERO)); - } - } - - @Test - public void testPayouts() throws IOException { - // setup address for this little play - Context ctx = step("(do (def HERO " + HERO + ") (def VILLAIN " +VILLAIN + ") )"); - - ctx = step("(import convex.trusted-oracle :as oaddr)"); - - // call to create oracle with key :bar and current address (HERO) trusted - ctx = step(ctx, "(oaddr/register :bar {:trust #{*address*}})"); - - // deploy a prediction market using the oracle - String contractString = Utils.readResourceAsString("lab/prediction-market.cvx"); - ctx=step(ctx,"(deploy ("+contractString+" oaddr :bar #{true,false}))"); - Address pmaddr = (Address) ctx.getResult(); - ctx = step(ctx, "(def pmaddr " + pmaddr + ")"); - ctx = stepAs(VILLAIN, ctx, "(def pmaddr "+pmaddr+")"); - - // initial state checks - assertEquals(false,evalB(ctx, "(call pmaddr (finalized?))")); - assertEquals(0L, evalL(ctx, "(balance pmaddr)")); - - { // Act 1. Two players stake. our Villain wins this time.... - Context c = ctx; - c = step(c, "(call pmaddr 5000 (stake true 4000))"); - c = stepAs(VILLAIN, c, "(call pmaddr 5000 (stake false 3000))"); - assertEquals(5000L, c.getBalance(pmaddr)); - - assertFalse(evalB(c, "(call pmaddr (finalized?))")); - assertEquals(0.64, evalD(c, "(call pmaddr (price true))"), 0.0001); // 64% chance on true. Looks a good bet - assertNull(eval(c, "(call pmaddr (payout))")); - - // But alas, our hero is thwarted... - c = step(c, "(oaddr/provide :bar false)"); - assertCVMEquals(Boolean.FALSE, c.getResult()); - - // collect payouts - c = step(c, "(call pmaddr (payout))"); - assertCVMEquals(0L, c.getResult()); - assertEquals(HERO_BALANCE - 4000, c.getBalance(HERO)); - - c = stepAs(VILLAIN, c, "(call pmaddr (payout))"); - assertCVMEquals(5000L, c.getResult()); - assertEquals(VILLAIN_BALANCE + 4000, c.getBalance(VILLAIN)); - - assertEquals(0L, c.getBalance(pmaddr)); - assertEquals(TestState.TOTAL_FUNDS, c.getState().computeTotalFunds()); - } - } - -} diff --git a/convex-core/src/test/java/convex/actors/RegistryTest.java b/convex-core/src/test/java/convex/actors/RegistryTest.java deleted file mode 100644 index 00005ac48..000000000 --- a/convex-core/src/test/java/convex/actors/RegistryTest.java +++ /dev/null @@ -1,111 +0,0 @@ -package convex.actors; - -import static convex.test.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -import java.io.IOException; - -import org.junit.jupiter.api.Test; - -import convex.core.data.ACell; -import convex.core.data.AHashMap; -import convex.core.data.Address; -import convex.core.data.Keyword; -import convex.core.data.Maps; -import convex.core.data.Symbol; -import convex.core.init.Init; -import convex.core.init.InitTest; -import convex.core.lang.ACVMTest; -import convex.core.lang.Context; -import convex.test.Samples; - -public class RegistryTest extends ACVMTest { - - protected RegistryTest() throws IOException { - super(InitTest.BASE); - } - - static final Address REG = Init.REGISTRY_ADDRESS; - - Context INITIAL_CONTEXT=context(); - - @Test - public void testRegistryContract() throws IOException { - // TODO: think about whether we want this in initial state - // String contractString=Utils.readResourceAsString("contracts/registry.con"); - // Object - // cfn=CoreTest.INITIAL_CONTEXT.eval(Reader.read(contractString)).getResult(); - // Context ctx=CoreTest.INITIAL_CONTEXT.deployContract(cfn); - // Address addr=(Address) ctx.getResult(); - - AHashMap ddo = Maps.of(Keyword.create("name"), "Bob"); - Context ctx = INITIAL_CONTEXT.actorCall(REG, 0, Symbol.create("register"), ddo); - assertEquals(ddo, ctx.actorCall(REG, 0, "lookup", ctx.getAddress()).getResult()); - } - - @Test - public void testRegistryCNS() throws IOException { - Context ctx=INITIAL_CONTEXT.fork(); - - assertEquals(REG,eval(ctx,"(call *registry* (cns-resolve 'convex.registry))")); - } - - @Test - public void testRegistryCNSUpdate() throws IOException { - Context ctx=INITIAL_CONTEXT.fork(); - - assertNull(eval(ctx,"(call *registry* (cns-resolve 'convex.test.foo))")); - - // Real Address we want for CNS mapping - final Address badAddr=Samples.BAD_ADDRESS; - - ctx=step(ctx,"(call *registry* (cns-update 'convex.test.foo "+badAddr+"))"); - assertNobodyError(ctx); - - final Address realAddr=Address.create(1); // Init address, FWIW - ctx=step(ctx,"(call *registry* (cns-update 'convex.test.foo "+realAddr+"))"); - assertNotError(ctx); - - assertEquals(realAddr,eval(ctx,"(call *registry* (cns-resolve 'convex.test.foo))")); - - { // Check VILLAIN can't steal CNS mapping - Context c=ctx.forkWithAddress(VILLAIN); - - // VILLAIN shouldn't be able to use update on existing CNS mapping - assertTrustError(step(c,"(call *registry* (cns-update 'convex.test.foo *address*))")); - - // original mapping should be held - assertEquals(realAddr,eval(c,"(call *registry* (cns-resolve 'convex.test.foo))")); - } - - { // Check Transfer of control to VILLAIN - Context c=step(ctx,"(call *registry* (cns-control 'convex.test.foo "+VILLAIN+"))"); - - // HERO shouldn't be able to use update or control any more - assertTrustError(step(c,"(call *registry* (cns-update 'convex.test.foo *address*))")); - assertTrustError(step(c,"(call *registry* (cns-control 'convex.test.foo *address*))")); - - // Switch to VILLAIN - c=c.forkWithAddress(VILLAIN); - - // Change mapping - c=step(c,"(call *registry* (cns-update 'convex.test.foo *address*))"); - assertNotError(c); - assertEquals(VILLAIN,eval(c,"(call *registry* (cns-resolve 'convex.test.foo))")); - } - - { // Check VILLAIN can create new mapping - // TODO probably shouldn't be free-for-all? - - Context c=ctx.forkWithAddress(VILLAIN); - - // VILLAIN shouldn't be able to use update on existing CNS mapping - c=step(c,"(call *registry* (cns-update 'convex.villain *address*))"); - assertNotError(c); - - // original mapping should be held - assertEquals(VILLAIN,eval(c,"(call *registry* (cns-resolve 'convex.villain))")); - } - } -} diff --git a/convex-core/src/test/java/convex/actors/TorusTest.java b/convex-core/src/test/java/convex/actors/TorusTest.java deleted file mode 100644 index 26a04ef9b..000000000 --- a/convex-core/src/test/java/convex/actors/TorusTest.java +++ /dev/null @@ -1,251 +0,0 @@ -package convex.actors; - -import static convex.test.Assertions.assertError; -import static convex.test.Assertions.assertNotError; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -import convex.core.data.Address; -import convex.core.data.prim.CVMDouble; -import convex.core.init.InitTest; -import convex.core.lang.ACVMTest; -import convex.core.lang.Context; -import convex.core.lang.RT; -import convex.core.util.Utils; -import convex.lib.FungibleTest; - -public class TorusTest extends ACVMTest { - Context INITIAL=context(); - - protected TorusTest() { - super(InitTest.STATE); - - try { - Context ctx=INITIAL; - ctx=step(ctx,"(import convex.fungible :as fun)"); - - ctx=step(ctx,"(import convex.asset :as asset)"); - - // Deploy currencies for testing (10m each, 2 decimal places) - ctx=step(ctx,"(def USD (deploy (fun/build-token {:supply 1000000000})))"); - USD=(Address) ctx.getResult(); - //System.out.println("USD deployed Address = "+USD); - ctx=step(ctx,"(def GBP (deploy (fun/build-token {:supply 1000000000})))"); - GBP=(Address) ctx.getResult(); - - // Deploy Torus actor itself - ctx=ctx.withJuice(INITIAL_JUICE); - - ctx= step(ctx,"(def TORUS (import torus.exchange :as torus))"); - TORUS=(Address)ctx.getResult(); - assertNotNull(ctx.getAccountStatus(TORUS)); - //System.out.println("Torus deployed Address = "+TORUS); - - // Deploy USD market. No market for GBP yet! - ctx= step(ctx,"(call TORUS (create-market USD))"); - USD_MARKET=(Address)ctx.getResult(); - INITIAL= ctx.withResult(TORUS).withJuice(INITIAL_JUICE); - } catch (Throwable e) { - e.printStackTrace(); - throw Utils.sneakyThrow(e); - } - } - - Address USD = null; - Address GBP = null; - Address TORUS = null; - Address USD_MARKET = null; - - static { - - } - - @Test public void testMissingMarket() { - Context ctx=INITIAL.fork(); - - assertNull(eval(ctx,"(torus/get-market GBP)")); - - // price should be null for missing markets, regardless of whether or not a token exists - assertNull(eval(ctx,"(torus/price GBP)")); - assertNull(eval(ctx,"(torus/price #789798789)")); - - } - - @Test public void testDeployedCurrencies() { - Context ctx=INITIAL.fork(); // Initial test context - ctx=step(ctx,"(import torus.exchange :as torus)"); - ctx= step(ctx,"(def GBP (import currency.GBP :as GBP))"); - ctx= step(ctx,"(def USD (import currency.USD :as USD))"); - assertNotNull(ctx.getResult()); - ctx= step(ctx,"(torus/price GBP USD)"); - assertTrue(ctx.getResult() instanceof CVMDouble); - - ctx= step(ctx,"(torus/price GBP USD)"); - - } - - - @Test public void testTorusAPI() { - Context ctx=INITIAL.fork(); - - // Deploy GBP market. - ctx= step(ctx,"(def GBPM (call TORUS (create-market GBP)))"); - Address GBP_MARKET=(Address)ctx.getResult(); - assertNotNull(GBP_MARKET); - - // Check we can access the USD market - ctx= step(ctx,"(def USDM (torus/get-market USD))"); - assertEquals(USD_MARKET,ctx.getResult()); - - // Prices should be null with no markets - assertNull(eval(ctx,"(torus/price USD)")); - assertNull(eval(ctx,"(torus/price GBP)")); - - // ============================================================ - // FIRST TEST: Initial deposit of $100k USD liquidity, £50k GBP liquidity - // Deposit some liquidity $100,000 for 1000 Gold = $100 price = 100000 CVX / US Cent - ctx=step(ctx,"(torus/add-liquidity USD 10000000 1000000000000)"); - assertEquals(1.0,Math.sqrt(10000000.0*1000000000000.0)/(long)RT.jvm(ctx.getResult()),0.00001); - ctx=step(ctx,"(torus/add-liquidity GBP 5000000 1000000000000)"); - assertEquals(1.0,Math.sqrt(5000000.0*1000000000000.0)/(long)RT.jvm(ctx.getResult()),0.00001); - - // ============================================================ - // SECOND TEST: Check prices - assertEquals(100000.0,evalD(ctx,"(torus/price USD)")); - assertEquals(200000.0,evalD(ctx,"(torus/price GBP)")); - - assertEquals(1.0,evalD(ctx,"(torus/price GBP GBP)")); - assertEquals(0.5,evalD(ctx,"(torus/price USD GBP)")); - assertEquals(2.0,evalD(ctx,"(torus/price GBP USD)")); - - // ============================================================ - // THIRD TEST: Check marginal buy trades for $1 / £1 - assertEquals(101,evalL(ctx,"(torus/buy GBP 100 GBP)")); - assertEquals(101,evalL(ctx,"(torus/buy-quote GBP 100 GBP)")); - assertEquals(51,evalL(ctx,"(torus/buy-quote USD 100 GBP)")); - assertEquals(51,evalL(ctx,"(torus/buy USD 100 GBP)")); - assertEquals(201,evalL(ctx,"(torus/buy GBP 100 USD)")); - assertEquals(201,evalL(ctx,"(torus/buy-quote GBP 100 USD)")); - - // ============================================================ - // FOURTH TEST: Check marginal sell trades for $1 / £1 - assertEquals(99,evalL(ctx,"(torus/sell GBP 100 GBP)")); - assertEquals(99,evalL(ctx,"(torus/sell-quote GBP 100 GBP)")); - assertEquals(49,evalL(ctx,"(torus/sell USD 100 GBP)")); - assertEquals(49,evalL(ctx,"(torus/sell-quote USD 100 GBP)")); - assertEquals(199,evalL(ctx,"(torus/sell GBP 100 USD)")); - assertEquals(199,evalL(ctx,"(torus/sell-quote GBP 100 USD)")); - - // Trades too big - assertError(step(ctx,"(torus/buy USD "+Long.MAX_VALUE+")")); - assertError(step(ctx,"(torus/buy USD 10000000)")); - - // Too expensive for pool - assertError(step(ctx,"(torus/buy USD 9999999)")); - } - - @Test public void testInitialTokenMarket() { - Context ctx=INITIAL.fork(); - - // Check we can access the USD market - ctx= step(ctx,"(def USDM (torus/get-market USD))"); - assertEquals(USD_MARKET,ctx.getResult()); - - // should be no price for initial market with zero liquidity - assertNull(eval(ctx,"(call USDM (price))")); - - // Offer tokens to market ($200k) - ctx= step(ctx,"(asset/offer USDM [USD 20000000])"); - assertEquals(20000000L,evalL(ctx,"(asset/get-offer USD *address* USDM)")); - - // ============================================================ - // FIRST TEST: Initial deposit of $100k USD liquidity - // Deposit some liquidity $100,000 for 1000 Gold = $100 price = 100000 CVX / US Cent - ctx= step(ctx,"(call USDM 1000000000000 (add-liquidity 10000000))"); - final long INITIAL_SHARES=RT.jvm(ctx.getResult()); - - assertEquals(10000000L,evalL(ctx,"(asset/balance USD USDM)")); - assertEquals(1000000000000L,evalL(ctx,"(balance USDM)")); - assertEquals(INITIAL_SHARES,evalL(ctx,"(asset/balance USDM *address*)")); // Initial pool shares, accessible as a fungible asset balance - - // Should have consumed half the full offer of tokens - assertEquals(10000000L,evalL(ctx,"(asset/get-offer USD *address* USDM)")); - - // price should be 100000 CVX / US Cent - assertEquals(100000.0,evalD(ctx,"(call USDM (price))")); - - // ============================================================ - // SECOND TEST: Initial deposit of $100k USD liquidity - // Deposit more liquidity $100,000 for 1000 Gold - previous token offer should cover this - ctx= step(ctx,"(call USDM 1000000000000 (add-liquidity 10000000))"); - final long NEW_SHARES=RT.jvm(ctx.getResult()); - assertEquals(20000000L,evalL(ctx,"(asset/balance USD USDM)")); - - // Check new pool shares, accessible as a fungible asset balance - assertEquals(INITIAL_SHARES+NEW_SHARES,evalL(ctx,"(asset/balance USDM *address*)")); - - // Price should be unchanged - assertEquals(100000.0,evalD(ctx,"(call USDM (price))")); - - // should have consumed remaining token offer - assertEquals(0L,evalL(ctx,"(asset/get-offer USD *address* USDM)")); - - // ============================================================ - // THIRD TEST - withdraw half of liquidity - long balanceBeforeWithdrawal=ctx.getBalance(); - ctx= step(ctx,"(call USDM (withdraw-liquidity "+NEW_SHARES+"))"); - assertEquals(RT.cvm(NEW_SHARES),ctx.getResult()); - - assertEquals(INITIAL_SHARES,evalL(ctx,"(asset/balance USDM *address*)")); - assertEquals(10000000L,evalL(ctx,"(asset/balance USD USDM)")); - assertEquals(990000000L,evalL(ctx,"(asset/balance USD *address*)")); - assertTrue(ctx.getBalance()>balanceBeforeWithdrawal); - assertEquals(1000000000000L,evalL(ctx,"(balance USDM)")); // Convex balance back to start - - // Generic fungible test on shares - FungibleTest.doFungibleTests(ctx,USD_MARKET,ctx.getAddress()); - - // ============================================================ - // FORTH TEST - buy half of all tokens ($50k) - ctx= step(ctx,"(call USDM *balance* (buy-tokens 5000000))"); - long paidConvex=RT.jvm(ctx.getResult()); - assertTrue(paidConvex>1000000000000L); // should cost more than pool Convex balance after fee - assertTrue(paidConvex<1100000000000L,"Paid:" +paidConvex); // but less than 10% fee - assertEquals(5000000L,evalL(ctx,"(asset/balance USD USDM)")); - assertEquals(995000000L,evalL(ctx,"(asset/balance USD *address*)")); - assertEquals(INITIAL_SHARES,evalL(ctx,"(asset/balance USDM *address*)")); - - // ============================================================ - // FIFTH TEST - sell back tokens ($50k) - ctx= step(ctx,"(asset/offer USDM [USD 5000000])"); - ctx= step(ctx,"(call USDM (sell-tokens 5000000))"); - long gainedConvex=RT.jvm(ctx.getResult()); - assertTrue(gainedConvex>900000000000L); // should gain most of money back - assertTrue(gainedConvex0); - ctx=step(ctx,"(torus/withdraw-liquidity USD "+shares+")"); - assertNotError(ctx); - assertEquals(0L,evalL(ctx,"(asset/balance USDM *address*)")); // should have no shares left - assertEquals(0L,evalL(ctx,"(asset/balance USD USDM)")); // should be no USD left in liquidity pool - assertEquals(0L,evalL(ctx,"(balance USDM)")); // should be no CVX left in liquidity pool - } - - @Test public void testSetup() { - assertNotNull(TORUS); - assertNotNull(USD); - assertNotNull(GBP); - assertNotNull(USD_MARKET); - } - -} diff --git a/convex-core/src/test/java/convex/comms/GenTestFormat.java b/convex-core/src/test/java/convex/comms/GenTestFormat.java deleted file mode 100644 index 4c8aab370..000000000 --- a/convex-core/src/test/java/convex/comms/GenTestFormat.java +++ /dev/null @@ -1,59 +0,0 @@ -package convex.comms; - -import static org.junit.Assert.assertEquals; - -import org.junit.runner.RunWith; - -import com.pholser.junit.quickcheck.From; -import com.pholser.junit.quickcheck.Property; -import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; - -import convex.core.data.ACell; -import convex.core.data.AString; -import convex.core.data.Blob; -import convex.core.data.Format; -import convex.core.data.FuzzTestFormat; -import convex.core.data.Ref; -import convex.core.data.Strings; -import convex.core.exceptions.BadFormatException; -import convex.core.lang.RT; -import convex.test.generators.PrimitiveGen; -import convex.test.generators.ValueGen; - -@RunWith(JUnitQuickcheck.class) -public class GenTestFormat { - @Property - public void messageRoundTrip(String str) throws BadFormatException { - AString s=Strings.create(str); - Blob b = Format.encodedBlob(s); - AString s2 = Format.read(b); - assertEquals(s, s2); - assertEquals(b, Format.encodedBlob(s2)); - - FuzzTestFormat.doMutationTest(b); - } - - @Property - public void primitiveRoundTrip(@From(PrimitiveGen.class) ACell prim) throws BadFormatException { - Blob b = Format.encodedBlob(prim); - ACell o = Format.read(b); - assertEquals(prim, o); - assertEquals(b, Format.encodedBlob(o)); - - FuzzTestFormat.doMutationTest(b); - } - - @Property - public void dataRoundTrip(@From(ValueGen.class) ACell value) throws BadFormatException { - Ref pref = ACell.createPersisted(value); // ensure persisted - Blob b = Format.encodedBlob(value); - ACell o = Format.read(b); - - assertEquals(RT.getType(value), RT.getType(o)); - assertEquals(value, o); - assertEquals(b, Format.encodedBlob(o)); - assertEquals(pref.getValue(), o); - - FuzzTestFormat.doMutationTest(b); - } -} diff --git a/convex-core/src/test/java/convex/comms/VLCEncodingTest.java b/convex-core/src/test/java/convex/comms/VLCEncodingTest.java deleted file mode 100644 index b833b3fa3..000000000 --- a/convex-core/src/test/java/convex/comms/VLCEncodingTest.java +++ /dev/null @@ -1,142 +0,0 @@ -package convex.comms; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.ByteBuffer; - -import org.junit.jupiter.api.Test; - -import convex.core.data.Blob; -import convex.core.data.Format; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.lang.RT; - -public class VLCEncodingTest { - - @Test - public void testMessageLength() throws BadFormatException { - ByteBuffer bb = Blob.fromHex("8048").getByteBuffer(); - assertEquals(0, bb.position()); - int len = Format.peekMessageLength(bb); - assertEquals(72, len); - assertEquals(2, Format.getVLCLength(len)); - } - - /** - * Test the assumption that MAX_MESSAGE_LENGTH is the largest length that can be - * VLC encoded in 2 bytes - * @throws BadFormatException - */ - @Test - public void testVLCLength() throws BadFormatException { - assertEquals(2, Format.getVLCLength(Format.LIMIT_ENCODING_LENGTH)); - assertEquals(3, Format.getVLCLength(Format.LIMIT_ENCODING_LENGTH + 1)); - - ByteBuffer bb = Blob.fromHex("BF7F").getByteBuffer(); - assertEquals(0, bb.position()); - int len = Format.peekMessageLength(bb); - assertEquals(Format.LIMIT_ENCODING_LENGTH, len); - assertEquals(2, Format.getVLCLength(len)); - } - - @Test - public void testLongVLC() { - // note 09 as tag for long - assertEquals("0900", Format.encodedString(0L)); - assertEquals("0901", Format.encodedString(1L)); - assertEquals("097f", Format.encodedString(-1L)); - - assertEquals("093f", Format.encodedString(63L)); // 6 lowest bits set - assertEquals("098040", Format.encodedString(64L)); // first overflow to 2 bytes VLC - assertEquals("098100", Format.encodedString(128L)); // first carry of positive bit - - assertEquals("0941", Format.encodedString(-63L)); - assertEquals("0940", Format.encodedString(-64L)); // sign bit only in 1 byte - assertEquals("09ff3f", Format.encodedString(-65L)); // sign overflow to 2 bytes VLC - assertEquals("09ff00", Format.encodedString(-128L)); - assertEquals("09fe7f", Format.encodedString(-129L)); // first negative carry - - assertEquals("0980ffffffffffffffff7f", Format.encodedString(Long.MAX_VALUE)); - assertEquals("09ff808080808080808000", Format.encodedString(Long.MIN_VALUE)); - } - - -// TODO: Currently not allowing BigInteger as valid data object. May reconsider. -// @Test public void testBigIntegerVLC() { -// // note 0a as tag for short -// assertEquals("0a00",Format.encodedString(BigInteger.valueOf(0))); -// assertEquals("0a01",Format.encodedString(BigInteger.valueOf(1))); -// assertEquals("0a7f",Format.encodedString(BigInteger.valueOf(-1))); -// -// assertEquals("0a3f",Format.encodedString(BigInteger.valueOf(63))); // 6 lowest bits set -// assertEquals("0a8040",Format.encodedString(BigInteger.valueOf(64))); // first overflow to 2 bytes VLC -// assertEquals("0a8100",Format.encodedString(BigInteger.valueOf(128))); // first carry of positive bit -// -// assertEquals("0a41",Format.encodedString(BigInteger.valueOf(-63))); -// assertEquals("0a40",Format.encodedString(BigInteger.valueOf(-64))); // sign bit only in 1 byte -// assertEquals("0aff3f",Format.encodedString(BigInteger.valueOf(-65))); // sign overflow to 2 bytes VLC -// assertEquals("0aff00",Format.encodedString(BigInteger.valueOf(-128))); -// assertEquals("0afe7f",Format.encodedString(BigInteger.valueOf(-129))); // first negative carry -// -// assertEquals("0a80ffffffffffffffff7f",Format.encodedString(BigInteger.valueOf(Long.MAX_VALUE))); -// assertEquals("0aff808080808080808000",Format.encodedString(BigInteger.valueOf(Long.MIN_VALUE))); -// -// BigInteger b1=BigInteger.valueOf(Long.MAX_VALUE).multiply(BigInteger.valueOf(128)); -// assertEquals("0a80ffffffffffffffffff00",Format.encodedString(b1)); -// -// BigInteger b2=BigInteger.valueOf(Long.MIN_VALUE).multiply(BigInteger.valueOf(128)); -// assertEquals("0aff80808080808080808000",Format.encodedString(b2)); -// } - - @Test - public void testLongVLCBadFormat() { - // too long encodings - assertThrows(BadFormatException.class, () -> Format.read(Blob.fromHex("098000"))); - assertThrows(BadFormatException.class, () -> Format.read(Blob.fromHex("09FF7F"))); - } - - @Test - public void testVLCSignExtend() { - assertEquals(0L, Format.vlcSignExtend((byte) 0x00)); - assertEquals(-64L, Format.vlcSignExtend((byte) 0x40)); - assertEquals(-1L, Format.vlcSignExtend((byte) 0xFF)); - assertEquals(63, Format.vlcSignExtend((byte) 0x3F)); - assertEquals(0L, Format.vlcSignExtend((byte) 0x80)); - } - -// @Test public void testBigIntegerVLCRegression() throws BadFormatException { -// BigInteger b1=new BigInteger("-10826789006513807832719466915686947597958886414817953"); -// String b1s=b1.toString(16); -// String encodedString=Format.encodedString(b1); -// BigInteger b2=Format.read(Blob.fromHex(encodedString)); -// String b2s=b2.toString(16); -// assertEquals(b1s,b2s); -// assertEquals(b1,b2); -// } - -// @Test public void testLongVLCRegression() throws BadFormatException { -// long longVal=-221195466131295L; -// BigInteger b1=BigInteger.valueOf(longVal); -// String encodedString=Format.encodedString(b1); -// String longEncodedString=Format.encodedString(longVal); -// assertEquals(longEncodedString.substring(2),encodedString.substring(2)); // should be equal after tag -// BigInteger b2=Format.read(Blob.fromHex(encodedString)); -// assertEquals(b1,b2); -// } - - @Test - public void testLongVLCRegression2() throws BadFormatException { - CVMLong b = CVMLong.create(1496216L); - Blob blob = Format.encodedBlob(b); - assertEquals(b, Format.read(blob)); - } - - @Test - public void testLongVLCRegression() throws BadFormatException { - CVMLong b = RT.cvm(1234578); - Blob blob = Format.encodedBlob(b); - assertEquals(b, Format.read(blob)); - } -} diff --git a/convex-core/src/test/java/convex/comms/VLCParamTest.java b/convex-core/src/test/java/convex/comms/VLCParamTest.java deleted file mode 100644 index 8e22a37d1..000000000 --- a/convex-core/src/test/java/convex/comms/VLCParamTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package convex.comms; - -import static org.junit.Assert.assertEquals; - -import java.util.Arrays; -import java.util.Collection; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import convex.core.data.ACell; -import convex.core.data.Blob; -import convex.core.data.Format; -import convex.core.data.FuzzTestFormat; -import convex.core.data.Tag; -import convex.core.data.prim.CVMByte; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.lang.RT; - -@RunWith(Parameterized.class) -public class VLCParamTest { - private ACell value; - - public VLCParamTest(Object value) { - // create using CVM-coerced values - this.value = RT.cvm(value); - } - - @Parameterized.Parameters(name = "{0}") - public static Collection dataExamples() { - return Arrays.asList(new Object[][] { { 0L }, { 63L }, { 64L }, { -63L }, { -64L }, { -65L }, { 1234L }, - { 1234578 }, { -1234578 }, { CVMByte.create(1) }, { CVMByte.create(255) }, { Long.MAX_VALUE }, { Long.MIN_VALUE }, - { Integer.MAX_VALUE }, { Integer.MIN_VALUE }, -// { BigInteger.valueOf(Long.MAX_VALUE).multiply(BigInteger.TEN) }, -// { BigInteger.valueOf(Long.MIN_VALUE).multiply(BigInteger.TEN) }, - - }); - } - - @Test - public void testRoundTrip() throws BadFormatException { - Blob b = Format.encodedBlob(value); - ACell v2 = Format.read(b); - assertEquals(value, v2); - - if (value instanceof CVMLong) { - CVMLong cl=(CVMLong) value; - assertEquals(Tag.LONG, b.byteAt(0)); // check correct tag - assertEquals(1 + Format.getVLCLength(cl.longValue()), b.count()); // check length after tag - } - - FuzzTestFormat.doMutationTest(b); - } -} diff --git a/convex-core/src/test/java/convex/core/BeliefMergeTest.java b/convex-core/src/test/java/convex/core/BeliefMergeTest.java deleted file mode 100644 index d0796f978..000000000 --- a/convex-core/src/test/java/convex/core/BeliefMergeTest.java +++ /dev/null @@ -1,497 +0,0 @@ -package convex.core; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.time.Instant; -import java.util.HashSet; -import java.util.Random; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.parallel.Execution; -import org.junit.jupiter.api.parallel.ExecutionMode; - -import convex.core.crypto.Ed25519KeyPair; -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.core.data.BlobMap; -import convex.core.data.BlobMaps; -import convex.core.data.PeerStatus; -import convex.core.data.RecordTest; -import convex.core.data.SignedData; -import convex.core.data.Vectors; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.BadSignatureException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.Juice; -import convex.core.lang.RT; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Transfer; -import convex.core.util.Text; -import convex.core.util.Utils; - -@Execution(value = ExecutionMode.CONCURRENT) -public class BeliefMergeTest { - - public static final int NUM_PEERS = 9; - public static final int NUM_INITIAL_TRANS = 10; - public static final int ROUNDS = 20; - - public static final Ed25519KeyPair[] KEY_PAIRS = new Ed25519KeyPair[NUM_PEERS]; - public static final Address[] ADDRESSES = new Address[NUM_PEERS]; - public static final AccountKey[] KEYS = new AccountKey[NUM_PEERS]; - public static final State INITIAL_STATE; - private static final long TEST_TIMESTAMP = Instant.parse("1977-11-13T00:30:00Z").toEpochMilli(); - private static final long TOTAL_VALUE; - - static { - // long seed=new Random().nextLong(); - long seed = 2654733563337952L; - // System.out.println("Generating with seed: "+seed); - AVector accounts = Vectors.empty(); - BlobMap peers = BlobMaps.empty(); - for (int i = 0; i < NUM_PEERS; i++) { - Ed25519KeyPair kp = Ed25519KeyPair.createSeeded(seed + i * 17777); - AccountKey key = kp.getAccountKey(); - // TODO numeric addresses - Address address=Address.create(i); - KEY_PAIRS[i] = kp; - KEYS[i] = key; - ADDRESSES[i] = address; - AccountStatus accStatus = AccountStatus.create((i + 1) * 1000000,key); - PeerStatus peerStatus = PeerStatus.create(address,(i + 1) * 100000); - accounts = accounts.conj(accStatus); - peers = peers.assoc(key, peerStatus); - } - - AVector globals = Constants.INITIAL_GLOBALS; - globals = globals.assoc(State.GLOBAL_JUICE_PRICE, RT.cvm(1L)); // cheap juice for simplicity. USe CVM long - INITIAL_STATE = State.create(accounts, peers, globals, BlobMaps.empty()); - TOTAL_VALUE = INITIAL_STATE.computeTotalFunds(); - } - - private Peer initialPeerState(int i) { - return Peer.create(KEY_PAIRS[i], INITIAL_STATE); - } - - private Peer[] initialBeliefs() { - int n = NUM_PEERS; - Peer[] result = new Peer[n]; - for (int i = 0; i < n; i++) { - result[i] = initialPeerState(i); - } - return result; - } - - public Peer[] shareBeliefs(Peer[] initial) throws BadSignatureException, InvalidDataException { - int n = initial.length; - Peer[] result = new Peer[n]; - - // extract beliefs to share - Belief[] sharedBeliefs = new Belief[n]; - for (int j = 0; j < n; j++) - sharedBeliefs[j] = initial[j].getBelief(); - - for (int i = 0; i < n; i++) { - Peer ps = initial[i]; - result[i] = ps.mergeBeliefs(sharedBeliefs); // belief merge step - } - return result; - } - - public Peer[] shareGossip(Peer[] initial, int numGossips, int round) - throws BadSignatureException, InvalidDataException { - Random r = new Random(107701 + round * 1337); - int n = initial.length; - Peer[] result = new Peer[n]; - - Belief[] sharedBeliefs = new Belief[n]; - for (int j = 0; j < n; j++) - sharedBeliefs[j] = initial[j].getBelief(); - - for (int i = 0; i < n; i++) { - Peer ps = initial[i]; - Belief[] sources = new Belief[numGossips]; - for (int j = 0; j < numGossips; j++) { - sources[j] = sharedBeliefs[r.nextInt(n)]; - } - result[i] = ps.mergeBeliefs(sources); // belief merge step - } - return result; - } - - @SuppressWarnings("unchecked") - private Peer[] proposeTransactions(Peer[] initial, int peerIndex, ATransaction... transactions) - throws BadSignatureException { - Peer[] result = initial.clone(); - Peer ps = initial[peerIndex]; // current per under consideration - - // create a block of transactions - int tcount = transactions.length; - SignedData[] signedTransactions = (SignedData[]) new SignedData[tcount]; - for (int ix = 0; ix < tcount; ix++) { - signedTransactions[ix] = initial[peerIndex].sign(transactions[ix]); - } - long newTimeStamp = ps.getTimeStamp() + peerIndex + 100; - Block block = Block.of(newTimeStamp, KEYS[peerIndex], signedTransactions); - - ps = ps.proposeBlock(block); - result[peerIndex] = ps; - return result; - } - - @Test - public void testBasicMerge() throws BadSignatureException, InvalidDataException { - Peer b0 = initialPeerState(0); - Peer b1 = initialPeerState(1); - assertNotEquals(b0, b1); // should not be equal - no knowledge of other peer chains yet - - Peer bm0 = b0.mergeBeliefs(b1.getBelief()); - assertTrue(b0.getPeerOrder() == bm0.getPeerOrder()); - - // propose a new block by peer 1, after 200ms - long newTimestamp1 = TEST_TIMESTAMP + 200; - b1 = b1.updateTimestamp(b1.getTimeStamp() + 200); - assertEquals(0, b1.getPeerOrder().getBlocks().size()); - Peer b1a = b1.proposeBlock(Block.of(newTimestamp1,KEYS[1])); // empty block, just with timestamp - assertEquals(1, b1a.getPeerOrder().getBlocks().size()); - - // merge updated belief, new proposed block should be included - Peer bm2 = b0.mergeBeliefs(b1a.getBelief()); - assertEquals(b1a.getPeerOrder().getBlocks(), bm2.getPeerOrder().getBlocks()); - } - - /** - * This test creates a set of peers, and a single transaction sending tokens - * from the first peers to the last peer Each round of peers updates is - * gossipped simultaneously and the results checked at each stage To validate - * correct propagation of the new block across the network - * @throws BadSignatureException - * @throws InvalidDataException - * @throws BadFormatException - */ - @Test - public void testSingleBlockConsensus() throws BadSignatureException, InvalidDataException, BadFormatException { - boolean ANALYSIS = false; - Peer[] bs0 = initialBeliefs(); - assertNotEquals(bs0[0].getBelief(), bs0[1].getBelief()); // only have own beliefs - validateBeliefs(bs0); - - Peer[] bs1 = shareBeliefs(bs0); // sync all beliefs - assertTrue(allBeliefsEqual(bs1)); // should share beliefs - - Peer[] bs2 = shareBeliefs(bs1); // sync again, should be idempotent - assertEquals(bs1[0].getBelief(), bs2[0].getBelief()); // belief should not change for peer 0 - assertTrue(allBeliefsEqual(bs2)); // beliefs across peers should be equal - - int PROPOSER = 0; - int RECEIVER = NUM_PEERS - 1; - Address PADDRESS = ADDRESSES[PROPOSER]; - Address RADDRESS = ADDRESSES[RECEIVER]; - AccountKey PKEY = KEYS[PROPOSER]; - AccountKey RKEY = KEYS[RECEIVER]; - long INITIAL_BALANCE_PROPOSER = INITIAL_STATE.getBalance(PADDRESS); - long INITIAL_BALANCE_RECEIVER = INITIAL_STATE.getBalance(RADDRESS); - long TRANSFER_AMOUNT = 100; - long TJUICE=Juice.TRANSFER; - - ATransaction trans = Transfer.create(PADDRESS, 1, RADDRESS, TRANSFER_AMOUNT); // note 1 = first sequence number required - Peer[] bs3 = proposeTransactions(bs2, PROPOSER, trans); - if (ANALYSIS) printAnalysis(bs3, "Make proposal"); - assertEquals(1, bs3[PROPOSER].getOrder(PKEY).getBlockCount()); - assertEquals(0, bs3[RECEIVER].getOrder(PKEY).getBlockCount()); - - // New block should win vote for all peers, but not achieve enough support - // for proposed consensus yet - Peer[] bs4 = shareBeliefs(bs3); - if (ANALYSIS) printAnalysis(bs4, "Share 1st round, each peer should adopt proposed block"); - assertEquals(1, bs4[PROPOSER].getOrder(PKEY).getBlockCount()); - assertEquals(1, bs4[RECEIVER].getOrder(RKEY).getBlockCount()); - assertEquals(0, bs4[PROPOSER].getOrder(RKEY).getBlockCount()); // proposer can't see block in receiver's - // chain yet - - // all peers should propose new consensus, but not confirmed yet - assertEquals(0, bs4[PROPOSER].getOrder(PKEY).getProposalPoint()); - assertEquals(0, bs4[RECEIVER].getOrder(RKEY).getProposalPoint()); - Peer[] bs5 = shareBeliefs(bs4); - if (ANALYSIS) printAnalysis(bs5, - "Share 2nd round: each peer should propose consensus after seeing majority for new block"); - assertEquals(1, bs5[PROPOSER].getOrder(PKEY).getProposalPoint()); - assertEquals(1, bs5[RECEIVER].getOrder(RKEY).getProposalPoint()); - - // all peers should now agree on consensus, but don't know each other's - // consensus yet - assertEquals(0, bs5[PROPOSER].getOrder(PKEY).getConsensusPoint()); - assertEquals(0, bs5[RECEIVER].getOrder(RKEY).getConsensusPoint()); - assertEquals(0, bs5[PROPOSER].getOrder(RKEY).getConsensusPoint()); - Peer[] bs6 = shareBeliefs(bs5); - if (ANALYSIS) printAnalysis(bs6, - "Share 3nd round: each peer should confirm consensus after seeing proposals from others"); - assertEquals(1, bs6[PROPOSER].getOrder(PKEY).getConsensusPoint()); - assertEquals(1, bs6[RECEIVER].getOrder(RKEY).getConsensusPoint()); - assertEquals(0, bs6[PROPOSER].getOrder(RKEY).getConsensusPoint()); - - Peer[] bs7 = shareBeliefs(bs6); - if (ANALYSIS) printAnalysis(bs7, "Share 4th round: should reach full consensus, confirmations shared"); - assertEquals(1, bs7[PROPOSER].getOrder(RKEY).getConsensusPoint()); // proposer now sees receivers consensus - - // final state checks - assertTrue(allBeliefsEqual(bs7)); // beliefs across peers should be equal - State finalState = bs7[0].getConsensusState(); - assertEquals(INITIAL_BALANCE_PROPOSER-TRANSFER_AMOUNT-TJUICE, finalState.getBalance(PADDRESS)); - assertEquals(INITIAL_BALANCE_RECEIVER+TRANSFER_AMOUNT, finalState.getBalance(RADDRESS)); - - // matter cannot be created or destroyed.... - assertEquals(TOTAL_VALUE, finalState.computeTotalFunds()); - } - - /** - * This test creates a set of peers, and one transaction for each peer Each - * round of peers updates is gossipped simultaneously and the results checked at - * each stage To validate correct propagation of the new block across the - * network - * @throws BadSignatureException - * @throws InvalidDataException - * @throws BadFormatException - */ - @Test - public void testMultiBlockConsensus() throws BadSignatureException, InvalidDataException, BadFormatException { - boolean ANALYSIS = false; - Peer[] bs0 = initialBeliefs(); - assertFalse(allBeliefsEqual(bs0)); // only have own beliefs - validateBeliefs(bs0); - - Peer[] bs1 = shareBeliefs(bs0); // sync all beliefs - assertTrue(allBeliefsEqual(bs1)); // should see other beliefs - - Peer[] bs2 = shareBeliefs(bs1); // sync again, should be idempotent - assertEquals(bs1[0].getBelief(), bs2[0].getBelief()); // belief should not change - assertTrue(allBeliefsEqual(bs2)); // beliefs across peers should be equal - - int PROPOSER = 0; - int RECEIVER = NUM_PEERS - 1; - Address PADDRESS = ADDRESSES[PROPOSER]; - Address RADDRESS = ADDRESSES[RECEIVER]; - AccountKey PKEY = KEYS[PROPOSER]; - AccountKey RKEY = KEYS[RECEIVER]; - Long INITIAL_BALANCE_PROPOSER = INITIAL_STATE.getBalance(PADDRESS); - Long INITIAL_BALANCE_RECEIVER = INITIAL_STATE.getBalance(RADDRESS); - long TJUICE=Juice.TRANSFER; - - Peer[] bs3 = bs2; - for (int i = 0; i < NUM_PEERS; i++) { - long TRANSFER_AMOUNT = 100L; - ATransaction trans = Transfer.create(ADDRESSES[i],1, ADDRESSES[NUM_PEERS - 1 - i], TRANSFER_AMOUNT); // note 1 = first - // sequence number - // required - bs3 = proposeTransactions(bs3, i, trans); - } - if (ANALYSIS) printAnalysis(bs3, "Make proposals"); - assertEquals(1, bs3[0].getOrder(PKEY).getBlockCount()); - assertEquals(1, bs3[RECEIVER].getOrder(RKEY).getBlockCount()); - assertEquals(0, bs3[RECEIVER].getOrder(PKEY).getBlockCount()); - - // New block should win vote for all peers, but not achieve enough support - // for proposed consensus yet - Peer[] bs4 = shareBeliefs(bs3); - if (ANALYSIS) - printAnalysis(bs4, "Share 1st round, each peer should see others chains, vote for same plus new blocks"); - assertEquals(NUM_PEERS, bs4[PROPOSER].getOrder(PKEY).getBlockCount()); - assertEquals(NUM_PEERS, bs4[RECEIVER].getOrder(RKEY).getBlockCount()); - assertEquals(1, bs4[PROPOSER].getOrder(RKEY).getBlockCount()); // proposer can only see 1st block from - // receiver - assertEquals(0, bs4[PROPOSER].getOrder(PKEY).getProposalPoint()); - assertEquals(0, bs4[RECEIVER].getOrder(RKEY).getProposalPoint()); - - // Next round - Peer[] bs5 = shareBeliefs(bs4); - if (ANALYSIS) printAnalysis(bs5, "Share 2nd round: "); - - // all peers should now agree on consensus - Peer[] bs6 = shareBeliefs(bs5); - if (ANALYSIS) printAnalysis(bs6, "Share 3nd round: "); - - Peer[] bs7 = shareBeliefs(bs6); - if (ANALYSIS) printAnalysis(bs7, "Share 4th round: should reach full consensus?"); - assertEquals(NUM_PEERS, bs7[PROPOSER].getOrder(RKEY).getConsensusPoint()); // proposer now sees receivers - // consensus - - // final state checks - assertTrue(allBeliefsEqual(bs7)); - State finalState = bs7[0].getConsensusState(); - // should have 1 transaction each - assertEquals(1L, finalState.getAccount(PADDRESS).getSequence()); - assertEquals(1L, finalState.getAccount(RADDRESS).getSequence()); - assertEquals(INITIAL_BALANCE_PROPOSER-TJUICE, finalState.getBalance(PADDRESS)); - assertEquals(INITIAL_BALANCE_RECEIVER-TJUICE, finalState.getBalance(RADDRESS)); - - // law of conservation of gil - assertEquals(TOTAL_VALUE, finalState.computeTotalFunds()); - } - - private boolean allBeliefsEqual(Peer[] pss) { - int n = pss.length; - for (int i = 0; i < n - 1; i++) { - if (!Utils.equals(pss[i].getBelief(), pss[i + 1].getBelief())) return false; - } - return true; - } - - /** - * This test creates a set of peers, and one transaction for each peer Each - * round of peers updates is gossipped partially To validate correct propagation - * of the new block across the network - * @throws BadSignatureException - * @throws InvalidDataException - * @throws BadFormatException - */ - @Test - public void testGossipConsensus() throws BadSignatureException, InvalidDataException, BadFormatException { - boolean ANALYSIS = false; - int GOSSIP_NUM = 4; - - Peer[] bs0 = initialBeliefs(); - if (ANALYSIS) printAnalysis(bs0, "Initial beliefs"); - assertFalse(allBeliefsEqual(bs0)); // only have own beliefs - validateBeliefs(bs0); - - Peer[] bs1 = shareBeliefs(bs0); // sync all beliefs - assertTrue(allBeliefsEqual(bs1)); // should see other beliefs - - Peer[] bs2 = shareBeliefs(bs1); // sync again, should be idempotent - assertEquals(bs1[0].getBelief(), bs2[0].getBelief()); // belief should not change - assertTrue(allBeliefsEqual(bs2)); // beliefs across peers should be equal - if (ANALYSIS) printAnalysis(bs2, "Shared beliefs"); - - int PROPOSER = 0; - int RECEIVER = NUM_PEERS - 1; - Address PADDRESS = ADDRESSES[PROPOSER]; - Address RADDRESS = ADDRESSES[RECEIVER]; - AccountKey PKEY = KEYS[PROPOSER]; - AccountKey RKEY = KEYS[RECEIVER]; - long INITIAL_BALANCE_PROPOSER = INITIAL_STATE.getBalance(PADDRESS); - long INITIAL_BALANCE_RECEIVER = INITIAL_STATE.getBalance(RADDRESS); - long TJUICE=Juice.TRANSFER*NUM_INITIAL_TRANS; - - - Peer[] bs3 = bs2; - for (int i = 0; i < NUM_PEERS; i++) { - // propose initial transactions - for (int j = 1; j <= NUM_INITIAL_TRANS; j++) { - long TRANSFER_AMOUNT = 100L; - ATransaction trans = Transfer.create(ADDRESSES[i],j, ADDRESSES[NUM_PEERS - 1 - i], TRANSFER_AMOUNT); // note 1 = - // first - // sequence - // number - // required - bs3 = proposeTransactions(bs3, i, trans); - } - } - if (ANALYSIS) printAnalysis(bs3, "Make proposals"); - assertEquals(NUM_INITIAL_TRANS, bs3[0].getOrder(PKEY).getBlockCount()); - assertEquals(NUM_INITIAL_TRANS, bs3[RECEIVER].getOrder(RKEY).getBlockCount()); - assertEquals(0, bs3[RECEIVER].getOrder(PKEY).getBlockCount()); - - Peer[] bs4 = bs3; - for (int i = 1; i < ROUNDS; i++) { - bs4 = shareGossip(bs4, GOSSIP_NUM, i); - if (ANALYSIS) printAnalysis(bs4, "Share round: " + i); - } - bs4 = shareGossip(bs4, GOSSIP_NUM, ROUNDS); - if (ANALYSIS) printAnalysis(bs4, "Share round: " + ROUNDS); - - // final state checks - assertEquals(NUM_PEERS * NUM_INITIAL_TRANS, bs4[PROPOSER].getOrder(RKEY).getConsensusPoint()); // proposer - // now sees - // receivers - // consensus - assertTrue(allBeliefsEqual(bs4)); - - Order finalChain = bs4[0].getOrder(PKEY); - AVector finalBlocks = finalChain.getBlocks(); - assertEquals(NUM_PEERS * NUM_INITIAL_TRANS, new HashSet<>(finalBlocks).size()); - - State finalState = bs4[0].getConsensusState(); - AVector accounts = finalState.getAccounts(); - if (ANALYSIS) printAccounts(accounts); - - // should have correct number of transactions each - for (int i = 0; i < NUM_PEERS; i++) { - assertEquals(NUM_INITIAL_TRANS, accounts.get(ADDRESSES[i].longValue()).getSequence()); - } - // should have equal balance - assertEquals(INITIAL_BALANCE_PROPOSER-TJUICE, finalState.getBalance(PADDRESS)); - assertEquals(INITIAL_BALANCE_RECEIVER-TJUICE, finalState.getBalance(RADDRESS)); - - // 100% of value still exists - assertEquals(TOTAL_VALUE, finalState.computeTotalFunds()); - - RecordTest.doRecordTests(bs4[0].getBelief()); - RecordTest.doRecordTests(finalState); - } - - private void printAccounts(AVector accounts) { - System.out.println("===== Accounts ====="); - for (int i = 0; i < NUM_PEERS; i++) { - Address address = ADDRESSES[i]; - AccountStatus as = accounts.get(address); - System.out.println(peerString(i) + " " + as); - } - } - - private void validateBeliefs(Peer[] pss) throws InvalidDataException, BadFormatException { - for (Peer ps : pss) { - ps.getBelief().validate(); - } - } - - private static void printAnalysis(Peer[] beliefs, String msg) throws BadSignatureException { - System.out.println("===== " + msg + " ====="); - for (int i = 0; i < NUM_PEERS; i++) { - if ((i >= 5) && (i < NUM_PEERS - 5)) { - System.out.println(".... (" + (NUM_PEERS - 10) + " peers skipped)"); - i = NUM_PEERS - 6; - continue; - } - Peer ps = beliefs[i]; - Belief b = beliefs[i].getBelief(); - - Order c = b.getOrder(KEYS[i]); - int agreedPeers = 0; - String mat = ""; - for (int j = 0; j < NUM_PEERS; j++) { - if ((j >= 5) && (j < NUM_PEERS - 5)) { - mat += " ...."; - j = NUM_PEERS - 6; - continue; - } - Order jc = b.getOrder(KEYS[j]); - mat += " " + ((jc == null) ? "----" : jc.getHash().toHexString(4)); - if (c.equals(jc)) agreedPeers++; - } - - long clen = c.getBlockCount(); - String blockRep = ""; - for (int ix = 0; ix < clen; ix++) { - blockRep += " " + c.getBlock(ix).getHash().toHexString(2); - } - - long pp = c.getProposalPoint(); - long cp = c.getConsensusPoint(); - System.out.println(peerString(i) + " clen:" + Text.leftPad(clen, 3) + " prop:" + Text.leftPad(pp, 3) - + " cons:" + Text.leftPad(cp, 3) + " state:" + ps.getConsensusState().getHash().toHexString(4) - + " hash: " + c.getHash().toHexString(4) + " mat:" + mat + " agreed: " + agreedPeers + "/" - + b.getOrders().count() + " blks:" + blockRep); - } - } - - private static String peerString(int i) { - return "Peer: " + Text.rightPad(i, 4) + " [" + ADDRESSES[i].toHexString(4) + "]"; - } - -} diff --git a/convex-core/src/test/java/convex/core/BeliefVotingTest.java b/convex-core/src/test/java/convex/core/BeliefVotingTest.java deleted file mode 100644 index 11cc2137c..000000000 --- a/convex-core/src/test/java/convex/core/BeliefVotingTest.java +++ /dev/null @@ -1,16 +0,0 @@ -package convex.core; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -import convex.core.data.Maps; - -public class BeliefVotingTest { - - @Test - public void testComputeVote() { - assertEquals(100.0, Belief.computeVote(Maps.hashMapOf(1, 50.0, 0, 50.0)), 0.000001); - assertEquals(0.0, Belief.computeVote(Maps.hashMapOf()), 0.000001); - } -} diff --git a/convex-core/src/test/java/convex/core/MessageSizeTest.java b/convex-core/src/test/java/convex/core/MessageSizeTest.java deleted file mode 100644 index adb50e591..000000000 --- a/convex-core/src/test/java/convex/core/MessageSizeTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package convex.core; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -import convex.core.data.Format; -import convex.core.data.StringShort; -import convex.test.Samples; - -public class MessageSizeTest { - - @Test public void testMaxEmbedded() { - assertEquals(Format.MAX_EMBEDDED_LENGTH,Samples.MAX_EMBEDDED_BLOB.getEncoding().count()); - assertTrue(Samples.MAX_EMBEDDED_BLOB.isEmbedded()); - - assertEquals(Format.MAX_EMBEDDED_LENGTH+1,Samples.NON_EMBEDDED_BLOB.getEncoding().count()); - assertFalse(Samples.NON_EMBEDDED_BLOB.isEmbedded()); - } - - @Test public void testEmbeddedStrings() { - assertTrue(Format.MAX_EMBEDDED_LENGTH>=Samples.MAX_EMBEDDED_STRING.getEncoding().count()); - assertEquals(StringShort.MAX_EMBEDDED_STRING_LENGTH,Samples.MAX_EMBEDDED_STRING.length()); - assertTrue(Samples.MAX_EMBEDDED_STRING.isEmbedded()); - - assertTrue(Format.MAX_EMBEDDED_LENGTHr)); - - RecordTest.doRecordTests(r1); - - } - - @Test - public void testResultCreation() { - Result r1=Result.create(CVMLong.create(0L),RT.cvm(1L),null); - - RecordTest.doRecordTests(r1); - } - -} diff --git a/convex-core/src/test/java/convex/core/StakingTest.java b/convex-core/src/test/java/convex/core/StakingTest.java deleted file mode 100644 index 31f336778..000000000 --- a/convex-core/src/test/java/convex/core/StakingTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package convex.core; - -import static convex.test.Assertions.assertArgumentError; -import static convex.test.Assertions.assertFundsError; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -import convex.core.data.ACell; -import convex.core.data.PeerStatus; -import convex.core.init.InitTest; -import convex.core.lang.ACVMTest; -import convex.core.lang.Context; - -public class StakingTest extends ACVMTest { - - protected StakingTest() { - super(InitTest.STATE); - } - - @Test - public void testDelegatedStaking() { - - } - - @Test - public void testStake() { - Context ctx0 =context(); - - Context ctx1 = ctx0.setDelegatedStake(InitTest.FIRST_PEER_KEY, 1000); - PeerStatus ps1 = ctx1.getState().getPeer(InitTest.FIRST_PEER_KEY); - assertEquals(1000L, ps1.getDelegatedStake()); - - // round tripping this should return to initial state precisely - // since we are not consuming any juice here, or adjusting anything other than - // stake positions - Context ctx2 = ctx1.setDelegatedStake(InitTest.FIRST_PEER_KEY, 0); - assertEquals(ctx0.getState(), ctx2.getState()); - - // test putting entire balance on stake - Context ctx3 = step(ctx0, "(stake " + InitTest.FIRST_PEER_KEY + " *balance*)"); - assertEquals(0L, ctx3.getBalance(InitTest.HERO)); - assertEquals(HERO_BALANCE, ctx3.getState().getPeer(InitTest.FIRST_PEER_KEY).getDelegatedStake(InitTest.HERO)); - - // test putting too much balance - assertFundsError(step(ctx0, "(stake " + InitTest.FIRST_PEER_KEY + " (inc *balance*))")); - } - - @Test - public void testStakeReturns() { - Context ctx0 = context(); - assertEquals(1000L, evalL(ctx0, "(stake " + InitTest.FIRST_PEER_KEY + " 1000)")); - } - - @Test - public void testBadStake() { - Context ctx0 = context(); - - // TODO: new test since HERO is now a peer manager - // not a peer, should be state error - //assertStateError(ctx0.setDelegatedStake(InitTest.HERO_KEYPAIR.getAccountKey(), 1000)); - - // bad arguments, out of range - assertArgumentError(ctx0.setDelegatedStake(InitTest.FIRST_PEER_KEY, -1)); - assertArgumentError(ctx0.setDelegatedStake(InitTest.FIRST_PEER_KEY, Long.MAX_VALUE)); - - // insufficient funds for stake - assertFundsError(ctx0.setDelegatedStake(InitTest.FIRST_PEER_KEY, Constants.MAX_SUPPLY)); - assertFundsError(ctx0.setDelegatedStake(InitTest.FIRST_PEER_KEY, ctx0.getBalance(InitTest.HERO) + 1)); - } -} diff --git a/convex-core/src/test/java/convex/core/StateTest.java b/convex-core/src/test/java/convex/core/StateTest.java deleted file mode 100644 index b9f211624..000000000 --- a/convex-core/src/test/java/convex/core/StateTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package convex.core; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.AccountStatus; -import convex.core.data.Blob; -import convex.core.data.Format; -import convex.core.data.RecordTest; -import convex.core.data.Ref; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.init.InitTest; - -/** - * Tests for the State data structure - */ -public class StateTest { - State INIT_STATE=InitTest.createState(); - - @Test - public void testEmptyState() { - State s = State.EMPTY; - AVector accts = s.getAccounts(); - assertEquals(0, accts.count()); - - RecordTest.doRecordTests(s); - } - - @Test - public void testInitialState() throws InvalidDataException { - State s = INIT_STATE; - assertSame(s, s.withAccounts(s.getAccounts())); - assertSame(s, s.withPeers(s.getPeers())); - - s.validate(); - - RecordTest.doRecordTests(s); - } - - @Test - public void testRoundTrip() throws BadFormatException { - State s = INIT_STATE; - // TODO: fix this - // s=s.store(Keywords.STATE); - // assertEquals(1,s.getStore().size()); - - assertEquals(0,s.getRef().getStatus()); - - Ref rs = ACell.createPersisted(s); - assertEquals(Ref.PERSISTED, rs.getStatus()); - - // Initial ref should now have persisted status - assertTrue(s.getRef().isPersisted()); - - Blob b = Format.encodedBlob(s); - State s2 = Format.read(b); - assertEquals(s, s2); - - AccountStatus as=s2.getAccount(InitTest.HERO); - assertNotNull(as); - - RecordTest.doRecordTests(s2); - RecordTest.doRecordTests(as); - } -} diff --git a/convex-core/src/test/java/convex/core/StateTransitionsTest.java b/convex-core/src/test/java/convex/core/StateTransitionsTest.java deleted file mode 100644 index 1f67caaed..000000000 --- a/convex-core/src/test/java/convex/core/StateTransitionsTest.java +++ /dev/null @@ -1,317 +0,0 @@ -package convex.core; - -import static convex.test.Assertions.assertCVMEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import org.junit.jupiter.api.Test; - -import convex.core.crypto.AKeyPair; -import convex.core.crypto.Ed25519KeyPair; -import convex.core.data.ABlob; -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.core.data.BlobMap; -import convex.core.data.SignedData; -import convex.core.data.Vectors; -import convex.core.exceptions.BadSignatureException; -import convex.core.init.InitTest; -import convex.core.lang.Juice; -import convex.core.lang.Reader; -import convex.core.lang.TestState; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Invoke; -import convex.core.transactions.Transfer; -import convex.core.util.Utils; - -/** - * Tests for State transition scenarios - */ -public class StateTransitionsTest { - - final AKeyPair KEYPAIR_A = Ed25519KeyPair.createSeeded(1001); - final AKeyPair KEYPAIR_B = Ed25519KeyPair.createSeeded(1002); - final AKeyPair KEYPAIR_C = Ed25519KeyPair.createSeeded(1003); - final AKeyPair KEYPAIR_NIKI = Ed25519KeyPair.createSeeded(1004); - final AKeyPair KEYPAIR_ROBB = Ed25519KeyPair.createSeeded(1005); - - final AKeyPair KEYPAIR_PEER = Ed25519KeyPair.createSeeded(1006); - final AccountKey FIRST_PEER_KEY=KEYPAIR_PEER.getAccountKey(); - - final Address ADDRESS_A = Address.create(0); // initial account - final Address ADDRESS_B = Address.create(1); // initial account - final Address ADDRESS_ROBB = Address.create(2); // initial account - final Address ADDRESS_C = Address.create(3); - - final Address ADDRESS_NIKI = Address.create(4); - - @Test - public void testAccountTransfers() throws BadSignatureException { - AVector accounts = Vectors.of( - AccountStatus.create(10000L,KEYPAIR_A.getAccountKey()), - AccountStatus.create(1000L,KEYPAIR_B.getAccountKey()), - AccountStatus.create(Constants.MAX_SUPPLY - 10000 - 1000,KEYPAIR_ROBB.getAccountKey()) - // No account for C yet - ); - State s = State.EMPTY.withAccounts(accounts); // don't need any peers for these tests - assertEquals(Constants.MAX_SUPPLY, s.computeTotalFunds()); - - assertEquals(10000, s.getBalance(ADDRESS_A)); - assertEquals(1000, s.getBalance(ADDRESS_B)); - assertNull(s.getBalance(ADDRESS_C)); - - long TCOST = Juice.TRANSFER * s.getJuicePrice().longValue(); - - { // transfer from existing to existing account A->B - Transfer t1 = Transfer.create(ADDRESS_A,1, ADDRESS_B, 50); - SignedData st = KEYPAIR_A.signData(t1); - long nowTS = Utils.getCurrentTimestamp(); - Block b = Block.of(nowTS,FIRST_PEER_KEY, st); - BlockResult br = s.applyBlock(b); - AVector results = br.getResults(); - assertEquals(1, results.count()); - assertNull(br.getErrorCode(0),br.getResult(0).toString()); // should be null for successful transfer transaction - State s2 = br.getState(); - assertEquals(9950 - TCOST, s2.getBalance(ADDRESS_A)); - assertEquals(1050, s2.getBalance(ADDRESS_B)); - assertCVMEquals(nowTS, s2.getTimeStamp()); - } - - { // transfer from existing to non-existing account A -> C - Transfer t1 = Transfer.create(ADDRESS_A,1, ADDRESS_C, 50); - SignedData st = KEYPAIR_A.signData(t1); - Block b = Block.of(System.currentTimeMillis(),FIRST_PEER_KEY, st); - State s2 = s.applyBlock(b).getState(); - - // no transfer should have happened, although cost should have been paid - assertEquals(10000 - TCOST, s2.getBalance(ADDRESS_A)); - assertNull(s2.getBalance(ADDRESS_C)); - } - - { // transfer from a non-existent address - Transfer t1 = Transfer.create(ADDRESS_C,1, ADDRESS_B, 50); - SignedData st = KEYPAIR_C.signData(t1); - Block b = Block.of(System.currentTimeMillis(),FIRST_PEER_KEY, st); - BlockResult br=s.applyBlock(b); - assertEquals(ErrorCodes.NOBODY, br.getResult(0).getErrorCode()); - - } - - { // transfer from existing to new account A -> C - // First create new account C - State s0=s.putAccount(ADDRESS_C, AccountStatus.create(0L,KEYPAIR_C.getAccountKey())); - - Transfer t1 = Transfer.create(ADDRESS_A,1, ADDRESS_C, 50); - SignedData st = KEYPAIR_A.signData(t1); - Block b = Block.of(System.currentTimeMillis(),FIRST_PEER_KEY, st); - State s2 = s0.applyBlock(b).getState(); - - // Transfer should have happened - assertEquals(9950 - TCOST, s2.getBalance(ADDRESS_A)); - assertEquals(50, s2.getBalance(ADDRESS_C)); - } - - { // two transfers in sequence, both from A -> C - // First create new account C - State s0=s.putAccount(ADDRESS_C, AccountStatus.create(0L,KEYPAIR_C.getAccountKey())); - - Transfer t1 = Transfer.create(ADDRESS_A,1, ADDRESS_C, 150); - SignedData st1 = KEYPAIR_A.signData(t1); - Transfer t2 = Transfer.create(ADDRESS_A,2, ADDRESS_C, 150); - SignedData st2 = KEYPAIR_A.signData(t2); - Block b = Block.of(System.currentTimeMillis(),FIRST_PEER_KEY, st1, st2); - - BlockResult br = s0.applyBlock(b); - State s2 = br.getState(); - assertEquals(9700 - TCOST * 2, s2.getBalance(ADDRESS_A)); - assertEquals(1000, s2.getBalance(ADDRESS_B)); - assertEquals(300, s2.getBalance(ADDRESS_C)); - } - - { // two transfers in sequence, 2 different accounts A B --> new account C - // First create new account C - State s0=s.putAccount(ADDRESS_C, AccountStatus.create(0L,KEYPAIR_C.getAccountKey())); - - Transfer t1 = Transfer.create(ADDRESS_A,1, ADDRESS_C, 50); - SignedData st1 = KEYPAIR_A.signData(t1); - Transfer t2 = Transfer.create(ADDRESS_B,1, ADDRESS_C, 50); - SignedData st2 = KEYPAIR_B.signData(t2); - Block b = Block.of(System.currentTimeMillis(),FIRST_PEER_KEY, st1, st2); - - BlockResult br = s0.applyBlock(b); - State s2 = br.getState(); - assertEquals(9950 - TCOST, s2.getBalance(ADDRESS_A)); - assertEquals(950 - TCOST, s2.getBalance(ADDRESS_B)); - assertEquals(100, s2.getBalance(ADDRESS_C)); - - AVector results = br.getResults(); - assertEquals(2, results.count()); - assertCVMEquals(50L,br.getResult(0).getValue()); // result for successful transfer - assertEquals(Constants.MAX_SUPPLY, br.getState().computeTotalFunds()); - } - - { // transfer with an incorrect sequence number - Transfer t1 = Transfer.create(ADDRESS_A,2, ADDRESS_C, 50); - SignedData st = KEYPAIR_A.signData(t1); - Block b = Block.of(System.currentTimeMillis(),FIRST_PEER_KEY, st); - BlockResult br = s.applyBlock(b); - AVector results = br.getResults(); - assertEquals(1, results.count()); - assertEquals(ErrorCodes.SEQUENCE, br.getResult(0).getErrorCode()); - } - - { // transfer amount greater than current balance - Transfer t1 = Transfer.create(ADDRESS_A,1, ADDRESS_C, 50000); - SignedData st = KEYPAIR_A.signData(t1); - Block b = Block.of(System.currentTimeMillis(),FIRST_PEER_KEY, st); - BlockResult br = s.applyBlock(b); - assertEquals(ErrorCodes.FUNDS, br.getResult(0).getErrorCode()); - - State newState = br.getState(); - assertEquals(Constants.MAX_SUPPLY, newState.computeTotalFunds()); - } - - - - { // sending money to NIKI, a new account - // Two new Accounts - State s0=s.putAccount(ADDRESS_C, AccountStatus.create(0L,KEYPAIR_C.getAccountKey())); - s0=s0.putAccount(ADDRESS_NIKI, AccountStatus.create(0L,KEYPAIR_NIKI.getAccountKey())); - - // System.out.println(ADDRESS_NIKI); - // System.out.println("Niki has "+s.getBalance(ADDRESS_NIKI).getValue()); - - long AMT = 500; - // System.out.println("Tansferring "+AMT+" to Niki"); - - Transfer t1 = Transfer.create(ADDRESS_A,1, ADDRESS_NIKI, AMT); - SignedData st = KEYPAIR_A.signData(t1); - Block b = Block.of(System.currentTimeMillis(),FIRST_PEER_KEY, st); - BlockResult br = s0.applyBlock(b); - // System.out.println("Transfer complete...."); - - State newState = br.getState(); - assertEquals(AMT, newState.getBalance(ADDRESS_NIKI)); - // System.out.println("Niki has "+newState.getBalance(ADDRESS_NIKI).getValue()); - - } - - } - - @Test - public void testDeploys() throws BadSignatureException { - State s = TestState.STATE; - ATransaction t1 = Invoke.create(InitTest.HERO,1,Reader.read("(def my-lib-address (deploy '(defn foo [x] x)))")); - AKeyPair kp = InitTest.HERO_KEYPAIR; - Block b1 = Block.of(s.getTimeStamp().longValue(),FIRST_PEER_KEY, kp.signData(t1)); - BlockResult br=s.applyBlock(b1); - assertFalse(br.isError(0),br.getResult(0).toString()); - - s = br.getState(); - - } - - @Test public void testManyDeploysMemoryRegression() throws BadSignatureException { - State s=TestState.STATE; - long lastSize=s.getMemorySize(); - assertTrue(lastSize>0); - - for (int i=1; i<=100; i++) { // i is sequence number - ATransaction trans=Invoke.create(InitTest.HERO, i, Reader.read("(def storage-example\r\n" - + " (deploy '(do (def stored-data nil)\r\n" - + " (defn get ^{:callable? true} [] stored-data)\r\n" - + " (defn set ^{:callable? true} [x] (def stored-data x)))))\r\n")); - AKeyPair kp = InitTest.HERO_KEYPAIR; - Block b=Block.of(s.getTimeStamp().longValue(),FIRST_PEER_KEY,kp.signData(trans)); - BlockResult br=s.applyBlock(b); - Result r=br.getResult(0); - assertFalse(r.isError(),r.toString()); - assertTrue(r.getValue() instanceof Address); - State newState=br.getState(); - - long size=newState.getMemorySize(); - if (size<=lastSize) { - fail("[i="+i+"] Original size: "+lastSize+" -> new size: "+size); - } - lastSize=size; - s=newState; - } - } - - @Test - public void testMemoryAccounting() throws BadSignatureException { - State s = TestState.STATE; - AKeyPair kp = InitTest.HERO_KEYPAIR; - - long initialMem=s.getMemorySize(); - - ATransaction t1 = Invoke.create(InitTest.HERO,1,Reader.read("(def a 1)")); - Block b1 = Block.of(s.getTimeStamp().longValue(),FIRST_PEER_KEY, kp.signData(t1)); - BlockResult br=s.applyBlock(b1); - - // should not be an error - assertNull(br.getErrorCode(0),br.getResult(0).toString()); - - s = br.getState(); - - // should have increased memory size for account - long newMem=s.getMemorySize(); - assertTrue(initialMem> sched2 = s.getSchedule(); - assertEquals(1L, sched2.count()); - // no change to target balance yet - assertEquals(BAL2 + 10000000, s.getBalance(TARGET)); - - // advance 999ms - ATransaction t3 = Invoke.create(InitTest.HERO,3, Reader.read("1")); - Block b3 = Block.of(s.getTimeStamp().longValue() + 999,FIRST_PEER_KEY, kp.signData(t3)); - BlockResult br3 = s.applyBlock(b3); - assertNull(br3.getErrorCode(0)); - s = br3.getState(); - // no change to target balance yet - assertEquals(BAL2 + 10000000, s.getBalance(TARGET)); - - // advance 1ms to trigger scheduled transfer - ATransaction t4 = Invoke.create(InitTest.HERO,4, Reader.read("1")); - Block b4 = Block.of(s.getTimeStamp().longValue() + 1,FIRST_PEER_KEY, kp.signData(t4)); - BlockResult br4 = s.applyBlock(b4); - assertNull(br4.getErrorCode(0)); - s = br4.getState(); - // no change to target balance yet - assertEquals(BAL2 + 20000000, s.getBalance(TARGET)); - - } - -} diff --git a/convex-core/src/test/java/convex/core/TransactionTest.java b/convex-core/src/test/java/convex/core/TransactionTest.java deleted file mode 100644 index cf99864a4..000000000 --- a/convex-core/src/test/java/convex/core/TransactionTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package convex.core; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; - -import org.junit.jupiter.api.Test; - -import convex.core.data.Address; -import convex.core.init.InitTest; -import convex.core.lang.ACVMTest; -import convex.core.lang.Context; -import convex.core.lang.Juice; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Transfer; - -/** - * Tests for Transactions, especially when applied in isolation to a State - */ -public class TransactionTest extends ACVMTest { - - protected TransactionTest() { - super(InitTest.STATE); - } - - Address HERO=InitTest.HERO; - Address VILLAIN=InitTest.VILLAIN; - long JP=Constants.INITIAL_JUICE_PRICE; - - protected State state() { - return context().getState(); - } - - protected State apply(ATransaction t) { - State s=state(); - Context ctx= s.applyTransaction(t); - assertFalse(ctx.isExceptional()); - return ctx.getState(); - } - - @Test - public void testTransfer() { - Transfer t1=Transfer.create(HERO, 1, VILLAIN, 1000); - State s=apply(t1); - long expectedFees=Juice.TRANSFER*JP; - assertEquals(1000+expectedFees,state().getAccount(HERO).getBalance()-s.getAccount(HERO).getBalance()); - assertEquals(expectedFees,s.getGlobalFees().longValue()); - } - -} diff --git a/convex-core/src/test/java/convex/core/crypto/AccountKeyTest.java b/convex-core/src/test/java/convex/core/crypto/AccountKeyTest.java deleted file mode 100644 index 16cfef971..000000000 --- a/convex-core/src/test/java/convex/core/crypto/AccountKeyTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package convex.core.crypto; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -import convex.core.data.AccountKey; -import convex.core.data.Blob; -import convex.core.data.Format; -import convex.core.exceptions.BadFormatException; - -public class AccountKeyTest { - @Test public void testEncoding() throws BadFormatException { - AccountKey ak=AccountKey.dummy("1234"); - Blob b=ak.getEncoding(); - AccountKey ak2=AccountKey.create(Format.read(b)); - - assertEquals(ak,ak2); - } -} diff --git a/convex-core/src/test/java/convex/core/crypto/Ed25519Test.java b/convex-core/src/test/java/convex/core/crypto/Ed25519Test.java deleted file mode 100644 index dec907955..000000000 --- a/convex-core/src/test/java/convex/core/crypto/Ed25519Test.java +++ /dev/null @@ -1,201 +0,0 @@ -package convex.core.crypto; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.Signature; -import java.security.SignatureException; -import java.security.spec.InvalidKeySpecException; -import java.util.Base64; - -import org.junit.jupiter.api.Test; - -import convex.core.data.ACell; -import convex.core.data.AccountKey; -import convex.core.data.Blob; -import convex.core.data.SignedData; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.RT; - -public class Ed25519Test { - - @Test - public void testKeyGen() { - AKeyPair kp1=Ed25519KeyPair.generate(); - AKeyPair kp2=Ed25519KeyPair.generate(); - assertNotEquals(kp1,kp2); - } - - @Test - public void testPublicKeyBytes() { - Ed25519KeyPair kp1=Ed25519KeyPair.generate(); - byte[] publicBytes=kp1.getPublicKeyBytes(); - byte[] addressBytes=kp1.getAccountKey().getBytes(); - assertArrayEquals(publicBytes,addressBytes); - } - - @Test - public void testKeyRebuilding() { - Ed25519KeyPair kp1=Ed25519KeyPair.generate(); - Ed25519KeyPair kp2=Ed25519KeyPair.create(kp1.getSeed()); - assertEquals(kp1,kp2); - assertEquals(kp1.getAccountKey(),kp2.getAccountKey()); - - ACell data=RT.cvm(1L); - - // TODO: figure out why encodings are different - //assertEquals(kp1.getEncodedPrivateKey(),kp2.getEncodedPrivateKey()); - assertEquals(kp1.signData(data),kp2.signData(data)); - } - - @Test - public void testPrivateKeyBytes() { - Ed25519KeyPair kp1=Ed25519KeyPair.generate(); - PrivateKey priv=kp1.getPrivate(); - PublicKey pub=kp1.getPublic(); - AccountKey address=kp1.getAccountKey(); - - ACell data=RT.cvm(1L); - SignedData sd1=kp1.signData(data); - assertTrue(sd1.checkSignature()); - - byte[] privateKeyBytes=kp1.getPrivate().getEncoded(); - - - Ed25519KeyPair kp2=Ed25519KeyPair.create(pub,priv); - assertEquals(address,kp2.getAccountKey()); - assertArrayEquals(privateKeyBytes,kp2.getPrivate().getEncoded()); - - SignedData sd2=kp2.signData(data); - assertTrue(sd2.checkSignature()); - - Blob pkb=Ed25519KeyPair.extractPrivateKey(priv); - AKeyPair kp3=Ed25519KeyPair.create(address, pkb); - - assertEquals(sd2,kp3.signData(data)); - } - - @Test - public void testCreateFromPrivateKey() { - Ed25519KeyPair kp1=Ed25519KeyPair.generate(); - PrivateKey priv=kp1.getPrivate(); - // PublicKey pub=kp1.getPublic(); - - Ed25519KeyPair kp2 = Ed25519KeyPair.create(priv); - assertTrue(kp1.equals(kp2)); - } - - @Test - public void testSeededKeyGen() { - AKeyPair kp1=Ed25519KeyPair.createSeeded(1337); - AKeyPair kp2=Ed25519KeyPair.createSeeded(1337); - AKeyPair kp3=Ed25519KeyPair.createSeeded(13378); - assertTrue(kp1.equals(kp2)); - assertFalse(kp2.equals(kp3)); - } - - @Test - public void testSigFromHex() throws BadFormatException, InvalidDataException { - String s="cafebabe000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; - ASignature s1=ASignature.fromHex(s); - s1.validate(); - - assertEquals(s,s1.toHexString()); - - assertThrows(IllegalArgumentException.class,()->ASignature.fromHex("00")); - } - - @Test - public void testAccountKeyRoundTrip() { - // Address should round trip to a Ed25519 public key and back again - AccountKey a=AccountKey.fromHex("0123456701234567012345670123456701234567012345670123456701234567"); - PublicKey pk=Ed25519KeyPair.publicKeyFromBytes(a.getBytes()); - AccountKey b=Ed25519KeyPair.extractAccountKey(pk); - assertEquals(a,b); - } - - /** - * Example test values from: https://stackoverflow.com/questions/53921655/rebuild-of-ed25519-keys-with-bouncy-castle-java - * @throws NoSuchAlgorithmException - * @throws IOException - * @throws InvalidKeySpecException - * @throws InvalidKeyException - * @throws SignatureException - */ - @Test - public void testExample() throws NoSuchAlgorithmException, IOException, InvalidKeySpecException, InvalidKeyException, SignatureException { - - byte [] msg = "eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc".getBytes(StandardCharsets.UTF_8); - - byte[] privateKeyBytes = Base64.getUrlDecoder().decode("nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A"); - byte[] publicKeyBytes = Base64.getUrlDecoder().decode("11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"); - assertEquals(32,privateKeyBytes.length); - assertEquals(32,publicKeyBytes.length); - - PublicKey publicKey=Ed25519KeyPair.publicKeyFromBytes(publicKeyBytes); - PrivateKey privateKey=Ed25519KeyPair.privateKeyFromBytes(privateKeyBytes); - - // Sign - Signature signer = Signature.getInstance("EdDSA"); - signer.initSign(privateKey); - signer.update(msg, 0, msg.length); - byte[] signature = signer.sign(); - - String sigText=Base64.getUrlEncoder().encodeToString(signature).replace("=", ""); - assertEquals("hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg",sigText); - - // Verify - Signature verifier = Signature.getInstance("EdDSA"); - verifier.initVerify(publicKey); - verifier.update(msg); - assertTrue(verifier.verify(signature)); - - // Bad verify - wrong signature - verifier.initVerify(publicKey); - verifier.update(msg); - assertFalse(verifier.verify(new byte[64])); - } - - @Test - public void testRFC8032() { - // From RFC8032 7.1 - { // Empty message - Blob seed=Blob.fromHex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"); - AccountKey pk=AccountKey.fromHex("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"); - Blob msg=Blob.EMPTY; - Blob esig=Blob.fromHex("e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"); - doSigTests(seed,pk,msg,esig); - } - } - - private void doSigTests(Blob seed, AccountKey pk, Blob msg, Blob expectedSig) { - ASignature sig=ASignature.fromBlob(expectedSig); - assertTrue(sig.verify(Blob.EMPTY, pk)); - - byte [] sodiumPK=new byte[32]; - byte [] sodiumSK=new byte[64]; - Providers.SODIUM_SIGN.cryptoSignSeedKeypair(sodiumPK, sodiumSK, seed.getBytes()); - - assertEquals(pk,Blob.wrap(sodiumPK)); - - byte [] sodiumSig=new byte[64]; - // ABlob ssk=Blob.wrap(sodiumPK).append(Blob.wrap(sodiumSK)); - Providers.SODIUM_SIGN.cryptoSignDetached(sodiumSig, msg.getBytes(), (int)msg.count(), sodiumSK); - - // TODO: figure out how to get LazySodium to replicate test vectors - assertEquals(sig.getSignatureBlob(),Blob.wrap(sodiumSig)); - - } - -} diff --git a/convex-core/src/test/java/convex/core/crypto/HashTest.java b/convex-core/src/test/java/convex/core/crypto/HashTest.java deleted file mode 100644 index b3d8aa4c3..000000000 --- a/convex-core/src/test/java/convex/core/crypto/HashTest.java +++ /dev/null @@ -1,102 +0,0 @@ -package convex.core.crypto; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; - -import org.bouncycastle.util.Arrays; -import org.junit.jupiter.api.Test; - -import convex.core.data.Blob; -import convex.core.data.Format; -import convex.core.data.Hash; -import convex.core.data.Ref; -import convex.core.data.Strings; -import convex.core.data.Tag; -import convex.core.util.Utils; - -/** - * Tests for hashing functionality - */ -public class HashTest { - public static final String GENESIS_HEADER = "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c"; - - @Test - void testBasicSHA256() { - // empty bytes - Hash h1 = Hashing.sha256(Utils.EMPTY_BYTES); - assertEquals("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", h1.toHexString()); - - // 32 empty bytes - Hash h1_32 = Hashing.sha256(new byte[32]); - assertEquals("66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925", h1_32.toHexString()); - - // Hash from https://www.freeformatter.com/sha256-generator.html - Hash h2 = Hashing.sha256("Hello"); - assertEquals("185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969", h2.toHexString()); - - // Hash from https://passwordsgenerator.net/sha256-hash-generator/ - Hash h3 = Hashing.sha256("Boo"); - assertEquals("BF66F3E41E470B7D073DB8C5FB82E737962D63080EDBCC4F9EF3C3CE735472EA", - h3.toHexString().toUpperCase()); - } - - @Test - void testBuiltinHashes() { - Hash h1 = Hashing.sha3(Utils.EMPTY_BYTES); - assertEquals("a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", h1.toHexString()); - assertEquals(Hash.EMPTY_HASH, h1); - - } - - @Test - void testNullHash() { - // hash of single zero byte (CVM null encoding), tested against multiple online calculators - assertEquals("5d53469f20fef4f8eab52b88044ede69c77a6a68a60728609fc4a65ff531e7d0", Hash.NULL_HASH.toHexString()); - - // different ways of getting the same result, should all correspond - assertSame(Hash.NULL_HASH, Hash.compute(null)); - assertSame(Hash.NULL_HASH, Ref.get(null).getHash()); - } - - @Test - void testDataLength() { - assertEquals(34, Hash.NULL_HASH.getEncodingLength()); - } - - @Test - void testExtractHash() { - Hash h = Hash.compute(Strings.create("foo")); - Blob b = Format.encodedBlob(h); - byte[] bs = b.getBytes(); - Hash h2 = Hash.wrap(bs, 2); // all bytes except the initial tag byte and count - assertEquals(h, h2); - } - - @Test - void testBitcoinGenesis() { - // Bitcoin genesis block header - // 0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c - // Should hash to: - // 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26 - // after double hashing and interpretation in little-endian format - byte[] genesisHeader = Utils.hexToBytes(GENESIS_HEADER); - assertEquals(80, genesisHeader.length); - // genesisHeader=Arrays.reverse(genesisHeader); - Hash h1 = Hashing.sha256(genesisHeader); - assertEquals("af42031e805ff493a07341e2f74ff58149d22ab9ba19f61343e2c86c71c5d66d", h1.toHexString()); - Hash h2 = h1.computeHash(Hashing.getSHA256Digest()); - assertEquals("6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000", h2.toHexString()); - // reversed bytes for little-endian format - assertEquals("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", - Utils.toHexString(Arrays.reverse(h2.getBytes()))); - } - - @Test - void testEquality() { - Hash h = Hash.NULL_HASH; - assertEquals(h, Hashing.sha3(new byte[] { Tag.NULL })); - assertEquals(h, h.toBlob()); - - assertEquals(0, h.compareTo(h.toBlob())); - } -} diff --git a/convex-core/src/test/java/convex/core/crypto/MnemonicTest.java b/convex-core/src/test/java/convex/core/crypto/MnemonicTest.java deleted file mode 100644 index 508210eb2..000000000 --- a/convex-core/src/test/java/convex/core/crypto/MnemonicTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package convex.core.crypto; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.Arrays; -import java.util.Random; - -import org.junit.jupiter.api.Test; - -import convex.core.data.Blob; - -public class MnemonicTest { - @Test - public void testZero() { - byte[] bs = new byte[16]; - assertEquals("a a a a a a a a a a a a", Mnemonic.encode(bs)); - Arrays.fill(bs, (byte) -1); - assertEquals("yoke yoke yoke yoke yoke yoke yoke yoke yoke yoke yoke yoke", Mnemonic.encode(bs)); - } - - @Test - public void testRoundTrips() { - Random r = new Random(78976976); - for (int i = 0; i < 100; i++) { - int n = r.nextInt(100) + 1; - byte[] bs = Blob.createRandom(r, n).getBytes(); - - // round trip byte array - String m = Mnemonic.encode(bs); - byte[] bs2 = Mnemonic.decode(m, 8 * n); - assertTrue(Arrays.equals(bs, bs2)); - assertEquals(n, bs2.length); - } - } - - @Test - public void testRandom() { - String mnem = Mnemonic.createSecureRandom(); - byte[] bs = Mnemonic.decode(mnem, 128); - assertEquals(16, bs.length); - } -} diff --git a/convex-core/src/test/java/convex/core/crypto/PBETest.java b/convex-core/src/test/java/convex/core/crypto/PBETest.java deleted file mode 100644 index cb5ec473d..000000000 --- a/convex-core/src/test/java/convex/core/crypto/PBETest.java +++ /dev/null @@ -1,16 +0,0 @@ -package convex.core.crypto; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.bouncycastle.util.encoders.Hex; -import org.junit.jupiter.api.Test; - -public class PBETest { - - @Test - public void testKeyDerivation() { - byte[] bs = PBE.deriveKey(new char[0], new byte[1], 128); - assertEquals(16, bs.length); - assertEquals("a352cdf92312599de774874ad9f3fcc5", Hex.toHexString(bs)); - } -} diff --git a/convex-core/src/test/java/convex/core/crypto/PEMToolsTest.java b/convex-core/src/test/java/convex/core/crypto/PEMToolsTest.java deleted file mode 100644 index 5bdba2fd1..000000000 --- a/convex-core/src/test/java/convex/core/crypto/PEMToolsTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package convex.core.crypto; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.security.PrivateKey; -import java.security.SecureRandom; - -import org.junit.jupiter.api.Test; - -import convex.core.data.AString; -import convex.core.data.Strings; -import convex.core.util.Utils; - -public class PEMToolsTest { - - String generateRandomHex(int size) { - SecureRandom random = new SecureRandom(); - byte password[] = new byte[size]; - random.nextBytes(password); - return Utils.toHexString(password); - } - - @Test - public void testPEMPrivateKey() { - AKeyPair keyPair = Ed25519KeyPair.generate(); - - String testPassword = generateRandomHex(32); - String pemText = null; - try { - pemText = PEMTools.encryptPrivateKeyToPEM(keyPair.getPrivate(), testPassword.toCharArray()); - } catch (Error e) { - throw e; - } - - assertTrue(pemText != null); - PrivateKey privateKey = null; - try { - privateKey = PEMTools.decryptPrivateKeyFromPEM(pemText, testPassword.toCharArray()); - } catch (Error e) { - throw e; - } - - AKeyPair importKeyPair = Ed25519KeyPair.create(privateKey); - AString data = Strings.create(generateRandomHex(1024)); - ASignature leftSignature = keyPair.sign(data.getHash()); - ASignature rightSignature = importKeyPair.sign(data.getHash()); - assertTrue(leftSignature.equals(rightSignature)); - - - // TODO: fix equality testing - // Blob key1 = keyPair.getEncodedPrivateKey(); - // Blob key2 = importKeyPair.getEncodedPrivateKey(); - //assertEquals(key1,key2); - //(keyPair,importKeyPair); - } -} diff --git a/convex-core/src/test/java/convex/core/crypto/PFXTest.java b/convex-core/src/test/java/convex/core/crypto/PFXTest.java deleted file mode 100644 index fc18e12dd..000000000 --- a/convex-core/src/test/java/convex/core/crypto/PFXTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package convex.core.crypto; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.io.File; -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.KeyStore; - -import org.junit.jupiter.api.Test; - -import convex.core.init.InitTest; -import convex.core.lang.RT; - -public class PFXTest { - - @Test public void testNewStore() throws IOException, GeneralSecurityException { - File f=File.createTempFile("temp-keystore", "pfx"); - - PFXTools.createStore(f, "test"); - - // check password is being applied - assertThrows(IOException.class,()->PFXTools.loadStore(f,"foobar")); - - // don't throw, no integrity checking on null? - //assertThrows(IOException.class,()->PFXUtils.loadStore(f,null)); - - KeyStore ks=PFXTools.loadStore(f, "test"); - AKeyPair kp=InitTest.HERO_KEYPAIR; - PFXTools.setKeyPair(ks, kp, "thehero"); - PFXTools.saveStore(ks, f, "test"); - - String alias=InitTest.HERO_KEYPAIR.getAccountKey().toHexString(); - KeyStore ks2=PFXTools.loadStore(f, "test"); - assertEquals(alias,ks2.aliases().asIterator().next()); - - AKeyPair kp2=PFXTools.getKeyPair(ks2,alias, "thehero"); - assertEquals(kp.signData(RT.cvm(1L)).getEncoding(),kp2.signData(RT.cvm(1L)).getEncoding()); - } -} diff --git a/convex-core/src/test/java/convex/core/crypto/ParamTestHash.java b/convex-core/src/test/java/convex/core/crypto/ParamTestHash.java deleted file mode 100644 index f2770e489..000000000 --- a/convex-core/src/test/java/convex/core/crypto/ParamTestHash.java +++ /dev/null @@ -1,47 +0,0 @@ -package convex.core.crypto; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.Arrays; -import java.util.Collection; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import convex.core.data.ABlob; -import convex.core.data.Blob; -import convex.core.data.Hash; -import convex.core.util.Utils; - -@RunWith(Parameterized.class) -public class ParamTestHash { - private Hash hash; - - public ParamTestHash(String label, Hash data) { - this.hash = data; - } - - @Parameterized.Parameters(name = "{index}: {0}") - public static Collection dataExamples() { - return Arrays.asList(new Object[][] { - { "Empty bytes", Hashing.sha256(Utils.EMPTY_BYTES) }, - { "Short string data", Hashing.sha256("Hello World") }, - { "Length 2 strict sublist of byte data", Hashing.sha256(new byte[] { 1, 2, 3, 4 }) }, - { "Bitcoin genesis header block", Blob.fromHex(HashTest.GENESIS_HEADER).computeHash(Hashing.getSHA256Digest()) } }); - } - - @Test - public void testHexRoundTrip() { - String hex = hash.toHexString(); - Hash d2 = Hash.fromHex(hex); - assertEquals(hash, d2); - assertEquals(hash.hashCode(), d2.hashCode()); - } - - @Test - public void testSlice() { - ABlob d = hash.slice(0, hash.count()); - assertEquals(hash.toBlob(), d); - } -} diff --git a/convex-core/src/test/java/convex/core/crypto/SignKeyPairTest.java b/convex-core/src/test/java/convex/core/crypto/SignKeyPairTest.java deleted file mode 100644 index bd21a1d05..000000000 --- a/convex-core/src/test/java/convex/core/crypto/SignKeyPairTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package convex.core.crypto; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertFalse; - -import org.junit.jupiter.api.Test; - -public class SignKeyPairTest { - - @Test - public void testSeeded() { - AKeyPair kp1 = AKeyPair.createSeeded(13); - AKeyPair kp2 = AKeyPair.createSeeded(13); - assertTrue(kp1.equals(kp2)); - AKeyPair kp3 = AKeyPair.createSeeded(1337); - assertFalse(kp1.equals(kp3) ); - } - - -} diff --git a/convex-core/src/test/java/convex/core/crypto/SymmetricTest.java b/convex-core/src/test/java/convex/core/crypto/SymmetricTest.java deleted file mode 100644 index 71c994dfb..000000000 --- a/convex-core/src/test/java/convex/core/crypto/SymmetricTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package convex.core.crypto; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; - -import java.util.Arrays; - -import javax.crypto.SecretKey; - -import org.junit.jupiter.api.Test; - -public class SymmetricTest { - @Test - public void testRoundTrip() { - String plainText = "Hello World!!!"; - - SecretKey key1 = Symmetric.createSecretKey(); - byte[] message = Symmetric.encrypt(key1, plainText); - String decrypted = Symmetric.decryptString(key1, message); - - assertEquals(plainText, decrypted); - - SecretKey key2 = Symmetric.createSecretKey(); - byte[] message2 = Symmetric.encrypt(key2, plainText); - assertFalse(Arrays.equals(message, message2)); - - } - - @Test - public void testSecretKeyVariance() { - assertNotEquals(Symmetric.createSecretKey(), Symmetric.createSecretKey()); - } - - @Test - public void testEncoded() { - SecretKey k = Symmetric.createSecretKey(); - byte[] encoded = k.getEncoded(); - assertEquals(16, encoded.length); - } -} diff --git a/convex-core/src/test/java/convex/core/crypto/WalletTest.java b/convex-core/src/test/java/convex/core/crypto/WalletTest.java deleted file mode 100644 index b471944e7..000000000 --- a/convex-core/src/test/java/convex/core/crypto/WalletTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package convex.core.crypto; - -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.io.File; - -import org.junit.jupiter.api.Test; - -public class WalletTest { - - @Test - public void testTempStore() { - String password="OmarSharif"; - File file=Wallet.createTempStore(password); - assertNotNull(file); - } -} diff --git a/convex-core/src/test/java/convex/core/data/AccountKeyTest.java b/convex-core/src/test/java/convex/core/data/AccountKeyTest.java deleted file mode 100644 index 329f81c67..000000000 --- a/convex-core/src/test/java/convex/core/data/AccountKeyTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.Random; - -import org.junit.jupiter.api.Test; - -public class AccountKeyTest { - - @Test - public void testChecksumRoundTrip() { - Random r = new Random(1585875); - for (int i = 0; i < 10; i++) { - Blob ba = Blob.createRandom(r, AccountKey.LENGTH); - AccountKey a = AccountKey.wrap(ba.getBytes()); - - String s = a.toChecksumHex(); - assertEquals(a, AccountKey.fromHex(s)); - assertEquals(a, AccountKey.fromChecksumHex(s)); - - String sl = s.toLowerCase(); - - assertEquals(a, AccountKey.fromHex(sl)); - assertThrows(IllegalArgumentException.class, () -> AccountKey.fromChecksumHex(sl)); - } - } - - - @Test - public void testEquality() { - String aString = Blob.createRandom(new Random(), AccountKey.LENGTH).toHexString(); - AccountKey a = AccountKey.fromHex(aString); - assertEquals(a, AccountKey.fromHex(aString)); - - // AccountKey should not be equal to Blob with same byte content - Blob b = a.toBlob(); - assertEquals(a, b); - - // AccountKey has comparison equality with Blob - assertEquals(0, a.compareTo(b)); - - BlobsTest.doBlobTests(a); - } -} diff --git a/convex-core/src/test/java/convex/core/data/AddressTest.java b/convex-core/src/test/java/convex/core/data/AddressTest.java deleted file mode 100644 index 3715d3775..000000000 --- a/convex-core/src/test/java/convex/core/data/AddressTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -public class AddressTest { - - @Test - public void testAddress1() { - Address a1=Address.create(1); - assertEquals("#1",a1.toString()); - String hex="0000000000000001"; - assertEquals(hex,a1.toHexString()); - - assertEquals(a1,Address.fromHex(hex)); - assertTrue(a1.compareTo(Blob.fromHex(hex))==0); - } - - @Test - public void testAddress2() { - Address a1=Address.create(13); - assertEquals("#13",a1.toString()); - } - - @Test - public void testParse() { - assertEquals("#1",Address.parse("#1").toString()); - assertEquals("#2",Address.parse("2").toString()); - assertEquals("#16",Address.parse("0x0000000000000010").toString()); - } -} diff --git a/convex-core/src/test/java/convex/core/data/BlobMapsTest.java b/convex-core/src/test/java/convex/core/data/BlobMapsTest.java deleted file mode 100644 index 5e70f8768..000000000 --- a/convex-core/src/test/java/convex/core/data/BlobMapsTest.java +++ /dev/null @@ -1,297 +0,0 @@ -package convex.core.data; - -import static convex.test.Assertions.assertCVMEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -import convex.core.data.prim.CVMLong; -import convex.core.data.type.Types; -import convex.core.exceptions.InvalidDataException; -import convex.core.init.InitTest; -import convex.core.lang.RT; -import convex.test.Samples; - -public class BlobMapsTest { - - @Test - public void testEmpty() throws InvalidDataException { - BlobMap m = BlobMaps.empty(); - - assertFalse(m.containsKey(Blob.EMPTY)); - assertFalse(m.containsKey(null)); - assertFalse(m.containsValue(RT.cvm(1L))); - assertFalse(m.containsValue(null)); - - assertEquals(0L, m.count()); - assertSame(m, m.dissoc(Blob.fromHex("cafe"))); - assertSame(m, m.dissoc(Blob.fromHex(""))); - - // checks vs regular map - assertFalse(m.equals(Maps.empty())); - assertFalse(Maps.empty().equals(m)); - - doBlobMapTests(m); - } - - @Test - public void testBadAssoc() throws InvalidDataException { - BlobMap m =BlobMaps.create(InitTest.HERO, RT.cvm(1L)); - m=m.assoc(InitTest.VILLAIN, RT.cvm(2L)); - assertEquals(2L,m.count()); - - assertNull(m.assoc(null, null)); - } - - - @Test - public void testAssoc() throws InvalidDataException { - Blob k1 = Blob.fromHex("cafe"); - Blob k2 = Blob.fromHex("cafebabe"); - Blob k3 = Blob.fromHex("ccca"); - BlobMap m = BlobMaps.create(k1, RT.cvm(17L)); - - doBlobMapTests(m); - - assertTrue(m.containsKey(k1)); - assertTrue(m.containsValue(RT.cvm(17L))); - assertFalse(m.containsKey(k2)); - assertFalse(m.containsKey(Blob.EMPTY)); - assertFalse(m.containsKey(null)); - - // add second entry - m = m.assoc(k2, RT.cvm(23L)); - assertEquals(2L, m.count()); - MapEntry e2 = m.entryAt(1); - assertSame(k2, e2.getKey()); - assertEquals(RT.cvm(23L), e2.getValue()); - - doBlobMapTests(m); - - // add third entry - m = m.assoc(k3, RT.cvm(34L)); - assertNotNull(m.toString()); - assertEquals(3L, m.count()); - MapEntry e3 = m.entryAt(2); - assertEquals(e3, m.getEntry(k3)); - assertEquals(RT.cvm(34L), e3.getValue()); - - doBlobMapTests(m); - - assertEquals(Vectors.of(17L,23L,34L),m.values()); - } - - @Test - public void testGet() throws InvalidDataException { - Blob k1 = Blob.fromHex("cafe"); - BlobMap m = BlobMaps.of(k1, 17L); - assertNull(m.get(Samples.MAX_EMBEDDED_STRING)); // needs a blob. String counts as non-existent key - assertCVMEquals(17L,m.get(k1)); - - assertNull(m.get((Object)null)); // Null counts as non-existent key when used as an Object arg - } - - - @Test - public void testBlobMapConstruction() throws InvalidDataException { - BlobMap m = BlobMaps.empty(); - for (int i = 0; i < 100; i++) { - Long l = (long) Integer.hashCode(i); - CVMLong cl = RT.cvm(l); - LongBlob lb = LongBlob.create(l); - m = m.assoc(lb, cl); - assertEquals(cl, m.get(lb)); - } - assertEquals(100L, m.count()); - m.validate(); - - doBlobMapTests(m); - - for (int i = 0; i < 100; i++) { - Long l = (long) Integer.hashCode(i); - LongBlob lb = LongBlob.create(l); - m = m.dissoc(lb); - assertFalse(m.containsKey(lb), "Index: " + lb.toHexString()); - } - assertSame(BlobMaps.empty(), m); - } - - @Test - public void testBlobMapRandomConstruction() throws InvalidDataException { - BlobMap m = BlobMaps.empty(); - for (int i = 0; i < 100; i++) { - Long l = (Long.MAX_VALUE / 91 * i * 18); - CVMLong cl=RT.cvm(l); - LongBlob lb = LongBlob.create(l); - m = m.assoc(lb, cl); - assertEquals(cl, m.get(lb)); - } - assertEquals(100L, m.count()); - m.validate(); - - doBlobMapTests(m); - - for (int i = 0; i < 100; i++) { - Long l = (Long.MAX_VALUE / 91 * i * 18); - LongBlob lb = LongBlob.create(l); - m = m.dissoc(lb); - assertFalse(m.containsKey(lb), "Index: " + lb.toHexString()); - } - assertSame(BlobMaps.empty(), m); - } - - @Test - public void testIdentity() { - Blob bb = Blob.fromHex("000000000000cafe"); - LongBlob bl = LongBlob.create(0xcafe); - Address ba=Address.create(0xcafe); - assertNotEquals(BlobMap.create(bb, bl), BlobMap.create(ba,bl)); // different entry key types - assertEquals(BlobMap.create(bb, bl), BlobMap.create(bl,bl)); // same entry key types - } - - @Test - public void testSingleEntry() throws InvalidDataException { - Blob k = Blob.fromHex("cafe"); - Blob k2 = Blob.fromHex("cafebabe"); - CVMLong val=RT.cvm(177777L); - BlobMap m = BlobMaps.create(k, val); - assertEquals(1L, m.count()); - - assertEquals(val, m.get(k)); - - assertNull(m.get(Blob.EMPTY)); - assertNull(m.get(k2)); - - assertSame(BlobMaps.empty(), m.dissoc(k)); - assertSame(m, m.dissoc(k2)); // long key miss - assertSame(m, m.dissoc(k.slice(0, 1))); // short prefix key miss - assertSame(m, m.dissoc(Blob.fromHex("caef"))); // partial prefix key miss - - MapEntry me = m.entryAt(0); - assertEquals(k, me.getKey()); - assertEquals(val, me.getValue()); - - doBlobMapTests(m); - } - - @Test - public void testPrefixEntryTwo() throws InvalidDataException { - Blob k1 = Blob.fromHex("cafe"); - Blob k2 = Blob.fromHex("cafebabe"); - BlobMap m = BlobMaps.of(k1, 17L, k2, 23L); - BlobMap m1 = BlobMaps.of(k1, 17L); - BlobMap m2 = BlobMaps.of(k2, 23L); - assertSame(m, m.dissoc(k1.slice(0, 1))); - assertEquals(m1, m.dissoc(k2)); - assertEquals(m2, m.dissoc(k1)); - - doBlobMapTests(m); - } - - @Test - public void testInitialPeersBlobMap() { - BlobMap bm = InitTest.STATE.getPeers(); - doBlobMapTests(bm); - - BlobMap fm =bm.filterValues(ps -> ps==bm.get(InitTest.FIRST_PEER_KEY)); - assertEquals(1L,fm.count()); - } - - @Test - public void testPrefixEntryThree() throws InvalidDataException { - Blob k1 = Blob.fromHex("cafe"); - Blob k2 = Blob.fromHex("cafebabe"); - Blob k3 = Blob.fromHex("cafefeed"); - BlobMap m = BlobMaps.of(k1, 17L, k2, 23L, k3, 47L); - m.validate(); - assertEquals(2L, m.dissoc(k1).count()); - - assertSame(m, m.assocEntry(m.getEntry(k1))); - assertEquals(m, m.assoc(k1, RT.cvm(17L))); - assertNotEquals(m, m.assoc(k1, RT.cvm(27L))); - - assertEquals(m, BlobMaps.of(k2, 23L, k3, 47L).assoc(k1, RT.cvm(17L))); - - Blob k0 = Blob.fromHex("ca"); - BlobMap m4 = m.assoc(k0, RT.cvm(7L)); - m4.validate(); - BlobMap m4b = BlobMaps.of(k0, 7L, k1, 17L, k2, 23L, k3, 47L); - assertEquals(m4, m4b); - doBlobMapTests(m4); - - doBlobMapTests(m); - } - - @Test - public void testDissocEntries() throws InvalidDataException { - BlobMap m = Samples.INT_BLOBMAP_7; - long n=m.count(); - - for (int i=0; i me=m.entryAt(i); - BlobMap dm= (BlobMap)m.dissoc(me.getKey()); - dm.validate(); - assertEquals(n-1,dm.count()); - BlobMap m2=dm.assocEntry(me); - assertEquals(m,m2); - } - } - - @Test - public void testDissocAll() throws InvalidDataException { - BlobMap m=BlobMaps.empty(); - long n=100; - - for (long i=0; i m = Samples.INT_BLOBMAP_7; - - assertSame(m, m.removeLeadingEntries(0)); - assertSame(BlobMaps.empty(), m.removeLeadingEntries(7)); - } - - @Test - public void testSmallIntBlobMap() { - BlobMap m = Samples.INT_BLOBMAP_7; - - for (int i = 0; i < 7; i++) { - MapEntry me = m.entryAt(i); - assertEquals(i, me.getValue().longValue()); - assertEquals(me, m.getEntry(me.getKey())); - } - doBlobMapTests(m); - } - - private void doBlobMapTests(BlobMap m) { - long n = m.count(); - - if (n >= 2) { - MapEntry e1 = m.entryAt(0); - MapEntry e2 = m.entryAt(n - 1); - assertTrue(e1.getKey().compareTo(e2.getKey()) < 0); - } - - assertEquals(Types.BLOBMAP,m.getType()); - - CollectionsTest.doMapTests(m); - } -} diff --git a/convex-core/src/test/java/convex/core/data/BlobsTest.java b/convex-core/src/test/java/convex/core/data/BlobsTest.java deleted file mode 100644 index f208f1695..000000000 --- a/convex-core/src/test/java/convex/core/data/BlobsTest.java +++ /dev/null @@ -1,239 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.Random; - -import org.junit.jupiter.api.Test; - -import convex.core.data.prim.CVMByte; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.test.Samples; - -public class BlobsTest { - @Test - public void testCompare() { - assertTrue(Blob.fromHex("01").compareTo(Blob.fromHex("FF")) < 0); - assertTrue(Blob.fromHex("40").compareTo(Blob.fromHex("30")) > 0); - assertTrue(Blob.fromHex("0102").compareTo(Blob.fromHex("0201")) < 0); - - assertTrue(Blob.fromHex("01").compareTo(Blob.fromHex("01")) == 0); - assertTrue(Blob.fromHex("").compareTo(Blob.fromHex("")) == 0); - } - - @Test - public void testNullHash() { - AArrayBlob d = Blob.create(new byte[] { Tag.NULL }); - assertTrue(d.getContentHash().equals(Hash.NULL_HASH)); - } - - @Test - public void testHexEquals() { - assertTrue(Blob.fromHex("0123").hexEquals(Blob.fromHex("0123"), 0, 4)); - assertTrue(Blob.fromHex("0125").hexEquals(Blob.fromHex("5123"), 1, 2)); - assertTrue(Blob.fromHex("012345").hexEquals(Blob.fromHex("a123"), 2, 2)); - - // zero length ranges - assertTrue(Blob.fromHex("0123").hexEquals(Blob.fromHex("4567"), 1, 0)); - assertTrue(Blob.fromHex("0123").hexEquals(Blob.fromHex("4567"), 0, 0)); - assertTrue(Blob.fromHex("0123").hexEquals(Blob.fromHex("4567"), 4, 0)); - } - - @Test - public void testHexMatchLength() { - assertEquals(4,Blob.fromHex("0123").hexMatchLength(Blob.fromHex("0123"), 0, 4)); - assertEquals(3,Blob.fromHex("0123").hexMatchLength(Blob.fromHex("012f"), 0, 4)); - assertEquals(3,Blob.fromHex("ffff0123").hexMatchLength(Blob.fromHex("ffff012f"), 4, 4)); - - assertEquals(0,Blob.fromHex("ffff0123").hexMatchLength(Blob.fromHex("ffff012f"), 3, 0)); - } - - - @Test - public void testFromHex() { - // bad length for blob - assertNull(Blob.fromHex("2")); - assertNull(Blob.fromHex("zz")); - } - - @Test - public void testBlobTreeConstruction() { - Random r = new Random(); - int clen = Blob.CHUNK_LENGTH; - int hclen = clen / 2; - Blob a = Blob.createRandom(r, clen); - Blob b = Blob.createRandom(r, hclen); - BlobTree bt = BlobTree.create(a, b); - - assertEquals(clen + hclen, bt.count()); - assertEquals(a, bt.getChunk(0)); - assertEquals(b, bt.getChunk(1)); - - doBlobTests(bt); - } - - @Test - public void testToLong() { - assertEquals(255L,Blob.fromHex("ff").toLong()); - assertEquals(-1L,Blob.fromHex("ffffffffffffffff").toLong()); - } - - @Test - public void testLongBlob() { - LongBlob b = LongBlob.create("cafebabedeadbeef"); - Blob bb = Blob.fromHex("cafebabedeadbeef"); - - assertEquals(b.longValue(),bb.longValue()); - - assertEquals(10, b.getHexDigit(1)); // 'a' - - for (int i = 0; i < 8; i++) { - assertEquals(b.byteAt(i), bb.byteAt(i)); - } - - for (int i = 0; i < 16; i++) { - assertEquals(b.getHexDigit(i), bb.getHexDigit(i)); - } - - assertTrue(bb.hexEquals(b)); - assertTrue(b.hexEquals(bb, 3, 10)); - - assertEquals(16, b.commonHexPrefixLength(bb)); - assertEquals(10, b.commonHexPrefixLength(bb.slice(0, 5))); - assertEquals(8, bb.commonHexPrefixLength(b.slice(0, 4))); - - // Longblobs considered as Blob type - assertEquals(bb, b); - assertEquals(b, bb); - - assertEquals(bb, b.toBlob()); - assertEquals(bb.hashCode(), b.hashCode()); - - doBlobTests(b); - - } - - @Test - public void testEmptyBlob() { - ABlob blob = Blob.EMPTY; - assertEquals(0L,blob.toLong()); - assertSame(blob,blob.getChunk(0)); - assertSame(blob,blob.slice(0,0)); - - doBlobTests(Blob.EMPTY); - } - - @Test - public void testBlobSlice() { - ABlob blob = Blob.fromHex("cafebabedeadbeef").slice(2,4); - assertEquals(8,blob.hexLength()); - doBlobTests(blob); - } - - @Test - public void testFullBlob() { - ABlob fb2 = Samples.FULL_BLOB.append(Samples.FULL_BLOB); - assertEquals(Blob.EMPTY, Samples.FULL_BLOB.getChunk(1)); - assertEquals(Samples.FULL_BLOB, fb2.getChunk(1)); - - assertTrue(Samples.FULL_BLOB.hexEquals(Samples.FULL_BLOB)); - - doBlobTests(Samples.FULL_BLOB); - } - - @Test - public void testEncodingSize() { - int el=(int) Samples.FULL_BLOB.getEncoding().count(); - assertEquals(Blobs.MAX_ENCODING_LENGTH,el); - assertEquals(Blob.MAX_ENCODING_LENGTH,el); - - assertTrue(Samples.MAX_EMBEDDED_BLOB.isEmbedded()); - assertFalse(Samples.FULL_BLOB.isEmbedded()); - } - - @Test - public void testBigBlob() throws InvalidDataException, BadFormatException { - BlobTree bb = Samples.BIG_BLOB_TREE; - long len = bb.count(); - - assertEquals(Samples.BIG_BLOB_LENGTH, len); - - assertSame(bb, bb.slice(0)); - assertSame(bb, bb.slice(0, len)); - - Blob firstChunk = bb.getChunk(0); - assertEquals(Blob.CHUNK_LENGTH, firstChunk.count()); - assertEquals(bb.byteAt(0), firstChunk.byteAt(0)); - - Blob blob = bb.toBlob(); - assertEquals(bb.count(), blob.count()); - assertEquals(bb.byteAt(len - 1), blob.byteAt(len - 1)); - assertEquals(bb.byteAt(0), blob.byteAt(0)); - assertEquals(bb.getChunk(1), blob.getChunk(1)); - - assertEquals(len * 2, bb.commonHexPrefixLength(bb)); - - bb.validate(); - - Ref rb = ACell.createPersisted(bb); - BlobTree bbb = Format.read(bb.getEncoding()); - bbb.validate(); - assertEquals(bb, bbb); - assertEquals(bb, rb.getValue()); - assertEquals(bb.count(), bb.hexMatchLength(bbb, 0, len)); - - doBlobTests(bb); - } - - @Test - public void testBlobTreeOutOfRange() { - assertThrows(IndexOutOfBoundsException.class, () -> Samples.BIG_BLOB_TREE.byteAt(Samples.BIG_BLOB_LENGTH)); - assertThrows(IndexOutOfBoundsException.class, () -> Samples.BIG_BLOB_TREE.slice(-1)); - assertThrows(IndexOutOfBoundsException.class, () -> Samples.BIG_BLOB_TREE.slice(1, Samples.BIG_BLOB_LENGTH)); - } - - @Test - public void testBlobFormat() throws BadFormatException { - byte[] bf = new byte[] { Tag.BLOB, 0 }; - Blob b = Format.read(Blob.wrap(bf)); - assertEquals(0, b.count()); - assertNotEquals(b.getHash(), Hash.EMPTY_HASH); - - doBlobTests(b); - } - - /** - * Generic tests for an arbitrary ABlob instance - * @param a Any blob to test, might not be canonical - */ - public static void doBlobTests(ABlob a) { - long n = a.count(); - assertTrue(n >= 0L); - ABlob canonical=a.toCanonical(); - - // copy of the Blob data - ABlob b=Blob.wrap(a.getBytes()).toCanonical(); - - if (a.isRegularBlob()) { - assertEquals(a.count(),b.count()); - assertEquals(canonical,b); - } - - if (n>0) { - assertEquals(n*2,a.commonHexPrefixLength(b)); - - assertEquals(a.slice(n/2,n/2),b.slice(n/2, n/2)); - - assertEquals(a.get(n-1),CVMByte.create(a.byteAt(n-1))); - } - - ObjectsTest.doAnyValueTests(canonical); - } -} diff --git a/convex-core/src/test/java/convex/core/data/BlocksTest.java b/convex-core/src/test/java/convex/core/data/BlocksTest.java deleted file mode 100644 index a28402ce9..000000000 --- a/convex-core/src/test/java/convex/core/data/BlocksTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -import convex.core.Block; -import convex.core.crypto.AKeyPair; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.BadSignatureException; -import convex.core.init.InitTest; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Transfer; - -public class BlocksTest { - - @Test - public void testEquality() throws BadFormatException { - long ts = System.currentTimeMillis(); - Block b1 = Block.create(ts, InitTest.FIRST_PEER_KEY,Vectors.empty()); - Block b2 = Block.create(ts, InitTest.FIRST_PEER_KEY,Vectors.empty()); - - assertEquals(b1, b2); - assertEquals(b1.hashCode(), b2.hashCode()); - assertEquals(b1.getHash(), b2.getHash()); - assertEquals(b1.getEncoding(), b2.getEncoding()); - assertEquals(b1, Format.read(b2.getEncoding())); - - assertEquals(0,b1.getTransactions().count()); - - RecordTest.doRecordTests(b1); - } - - @Test - public void testTransactions() throws BadSignatureException { - AKeyPair kp = InitTest.HERO_KEYPAIR; - - ATransaction t = Transfer.create(InitTest.HERO,0, InitTest.VILLAIN, 1000); - SignedData st = kp.signData(t); - - long ts = System.currentTimeMillis(); - Block b = Block.create(ts, InitTest.FIRST_PEER_KEY,Vectors.of(st)); - assertEquals(1, b.length()); - assertEquals(t, b.getTransactions().get(0).getValue()); - - RecordTest.doRecordTests(b); - - } -} diff --git a/convex-core/src/test/java/convex/core/data/CollectionsTest.java b/convex-core/src/test/java/convex/core/data/CollectionsTest.java deleted file mode 100644 index 07098b700..000000000 --- a/convex-core/src/test/java/convex/core/data/CollectionsTest.java +++ /dev/null @@ -1,138 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.Iterator; -import java.util.ListIterator; -import java.util.Map; - -import convex.core.lang.RT; - -/** - * Tests for general collection types - */ -public class CollectionsTest { - - /** - * Generic tests for any sequence - * @param a Any Sequence Value - */ - public static void doSequenceTests(ASequence a) { - long n = a.count(); - - if (n > 0) { - T last = a.get(n - 1); - T first = a.get(0); - assertEquals(n - 1, a.longLastIndexOf(last)); - assertEquals(0L, a.longIndexOf(first)); - - assertSame(a, a.assoc(0, first)); - assertSame(a, a.assoc(n - 1, last)); - } - - // Out of range assocs should return null - assertNull( a.assoc(-2, null)); - assertNull( a.assoc(n + 2, null)); - - ListIterator it = a.listIterator(); - assertThrows(UnsupportedOperationException.class, () -> it.set(null)); - assertThrows(UnsupportedOperationException.class, () -> it.add(null)); - - assertThrows(IndexOutOfBoundsException.class, () -> a.listIterator(-1)); - assertThrows(IndexOutOfBoundsException.class, () -> a.listIterator(n + 1)); - - assertThrows(IndexOutOfBoundsException.class, () -> a.getElementRef(-1)); - assertThrows(IndexOutOfBoundsException.class, () -> a.getElementRef(n)); - - doCollectionTests(a); - } - - static final Keyword UNLIKELY_KEYWORD=Keyword.create("this-is-not-likely-to-happen-at-random"); - - /** - * Generic tests for any data structure - * @param a Any Data Structure - */ - public static void doDataStructureTests(ADataStructure a) { - long n = a.count(); - if (n == 0) { - assertSame(a.empty(), a); - } else { - assertFalse(a.isEmpty()); - T first =a.get(0); - assertEquals(first,a.getElementRef(0).getValue()); - } - - assertFalse(RT.bool(a.get(UNLIKELY_KEYWORD))); - assertSame(Keywords.FOO,a.get(UNLIKELY_KEYWORD,Keywords.FOO)); - - assertEquals(n,a.size()); - - ObjectsTest.doAnyValueTests(a); - } - - /** - * Generic tests for any collection - * @param a Any Collection - */ - public static void doCollectionTests(ACollection a) { - Iterator it = a.iterator(); - assertThrows(Throwable.class, () -> it.remove()); - - doDataStructureTests(a); - } - - /** - * Generic tests for any map - * @param a Any Map - */ - public static void doMapTests(AMap a) { - long n = a.count(); - if (n == 0) { - assertThrows(IndexOutOfBoundsException.class, () -> a.entryAt(0)); - } else { - MapEntry me = a.entryAt(n / 2); - assertNotNull(me); - assertSame(a, a.assocEntry(me)); - - K key = me.getKey(); - V value = me.getValue(); - - assertEquals(a.get(key), value); - - // remove and add back entry - AMap da = a.dissoc(key); - assertEquals(n - 1, da.count()); - assertNull(da.getEntry(key)); - assertEquals(a, da.assocEntry(me)); - } - - { // test that entrySet works properly - java.util.Set> es=a.entrySet(); - assertEquals(es.size(),a.size()); - AMap t=a; - for (Map.Entry me: es) { - t=t.dissoc(me.getKey()); - } - assertSame(a.empty(),t); - } - - assertThrows(IndexOutOfBoundsException.class, () -> a.entryAt(-1)); - assertThrows(IndexOutOfBoundsException.class, () -> a.entryAt(n)); - - doDataStructureTests(a); - } - - /** - * Generic tests for any set - * @param a Any Set - */ - public static void doSetTests(ASet a) { - doCollectionTests(a); - } -} diff --git a/convex-core/src/test/java/convex/core/data/EncodingTest.java b/convex-core/src/test/java/convex/core/data/EncodingTest.java deleted file mode 100644 index 38a8b3a4e..000000000 --- a/convex-core/src/test/java/convex/core/data/EncodingTest.java +++ /dev/null @@ -1,223 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.nio.BufferUnderflowException; -import java.nio.ByteBuffer; - -import org.junit.jupiter.api.Test; - -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.lang.RT; -import convex.test.Samples; - -public class EncodingTest { - - @Test public void testVLCLongLength() throws BadFormatException, BufferUnderflowException { - ByteBuffer bb=ByteBuffer.allocate(100); - bb.put(Tag.LONG); - Format.writeVLCLong(bb, Long.MAX_VALUE); - - // must be max long length plus tag - assertEquals(Format.MAX_VLC_LONG_LENGTH+1,bb.position()); - - bb.flip(); - Blob b=Blob.fromByteBuffer(bb); - - CVMLong max=RT.cvm(Long.MAX_VALUE); - - assertEquals(max,Format.read(b)); - - assertEquals(max.getEncoding(),b); -; } - -// @Test public void testBigIntegerRegression() throws BadFormatException { -// BigInteger expected=BigInteger.valueOf(-4223); -// assertEquals(expected,Format.read("0adf01")); -// -// assertThrows(BadFormatException.class,()->Format.read("0affdf01")); -// } -// -// @Test public void testBigIntegerRegression2() throws BadFormatException { -// BigInteger b=BigInteger.valueOf(1496216); -// Blob blob=Format.encodedBlob(b); -// assertEquals(b,Format.read(blob)); -// } -// -// @Test public void testBigIntegerRegression3() throws BadFormatException { -// Blob blob=Blob.fromHex("0a801d"); -// assertThrows(BadFormatException.class,()->Format.read(blob)); -// } -// -// @Test public void testBigDecimalRegression() throws BadFormatException { -// Blob blob=Blob.fromHex("0e001d"); -// BigDecimal bd=Format.read(blob); -// assertEquals(BigDecimal.valueOf(29),bd); -// assertEquals(blob,Format.encodedBlob(bd)); -// } - - @Test public void testEmbeddedRegression() throws BadFormatException { - Keyword k=Keyword.create("foo"); - Blob b=Format.encodedBlob(k); - Object o=Format.read(b); - assertEquals(k,o); - assertTrue(Format.isEmbedded(k)); - Ref r=Ref.get(o); - assertTrue(r.isDirect()); - } - -// @Test public void testEmbeddedBigInteger() throws BadFormatException { -// BigInteger one=BigInteger.ONE; -// assertFalse(Format.isEmbedded(one)); -// AVector v=Vectors.of(BigInteger.ONE,BigInteger.TEN); -// assertFalse(v.getRef(0).isEmbedded()); -// Blob b=Format.encodedBlob(v); -// AVector v2=Format.read(b); -// assertEquals(v,v2); -// assertEquals(b,Format.encodedBlob(v2)); -// } - - @Test public void testBadFormats() throws BadFormatException { - // test excess high order bits above the long range - assertEquals(-3717066608267863778L,((CVMLong)Format.read("09ccb594f3d1bde9b21e")).longValue()); - assertThrows(BadFormatException.class,()->{ - Format.read("09b3ccb594f3d1bde9b21e"); - }); - - // test excess high bytes for -1 - assertThrows(BadFormatException.class,()->Format.read("09ffffffffffffffffff7f")); - - // test excess high bytes for negative number - assertEquals(RT.cvm(Long.MIN_VALUE),(CVMLong)Format.read("09ff808080808080808000")); - assertThrows(BadFormatException.class,()->Format.read("09ff80808080808080808000")); - - } - - @Test public void testStringRegression() throws BadFormatException { - StringShort s=StringShort.create("��zI�&$\\ž1�����4�E4�a8�#?$wD(�#"); - Blob b=Format.encodedBlob(s); - StringShort s2=Format.read(b); - assertEquals(s,s2); - } - - @Test public void testListRegression() throws BadFormatException { - MapEntry me=MapEntry.create(Blobs.fromHex("41da2aa427dc50975dd0b077"), RT.cvm(-1449690165L)); - List l=List.reverse(me); - assertEquals(me,l.reverse()); // ensure MapEntry gets converted to canonical vector - - Blob b=Format.encodedBlob(l); - List l2=Format.read(b); - - assertEquals(l,l2); - } - - @Test public void testMalformedStrings() { - // bad examples constructed using info from https://www.w3.org/2001/06/utf-8-wrong/UTF-8-test.html - assertThrows(BadFormatException.class,()->Format.read("300180")); // continuation only - assertThrows(BadFormatException.class,()->Format.read("3001FF")); - } - - @Test public void testCanonical() { - assertTrue(Format.isCanonical(Vectors.empty())); - assertTrue(Format.isCanonical(null)); - assertTrue(Format.isCanonical(RT.cvm(1))); - assertTrue(Format.isCanonical(Blob.create(new byte[1000]))); // should be OK - assertFalse(Blob.create(new byte[10000]).isCanonical()); // too big to be canonical - } - - @Test public void testReadBlobData() throws BadFormatException { - Blob d=Blob.fromHex("cafebabe"); - Blob edData=Format.encodedBlob(d); - AArrayBlob dd=Format.read(edData); - assertEquals(d,dd); - assertSame(edData,dd.getEncoding()); // should re-use encoded data object directly - } - - @Test - public void testBadMessageTooLong() throws BadFormatException { - ACell o=Samples.FOO; - Blob data=Format.encodedBlob(o).append(Blob.fromHex("ff")).toBlob(); - assertThrows(BadFormatException.class,()->Format.read(data)); - } - - @Test - public void testMessageLength() throws BadFormatException { - // empty bytebuffer, therefore no message lengtg - ByteBuffer bb1=Blob.fromHex("").toByteBuffer(); - assertThrows(IndexOutOfBoundsException.class,()->Format.peekMessageLength(bb1)); - - // bad first byte! Needs to carry if 0x40 or more - ByteBuffer bb2=Blob.fromHex("43").toByteBuffer(); - assertThrows(BadFormatException.class,()->Format.peekMessageLength(bb2)); - - // maximum message length - ByteBuffer bb2a=Blob.fromHex("BF7F").toByteBuffer(); - assertEquals(Format.LIMIT_ENCODING_LENGTH,Format.peekMessageLength(bb2a)); - - // overflow message length - Blob overflow=Blob.fromHex("C000"); - ByteBuffer bb2aa=overflow.toByteBuffer(); - assertThrows(BadFormatException.class,()->Format.peekMessageLength(bb2aa)); - - ByteBuffer bb2b=Blob.fromHex("8043").toByteBuffer(); - assertEquals(67,Format.peekMessageLength(bb2b)); - - - ByteBuffer bb3=Blob.fromHex("FFFF").toByteBuffer(); - assertThrows(BadFormatException.class,()->Format.peekMessageLength(bb3)); - } - - @Test - public void testHexDigits() { - byte[] bs=new byte[8]; - - Blob src=Blob.fromHex("cafebabe"); - Format.writeHexDigits(bs, 2, src, 2, 4); - assertEquals(Blobs.fromHex("00000204feba0000"),Blob.wrap(bs)); - - Format.writeHexDigits(bs, 3, src, 0, 3); - assertEquals(Blobs.fromHex("0000020003caf000"),Blob.wrap(bs)); - } - - @Test - public void testWriteRef() { - // TODO: consider whether this is valid - // shouldn't be allowed to write a Ref directly as a top-level message - // ByteBuffer b=ByteBuffer.allocate(10); - // assertThrows(IllegalArgumentException.class,()->Format.write(b, Ref.create("foo"))); - } - - @Test - public void testMaxLengths() { - int ME=Format.MAX_EMBEDDED_LENGTH; - - Blob maxEmbedded=Blob.create(new byte[ME-3]); // Maximum embedded length - Blob notEmbedded=Blob.create(new byte[ME-2]); // Non-embedded length - assertTrue(maxEmbedded.isEmbedded()); - assertFalse(notEmbedded.isEmbedded()); - assertEquals(ME, maxEmbedded.getEncodingLength()); - - // Maps - assertEquals(2+16*ME,MapLeaf.MAX_ENCODING_LENGTH); - assertEquals(4+16*ME,MapTree.MAX_ENCODING_LENGTH); - assertEquals(Maps.MAX_ENCODING_SIZE,MapTree.MAX_ENCODING_LENGTH); - - // Vectors - assertEquals(1+Format.MAX_VLC_LONG_LENGTH+17*ME,VectorLeaf.MAX_ENCODING_SIZE); - - // Blobs - Blob maxBlob=Blob.create(new byte[Blob.CHUNK_LENGTH]); - assertEquals(Blob.MAX_ENCODING_LENGTH,maxBlob.getEncodingLength()); - assertEquals(Blob.MAX_ENCODING_LENGTH,Blobs.MAX_ENCODING_LENGTH); - - // Address - Address maxAddress=Address.create(Long.MAX_VALUE); - assertEquals(1+Format.MAX_VLC_LONG_LENGTH,Address.MAX_ENCODING_LENGTH); - assertEquals(Address.MAX_ENCODING_LENGTH,maxAddress.getEncodingLength()); - } -} diff --git a/convex-core/src/test/java/convex/core/data/FuzzTestFormat.java b/convex-core/src/test/java/convex/core/data/FuzzTestFormat.java deleted file mode 100644 index 841d3a17c..000000000 --- a/convex-core/src/test/java/convex/core/data/FuzzTestFormat.java +++ /dev/null @@ -1,97 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.nio.BufferUnderflowException; -import java.nio.ByteBuffer; -import java.util.Random; - -import org.junit.jupiter.api.Test; - -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.exceptions.MissingDataException; -import convex.core.lang.RT; -import convex.core.util.Utils; - -/** - * Fuzz testing for data formats. - * - * "Testing leads to failure, and failure leads to understanding." - Burt Rutan - */ -public class FuzzTestFormat { - - private static final int NUM_FUZZ = 3000; - private static Random r = new Random(7855875); - - @Test - public void fuzzTest() { - // create lots of blobs, see what is readable - - for (int i = 0; i < NUM_FUZZ; i++) { - long stime = System.currentTimeMillis(); - r.setSeed(i * 1000); - Blob b = Blob.createRandom(r, 100); - try { - doFuzzTest(b); - } catch (BadFormatException e) { - /* OK */ - } catch (BufferUnderflowException e) { - /* also OK */ - } catch (MissingDataException e) { - /* also OK */ - } - - if (System.currentTimeMillis() > stime + 100) { - System.err.println("Slow fuzz test: " + b); - } - } - } - - private static void doFuzzTest(Blob b) throws BadFormatException { - ByteBuffer bb = b.getByteBuffer(); - - ACell v = Format.read(bb); - - // If we have read the object, check that we can validate as a cell, at minimum - try { - RT.validate(v); - } catch (InvalidDataException e) { - throw new BadFormatException("Validation failed",e); - } - - // if we manage to read the object and it is not a Ref, it must be in canonical - // format! - assertTrue(Format.isCanonical(v),()->"Not canonical: "+Utils.getClassName(v)); - - Blob b2 = Format.encodedBlob(v); - assertEquals(v, Format.read(b2), - () -> "Expected to be able to regenerate value: " + v + " of type " + Utils.getClass(v)); - assertEquals(bb.position(), b2.count(), () -> { - return "Bad length re-reading " + Utils.getClass(v) + ": " + v + " with encoding " + b.toHexString() - + " and re-encoding" + b2.toHexString(); - }); - - // recursive fuzzing on this value - // this is good to test small mutations of - if (r.nextDouble() < 0.8) { - doMutationTest(b2); - } - - } - - public static void doMutationTest(Blob b) { - try { - byte[] bs = b.getBytes(); - bs[r.nextInt(bs.length)] += (byte) r.nextInt(255); - doFuzzTest(Blob.wrap(bs)); - } catch (BadFormatException e) { - /* OK */ - } catch (BufferUnderflowException e) { - /* also OK */ - } catch (MissingDataException e) { - /* also OK */ - } - } -} diff --git a/convex-core/src/test/java/convex/core/data/GenTestAnyValue.java b/convex-core/src/test/java/convex/core/data/GenTestAnyValue.java deleted file mode 100644 index 666ce62f7..000000000 --- a/convex-core/src/test/java/convex/core/data/GenTestAnyValue.java +++ /dev/null @@ -1,143 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.runner.RunWith; - -import com.pholser.junit.quickcheck.From; -import com.pholser.junit.quickcheck.Property; -import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; - -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.store.Stores; -import convex.core.util.Utils; -import convex.test.Samples; -import convex.test.generators.ValueGen; - -@RunWith(JUnitQuickcheck.class) -public class GenTestAnyValue { - - @Property - public void printFormats(@From(ValueGen.class) Object o) { - String s=Utils.print(o); - assertNotNull(s); - assertTrue(s.length()>0); - - // TODO: handle all reader cases - //Object o2=Reader.read(s); - // if (o!=null) assertNotNull(o2); - } - - @Property - public void genericTests(@From(ValueGen.class) ACell o) throws InvalidDataException, BadFormatException { - ObjectsTest.doAnyValueTests(o); - } - - @Property - public void testUpdateRefs(@From(ValueGen.class) Object o) { - if (o instanceof ACell) { - ACell rc=(ACell) o; - int n=rc.getRefCount(); - assertThrows(IndexOutOfBoundsException.class,()->rc.getRef(n)); - assertThrows(IndexOutOfBoundsException.class,()->rc.getRef(-1)); - if (n>0 ) { - assertNotNull(rc.getRef(0)); - assertSame(rc,rc.updateRefs(r->r)); - } - } - } - - @Property - public void testFuzzing(@From(ValueGen.class) ACell o) throws InvalidDataException { - Blob b=Format.encodedBlob(o); - FuzzTestFormat.doMutationTest(b); - - if (o instanceof ACell) { - // break all the refs! This should still pass validateCell(), since it woun't change structure. - ACell c=((ACell)o).updateRefs(r->{ - byte[] badBytes=r.getHash().getBytes(); - Utils.writeInt(badBytes, 28,12255); - Hash badHash=Hash.wrap(badBytes); - return Ref.forHash(badHash); - }); - c.validateCell(); - } - } - - @Property - public void validEmbedded(@From(ValueGen.class) ACell o) throws InvalidDataException, BadFormatException { - if (Format.isEmbedded(o)) { - ACell.createPersisted(o); // may have child refs to persist - Blob data=Format.encodedBlob(o); - - ACell o2=Format.read(data); - - // check round trip properties - assertEquals(o,o2); - AArrayBlob data2=Format.encodedBlob(o2); - assertEquals(data,data2); - assertTrue(Format.isEmbedded(o2)); - - // when we persist a ref to an embedded object, should be the object itself - Ref ref=Ref.get(o); - assertEquals(data,ref.getEncoding()); // should encode ref same as value - } else { - // when we persist a ref to non-embedded object, should be a ref type - Ref ref=Ref.get(o); - Blob b=ref.getEncoding(); - assertEquals(Tag.REF,b.byteAt(0)); - assertEquals(Ref.INDIRECT_ENCODING_LENGTH,b.count()); - } - } - - @Property (trials=20) - public void dataRoundTrip(@From(ValueGen.class) ACell o) throws BadFormatException { - Blob data=Format.encodedBlob(o); - - // introduce a small offset to ensure blobs working correctly - data=Samples.ONE_ZERO_BYTE_DATA.append(data).slice(1).toBlob(); - - Ref dataRef=Ref.get(o).persist(); // ensure in store - Hash hash=Hash.compute(o); - assertEquals(dataRef.getHash(),hash); - - // re-read data, should be canonical - ACell o2=Format.read(data); - assertTrue(Format.isCanonical(o2)); - - // equality checks - assertEquals(o,o2); - if (o!=null) assertEquals(o.hashCode(),o2.hashCode()); - assertEquals(hash,Hash.compute(o2)); - - // re-encoding - AArrayBlob data2=Format.encodedBlob(o2); - assertEquals(data,data2); - - // simulate retrieval via hash - Ref dataRef2=Stores.current().refForHash(hash); - if (dataRef2!=null) { - // Have in store - assertEquals(dataRef,dataRef2); - Ref r2=Ref.forHash(hash); - ACell o3=r2.getValue(); - assertEquals(o,o3); - } - } - - @Property - public void setInclusion(@From(ValueGen.class) ACell o) throws BadFormatException, InvalidDataException { - ASet s=Sets.of(o); - s.validate(); - assertEquals(o,s.iterator().next()); - - ASet s2=s.exclude(o); - assertTrue(s2.isEmpty()); - } - -} diff --git a/convex-core/src/test/java/convex/core/data/GenTestBlobs.java b/convex-core/src/test/java/convex/core/data/GenTestBlobs.java deleted file mode 100644 index 9aa79fd0e..000000000 --- a/convex-core/src/test/java/convex/core/data/GenTestBlobs.java +++ /dev/null @@ -1,25 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.runner.RunWith; - -import com.pholser.junit.quickcheck.From; -import com.pholser.junit.quickcheck.Property; -import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; - -import convex.core.util.Utils; -import convex.test.generators.BlobGen; - -@RunWith(JUnitQuickcheck.class) -public class GenTestBlobs { - - @Property - public void testToLong(@From(BlobGen.class) ABlob blob) { - long len=blob.count(); - long lv=blob.toLong(); - - int slen=Math.min(8,Utils.checkedInt(len)); - assertEquals(lv,blob.slice(len-slen,slen).toLong()); - } -} diff --git a/convex-core/src/test/java/convex/core/data/GenTestDataStructures.java b/convex-core/src/test/java/convex/core/data/GenTestDataStructures.java deleted file mode 100644 index fe75e6352..000000000 --- a/convex-core/src/test/java/convex/core/data/GenTestDataStructures.java +++ /dev/null @@ -1,72 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.Collection; - -import org.junit.runner.RunWith; - -import com.pholser.junit.quickcheck.From; -import com.pholser.junit.quickcheck.Property; -import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; - -import convex.core.lang.RT; -import convex.test.generators.DataStructureGen; - -@RunWith(JUnitQuickcheck.class) -public class GenTestDataStructures { - - @SuppressWarnings("rawtypes") - @Property - public void empty(@From(DataStructureGen.class) ADataStructure a) { - long c = a.count(); - - ADataStructure e = a.empty(); - if (c == 0) { - assertSame(a, e); - } - - assertEquals(0, e.count()); - assertTrue(e.isEmpty()); - assertEquals(0, e.size()); - } - - @SuppressWarnings("rawtypes") - @Property - public void testSequence(@From(DataStructureGen.class) ADataStructure a) { - ASequence seq = RT.sequence(a); - assertEquals(seq.count(), a.count()); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Property - public void testJavaInterface(@From(DataStructureGen.class) ADataStructure a) { - if (a instanceof Collection) { - ACollection coll = (ACollection) a; - - assertThrows(UnsupportedOperationException.class, () -> coll.clear()); - assertThrows(UnsupportedOperationException.class, () -> coll.addAll(coll)); - assertThrows(UnsupportedOperationException.class, () -> coll.retainAll(coll)); - assertThrows(UnsupportedOperationException.class, () -> coll.removeAll(coll)); - assertThrows(UnsupportedOperationException.class, () -> coll.remove(null)); - - if (coll instanceof List) { - List list = (List) a; - ASequence seq = (ASequence) list; // must be an ASequence - - assertThrows(UnsupportedOperationException.class, () -> list.set(0, null)); - assertThrows(UnsupportedOperationException.class, () -> list.addAll(0, coll)); - assertThrows(UnsupportedOperationException.class, () -> list.remove(0)); - - int n = list.size(); - if (n > 0) { - assertEquals(list.get(n - 1), RT.nth(seq, n - 1)); - } - } - } - } - -} diff --git a/convex-core/src/test/java/convex/core/data/GenTestMap.java b/convex-core/src/test/java/convex/core/data/GenTestMap.java deleted file mode 100644 index e347cef0e..000000000 --- a/convex-core/src/test/java/convex/core/data/GenTestMap.java +++ /dev/null @@ -1,72 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.runner.RunWith; - -import com.pholser.junit.quickcheck.From; -import com.pholser.junit.quickcheck.Property; -import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; - -import convex.test.generators.MapGen; -import convex.test.generators.PrimitiveGen; - -@RunWith(JUnitQuickcheck.class) -public class GenTestMap { - - @SuppressWarnings({ "rawtypes" }) - @Property - public void primitiveAssoc(@From(MapGen.class) AHashMap m, @From(PrimitiveGen.class) ACell prim) { - long n = m.count(); - long expectedN = (m.containsKey(prim)) ? n : n + 1; - - // add the key - m = m.assoc(prim, prim); - assertSame(prim, m.get(prim)); - assertEquals(expectedN, m.size()); - - // remove the key - m = m.dissoc(prim); - assertNull(m.get(prim)); - assertEquals(expectedN - 1, m.size()); - - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Property - public void mapToIdentity(@From(MapGen.class) AHashMap m) { - AHashMap m2 = m.mapEntries(e -> e); - - // check that the map is unchanged - assertTrue(m2 == m); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Property - public void merging1(@From(MapGen.class) AHashMap m) { - m = m.filterValues(v -> v != null); // don't want null values, to avoid accidental entry removal - - AMap m1 = m.mergeWith(m, (a, b) -> a); - assertEquals(m, m1); - - AMap m2 = m.mergeWith(Maps.empty(), (a, b) -> a); - assertEquals(m, m2); - - AMap m3 = m.mergeWith(Maps.empty(), (a, b) -> b); - assertSame(Maps.empty(), m3); - - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Property - public void merging2(@From(MapGen.class) AHashMap a, - @From(MapGen.class) AHashMap b) { - long[] c = new long[] { 0L }; - a.mergeWith(b, (va, vb) -> ((c[0]++ & 1) == 0L) ? va : vb); - assertTrue(c[0] >= Math.max(a.count(), b.count())); - } - -} diff --git a/convex-core/src/test/java/convex/core/data/GenTestMessages.java b/convex-core/src/test/java/convex/core/data/GenTestMessages.java deleted file mode 100644 index 9ff883669..000000000 --- a/convex-core/src/test/java/convex/core/data/GenTestMessages.java +++ /dev/null @@ -1,25 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.nio.ByteBuffer; - -import org.junit.runner.RunWith; - -import com.pholser.junit.quickcheck.Property; -import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; - -import convex.core.exceptions.BadFormatException; - -@RunWith(JUnitQuickcheck.class) -public class GenTestMessages { - - @Property - public void messageLengthVLC(Long a) throws BadFormatException { - ByteBuffer bb = ByteBuffer.allocate(Format.MAX_VLC_LONG_LENGTH); - Format.writeVLCLong(bb, a); - bb.flip(); - assertEquals(bb.remaining(), Format.getVLCLength(a)); - assertEquals(a, Format.readVLCLong(bb)); - } -} diff --git a/convex-core/src/test/java/convex/core/data/GenTestStrings.java b/convex-core/src/test/java/convex/core/data/GenTestStrings.java deleted file mode 100644 index 675265629..000000000 --- a/convex-core/src/test/java/convex/core/data/GenTestStrings.java +++ /dev/null @@ -1,19 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.runner.RunWith; - -import com.pholser.junit.quickcheck.From; -import com.pholser.junit.quickcheck.Property; -import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; - -import convex.test.generators.StringGen; - -@RunWith(JUnitQuickcheck.class) -public class GenTestStrings { - @Property - public void testStringProperties(@From(StringGen.class) AString a) { - assertEquals(a,Strings.create(a.toString())); - } -} diff --git a/convex-core/src/test/java/convex/core/data/GenTestVectors.java b/convex-core/src/test/java/convex/core/data/GenTestVectors.java deleted file mode 100644 index fb03977ea..000000000 --- a/convex-core/src/test/java/convex/core/data/GenTestVectors.java +++ /dev/null @@ -1,44 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.runner.RunWith; - -import com.pholser.junit.quickcheck.From; -import com.pholser.junit.quickcheck.Property; -import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; - -import convex.test.generators.VectorGen; - -@RunWith(JUnitQuickcheck.class) -public class GenTestVectors { - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Property - public void testGenericProperties(@From(VectorGen.class) AVector a) { - VectorsTest.doVectorTests(a); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Property - public void testConcatPrefixes(@From(VectorGen.class) AVector a, @From(VectorGen.class) AVector b) { - long al = a.count(); - long bl = b.count(); - - AVector ab = a.concat(b); - VectorsTest.doVectorTests(ab); // useful to test these - - assertEquals(al + bl, ab.count()); - - assertEquals(al, a.commonPrefixLength(a)); - assertTrue(al <= a.commonPrefixLength(ab)); - assertTrue(bl <= b.concat(a).commonPrefixLength(b)); - - long cp = a.commonPrefixLength(b); - assertEquals(cp, b.commonPrefixLength(a)); - if (cp > 0) { - assertEquals(a.get(cp - 1), b.get(cp - 1)); - } - } -} diff --git a/convex-core/src/test/java/convex/core/data/KeywordTest.java b/convex-core/src/test/java/convex/core/data/KeywordTest.java deleted file mode 100644 index 5a51e06a5..000000000 --- a/convex-core/src/test/java/convex/core/data/KeywordTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.ByteBuffer; - -import org.junit.jupiter.api.Test; - -import convex.core.Constants; -import convex.core.exceptions.BadFormatException; -import convex.core.util.Text; -import convex.test.Samples; - -public class KeywordTest { - - @Test - public void testBadKeywords() { - assertNotNull(Keyword.create(Text.whiteSpace(Constants.MAX_NAME_LENGTH))); - - // null return for invalid names - assertNull(Keyword.create(Text.whiteSpace(Constants.MAX_NAME_LENGTH+1))); - assertNull(Keyword.create("")); - assertNull(Keyword.create((String)null)); - - // exception for invalid names using createChecked - assertThrows(IllegalArgumentException.class, () -> Keyword.createChecked(Text.whiteSpace(Constants.MAX_NAME_LENGTH+1))); - assertThrows(IllegalArgumentException.class, () -> Keyword.createChecked("")); - assertThrows(IllegalArgumentException.class, () -> Keyword.createChecked((AString)null)); - assertThrows(IllegalArgumentException.class, () -> Keyword.createChecked((String)null)); - } - - @Test - public void testBadFormat() { - // should fail because this is an empty String - assertThrows(BadFormatException.class, () -> Keyword.read(Blob.fromHex("00").toByteBuffer())); - } - - @Test - public void testRoundTripRegression() throws BadFormatException { - Keyword k=Keyword.create("key17"); - - Blob enc=Blob.fromHex("33056b65793137"); - - assertEquals(enc,k.getEncoding()); - - ByteBuffer bb=enc.getByteBuffer(); - assertEquals(Tag.KEYWORD,bb.get()); - Keyword k2=Keyword.read(bb); - - assertEquals(k,k2); - assertEquals(enc,k.getEncoding()); - } - - @Test - public void testNormalKeyword() { - Keyword k = Keyword.create("foo"); - assertEquals(Samples.FOO, k); - - assertEquals("foo", k.getName().toString()); - assertEquals(":foo", k.toString()); - assertEquals(5, k.getEncoding().length); // tag+length+3 name - - } -} diff --git a/convex-core/src/test/java/convex/core/data/ListsTest.java b/convex-core/src/test/java/convex/core/data/ListsTest.java deleted file mode 100644 index 14b18b47b..000000000 --- a/convex-core/src/test/java/convex/core/data/ListsTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -import convex.core.data.prim.CVMLong; -import convex.test.Samples; - -public class ListsTest { - - @Test - public void testEmptyList() { - AList e = Lists.empty(); - assertEquals(0, e.size()); - assertSame(e, Lists.of()); - assertFalse(e.contains(null)); - doListTests(e); - } - - @Test - public void testPrimitiveEquality() { - assertEquals(Lists.of(1L), Lists.of(1L)); - } - - @Test - public void testListToArray() { - assertEquals(3, Lists.of(1, 2, 3).toArray().length); - assertEquals(0, Lists.empty().toArray().length); - } - - @Test public void testDrop() { - assertSame(Lists.empty(), Lists.empty().drop(0)); - assertNull(Lists.empty().drop(1)); - - AList ll=Lists.of(1L, 2L, 3L); - - assertSame(ll,ll.drop(0)); - assertSame(Lists.empty(),ll.drop(3)); - - assertEquals(Lists.of(2L,3L),ll.drop(1)); - assertEquals(Lists.of(3L),ll.drop(2)); - assertNull(ll.drop(5)); - - assertEquals(Lists.of(299),Samples.INT_LIST_300.drop(299)); - assertNull(Samples.INT_LIST_300.drop(400)); - } - - @Test - public void testToString() { - assertEquals("(1 2 3)",Lists.of(1L, 2L, 3L).toString()); - } - - @Test - public void testContainsAll() { - assertTrue(Lists.of(1, 2, 3).containsAll(Sets.of(2, 3))); - assertFalse(Lists.of(1, 2).containsAll(Sets.of(2, 3, 4))); - } - - @Test - public void testGenericListSamples() { - doListTests(Lists.of(1, 2L, Vectors.empty())); - doListTests(Samples.INT_LIST_10); - doListTests(Samples.INT_LIST_300); - } - - /** - * Generic tests for any list - * @param a Any List - */ - public static void doListTests(AList a) { - long n = a.count(); - - if (n == 0) { - assertSame(Lists.empty(), a); - } else { - T first = a.get(0); - assertEquals(first, a.iterator().next()); - } - - assertEquals(a, Lists.of(a.toArray())); - - // call inherited sequence tests - CollectionsTest.doSequenceTests(a); - } -} diff --git a/convex-core/src/test/java/convex/core/data/MapsTest.java b/convex-core/src/test/java/convex/core/data/MapsTest.java deleted file mode 100644 index 19b0d5799..000000000 --- a/convex-core/src/test/java/convex/core/data/MapsTest.java +++ /dev/null @@ -1,361 +0,0 @@ -package convex.core.data; - -import static convex.test.Assertions.assertCVMEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.function.Predicate; - -import org.junit.jupiter.api.Test; - -import convex.core.data.prim.CVMBool; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.exceptions.ValidationException; -import convex.core.init.InitTest; -import convex.core.lang.RT; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Transfer; -import convex.core.util.Bits; -import convex.test.Samples; - -/** - * Tests for CVM Map data structures. - */ -public class MapsTest { - - @Test - public void testMapBuilding() throws InvalidDataException, ValidationException { - int SIZE = 1000; - - AMap m = Maps.empty(); - for (long i = 0; i < SIZE; i++) { - CVMLong ci=RT.cvm(i); - assertFalse(m.containsKey(ci)); - m = m.assoc(ci, ci); - // Log.debug(i+ ": "+m); - if ((i < 10) || (i % 23 == 0)) m.validate(); // PERF: only check some steps - assertEquals(i + 1, m.size()); - assertEquals(ci, m.get(ci)); - assertTrue(m.containsKey(ci)); - } - - long C = 1000000; - assertEquals(SIZE * (SIZE - 1) / 2 + C, (long) m.reduceValues((acc, a) -> acc + a.longValue(), C)); - assertEquals(SIZE * (SIZE - 1) + C, (long) m.reduceEntries((acc, e) -> acc + e.getKey().longValue() + e.getValue().longValue(), C)); - - for (long i = 0; i < SIZE; i++) { - CVMLong ci=RT.cvm(i); - assertTrue(m.containsKey(ci)); - m = m.dissoc(ci); - assertEquals(SIZE - i - 1, m.size()); - assertNull(m.get(ci)); - if ((i < 10) || (i % 31 == 0)) m.validate(); // PERF: only check some steps - } - - assertTrue(m.isEmpty()); - } - - @Test - public void testDiabolicalMaps() { - // test that we can at least get hashes without nasty recursion - // shouldn't create maps without hashes in key values - assertNotNull(Samples.DIABOLICAL_MAP_2_10000.getHash()); - assertNotNull(Samples.DIABOLICAL_MAP_30_30.getHash()); - - // TestCollections.doMapTests(Samples.DIABOLICAL_MAP_2_10000); - // TestCollections.doMapTests(Samples.DIABOLICAL_MAP_30_30); - } - - @Test - public void testTreeIndexesForDigits() { - assertEquals(0, Bits.indexForDigit(0, (short) 0x111)); - assertEquals(-1, Bits.indexForDigit(2, (short) 0x111)); - assertEquals(1, Bits.indexForDigit(4, (short) 0x111)); - assertEquals(2, Bits.indexForDigit(8, (short) 0x111)); - assertEquals(-1, Bits.indexForDigit(15, (short) 0x111)); - - assertEquals(0, Bits.indexForDigit(4, (short) 0x010)); - assertEquals(-1, Bits.indexForDigit(3, (short) 0x010)); - assertEquals(-1, Bits.indexForDigit(5, (short) 0x010)); - } - - @Test - public void testTreePositionsForDigits() { - assertEquals(0, Bits.positionForDigit(0, (short) 0x111)); - assertEquals(1, Bits.positionForDigit(2, (short) 0x111)); - assertEquals(1, Bits.positionForDigit(4, (short) 0x111)); - assertEquals(2, Bits.positionForDigit(8, (short) 0x111)); - assertEquals(3, Bits.positionForDigit(15, (short) 0x111)); - - assertEquals(0, Bits.positionForDigit(4, (short) 0x010)); - assertEquals(0, Bits.positionForDigit(3, (short) 0x010)); - assertEquals(1, Bits.positionForDigit(5, (short) 0x010)); - } - - @Test - public void testContains() { - assertTrue(Samples.LONG_MAP_10.containsValue(RT.cvm(3L))); - assertFalse(Samples.LONG_MAP_10.containsValue(RT.cvm(12L))); - assertTrue(Samples.LONG_MAP_100.containsValue(RT.cvm(12L))); - assertFalse(Samples.LONG_MAP_100.containsValue(RT.cvm(100L))); - } - - @Test - public void testContainsRef() { - assertTrue(Samples.LONG_MAP_10.containsKeyRef(Ref.get(RT.cvm(1L)))); - assertFalse(Samples.LONG_MAP_10.containsKeyRef(Ref.get(RT.cvm(12L)))); - assertTrue(Samples.LONG_MAP_100.containsKeyRef(Ref.get(RT.cvm(12L)))); - assertFalse(Samples.LONG_MAP_100.containsKeyRef(Ref.get(RT.cvm(100L)))); - } - - @Test - public void testMapToString() { - AMap m = Maps.empty(); - assertEquals("{}", m.toString()); - m = m.assoc(RT.cvm(1L), RT.cvm(2L)); - assertEquals("{1 2}", m.toString()); - } - - @Test - public void testTruncateHexDigits() throws InvalidDataException, BadFormatException { - AHashMap m = Samples.LONG_MAP_100; - assertEquals(100, m.count()); - AHashMap m1 = m.mapEntries(e -> (e.getKeyHash().getHexDigit(0) < 8) ? e : null); - AHashMap m2 = m.mapEntries(e -> (e.getKeyHash().getHexDigit(0) >= 8) ? e : null); - assertEquals(100, m1.count() + m2.count()); - m1.validate(); - m2.validate(); - // should merge back to original map. Useful also for testing empty children - // merges. - assertEquals(m, m1.mergeDifferences(m2, (a, b) -> (a == null) ? b : a)); - } - - @Test - public void regressionEmbeddedTransfer() throws BadFormatException { - ATransaction trans=Transfer.create(InitTest.HERO,0, InitTest.HERO, 58); - CVMLong key=CVMLong.create(23771L); - AMap m=Maps.create(key,trans); - MapEntry me=m.entryAt(0); - assertEquals(key,me.getKey()); - assertEquals(trans,me.getValue()); - - // transaction should never be embedded - assertEquals(trans.isEmbedded(),me.getValueRef().isEmbedded()); - - Blob b=m.getEncoding(); - AMap m2=Format.read(b); - - assertEquals(m,m2); - - Blob b2=m2.getEncoding(); - assertEquals(b,b2); - } - - @Test - public void testMapToNull() throws InvalidDataException, BadFormatException { - // check that we obtain the singleton instance, using a map to null to remove - // keys - assertSame(Maps.empty(), Samples.LONG_MAP_100.mapEntries(e -> null)); - assertSame(Maps.empty(), Samples.LONG_MAP_10.mapEntries(e -> null)); - } - - @Test - public void testTreeDigitForIndex() { - assertEquals(5, MapTree.digitForIndex(0, (short) 0x020)); - - assertEquals(0, MapTree.digitForIndex(0, (short) 0x111)); - assertEquals(4, MapTree.digitForIndex(1, (short) 0x111)); - assertEquals(8, MapTree.digitForIndex(2, (short) 0x111)); - } - - @Test - public void testBadDigitNegative() { - assertThrows(IllegalArgumentException.class, () -> MapTree.digitForIndex(-1, (short) 0x111)); - assertThrows(IllegalArgumentException.class, () -> MapTree.digitForIndex(3, (short) 0x111)); - } - - @Test - public void testSmallMergeIndentity() { - AHashMap m0 = Maps.empty(); - AHashMap m1 = Maps.of(1, 2, 3, 4); - AHashMap m2 = Maps.of(3, 4, 5, 6); - AHashMap m3 = Maps.of(1, 2, 3, 4, 5, 6); - - assertSame(m0, m1.mergeWith(m3, (a, b) -> null)); - assertSame(m3, m3.mergeWith(m3, (a, b) -> a)); - assertSame(m2, m2.mergeWith(m3, (a, b) -> a)); - assertSame(m2, m2.mergeWith(m1, (a, b) -> a)); - assertSame(m0, m3.mergeWith(m3, (a, b) -> null)); - assertTrue(m3.equals(m2.mergeWith(m3, (a, b) -> b))); - - } - - @Test - public void regressionCreateWithDuplicateEntries() { - MapEntry e = MapEntry.of(1L, 2L); - AMap m = Maps.create(Vectors.of(e, e)); - assertEquals(1, m.size()); - } - - @Test - public void regressionTestMerge() throws InvalidDataException { - AHashMap m = Maps.of(Blob.fromHex("798b809c"), null); - m.validate(); - // this should remove the entry, since mergeWith removes null values - AHashMap m1 = m.mergeWith(m, (a, b) -> a); - assertSame(Maps.empty(), m1); - - CollectionsTest.doMapTests(m); - } - - @Test - public void testDuplicateEntryCreate() { - AMap m = Maps.of(10, 2, 10, 3); - assertEquals(1, m.size()); - assertEquals(RT.cvm(10L), m.entryAt(0).getKey()); - } - - @Test - public void testFilterHex() { - MapLeaf m = Maps.of(1, true, 2, true, 3, true, -1000, true); - assertEquals(4L,m.count()); - - // TODO: selective filter - //assertEquals(Maps.of(3L, true), m.filterHexDigits(0, 64)); // hex digit 0 = 6 only - - assertSame(m, m.filterHexDigits(0, 0xFFFF)); // all digits selected - assertSame(Maps.empty(), m.filterHexDigits(0, 0)); // all digits selected - } - - private static final Predicate EVEN_PRED = a -> { - return (a.longValue() & 1L) == 0L; - }; - - @Test - public void testFilterValues10() { - AHashMap m = Samples.LONG_MAP_10; - AHashMap m2 = m.filterValues(EVEN_PRED); - assertEquals(5, m2.size()); - } - - @Test - public void testFilterValues100() { - AHashMap m = Samples.LONG_MAP_100; - AHashMap m2 = m.filterValues(EVEN_PRED); - assertEquals(50, m2.size()); - - } - - @Test - public void testEmpty() { - AMap m=Maps.empty(); - assertEquals(0L,m.count()); - assertSame(m,Maps.empty()); - - assertEquals(2L,m.getEncoding().count()); - } - - @Test - public void testEquals() { - AMap m = Samples.LONG_MAP_100; - assertNotEquals(m, m.assoc(null, null)); - assertNotEquals(m, m.assoc(RT.cvm(2L), RT.cvm(3L))); - - CollectionsTest.doMapTests(m); - } - - @Test - public void testEqualsKeys() { - assertTrue(Maps.empty().equalsKeys(Maps.of())); - assertFalse(Maps.of(1, 2, 3, 4).equalsKeys(Maps.of(1, 2, 4, 5))); - assertTrue(Maps.of(1, 2, 3, 4).equalsKeys(Maps.of(1, 4, 3, 2))); - } - - @SuppressWarnings("unchecked") - @Test - public void testTreeMapBuilding() { - assertThrows(Throwable.class, () -> MapTree.create(new MapEntry[] { MapEntry.of(1, 2) }, 0)); - } - - @Test - public void testMapEntry() { - AMap m = Maps.of(1L, 2L); - MapEntry me = m.getEntry(RT.cvm(1L)); - assertCVMEquals(1L, me.getKey()); - assertCVMEquals(2L, me.getValue()); - - // out of range assocs - assertNull( me.assoc(2, RT.cvm(3L))); - assertNull( me.assoc(-1, RT.cvm(0L))); - - assertThrows(UnsupportedOperationException.class, () -> me.setValue(RT.cvm(6L))); - - assertEquals(me, me.assoc(0, RT.cvm(1L))); - assertEquals(me, me.assoc(1, RT.cvm(2L))); - - - assertTrue(me.contains(RT.cvm(1L))); - assertTrue(me.contains(RT.cvm(2L))); - assertFalse(me.contains(CVMBool.TRUE)); - assertFalse(me.contains(null)); - - // generic tests for MapEntry treated as a vector - VectorsTest.doVectorTests(me); - } - - @Test - public void testAssocs() { - AMap m = Maps.of(1L, 2L); - assertSame(m, m.assoc(RT.cvm(1L), RT.cvm(2L))); - - CollectionsTest.doMapTests(m); - } - - @Test - public void testConj() { - AMap m = Maps.of(1L, 2L); - AMap me = Maps.of(1L, 2L, 3L, 4L); - assertEquals(m, m.conj(Vectors.of(1L, 2L))); - assertEquals(me, m.conj(Vectors.of(3L, 4L))); - assertEquals(me, m.conj(MapEntry.of(3L, 4L))); - - // failures with conj'ing things that aren't valid map entries - assertNull(m.conj(Vectors.empty())); - assertNull(m.conj(Vectors.of(1L))); - assertNull(m.conj(Vectors.of(1L, 2L, 3L))); - assertNull(m.conj(null)); - - CollectionsTest.doMapTests(me); - - } - - @Test - public void testMergeWith() { - AHashMap m = Maps.of(1L, 1L, 2L, 2L, 3L, 3L, 4L, 4L, 5L, 5L, 6L, 6L, 7L, 7L, 8L, 8L); - AHashMap m2 = m.mergeWith(m, (a, b) -> ((a.longValue() & 1L) == 0L) ? a : null); - assertEquals(4, m2.size()); - - AHashMap bm = Maps.coerce(Samples.LONG_MAP_100); - AHashMap sm = Maps.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); - - // change values in big map using small map - AHashMap bm2 = bm.mergeWith(sm, (a, b) -> { - return (a == null) ? b : a; - }); - assertEquals(100, bm2.count()); - assertEquals(bm2, sm.mergeWith(bm, (a, b) -> { - return (b == null) ? a : b; - })); - - CollectionsTest.doMapTests(m); - CollectionsTest.doMapTests(m2); - } -} diff --git a/convex-core/src/test/java/convex/core/data/ObjectsTest.java b/convex-core/src/test/java/convex/core/data/ObjectsTest.java deleted file mode 100644 index af2b31012..000000000 --- a/convex-core/src/test/java/convex/core/data/ObjectsTest.java +++ /dev/null @@ -1,183 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import convex.core.Constants; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.store.AStore; -import convex.core.store.MemoryStore; -import convex.core.store.Stores; -import convex.core.util.Utils; - -/** - * Generic test functions for arbitrary Data Objects. - */ -public class ObjectsTest { - - /** - * Generic tests for a Cell - * - * @param a Cell to test - */ - public static void doCellTests(ACell a) { - if (a==null) return; - - assertEquals(a.getEncodingLength(),a.getEncoding().count()); - - try { - a.validateCell(); - // doCellStorageTest(a); // TODO: Maybe fix after we have ACell.toDirect() - } catch (InvalidDataException e) { - throw Utils.sneakyThrow(e); - } - - if (a.getRefCount()>0) { - doRefContainerTests(a); - } - - if (a.isCanonical()) { - // Canonical objects should map to themselves - assertSame(a,a.toCanonical()); - } else { - // non-canonical objects should map to a canonical object - ACell canon=a.toCanonical(); - assertNotSame(canon,a); - assertTrue(canon.isCanonical()); - assertEquals(a,canon); - } - - doCellRefTests(a); - } - - private static void doCellRefTests(ACell a) { - Ref cachedRef=a.cachedRef; - Ref ref=a.getRef(); - if (cachedRef!=null) assertSame(ref,cachedRef); - assertSame(ref,a.getRef()); - - assertEquals(a.isEmbedded(),ref.isEmbedded()); - - Ref refD=ref.toDirect(); - assertTrue(ref.equalsValue(refD)); - assertTrue(refD.equalsValue(ref)); - - } - - @SuppressWarnings("unused") - private static void doCellStorageTest(ACell a) throws InvalidDataException { - - AStore temp=Stores.current(); - try { - // test using a new memory store - MemoryStore ms=new MemoryStore(); - Stores.setCurrent(ms); - - Ref r=a.getRef(); - - Hash hash=r.getHash(); - - assertNull(ms.refForHash(hash)); - - // persist the Ref - ACell.createPersisted(a); - - // retrieve from store - Ref rr=ms.refForHash(hash); - - // should be able to retrieve and validate complete structure - assertNotNull(rr,()->"Failed to retrieve from store with "+Utils.getClassName(a) + " = "+a); - ACell b=rr.getValue(); - b.validate(); - assertEquals(a,b); - } finally { - Stores.setCurrent(temp); - } - } - - /** - * Generic tests for any CVM Value - * - * @param a Value to test - */ - public static void doAnyValueTests(ACell a) { - Hash h=Hash.compute(a); - - boolean embedded=Format.isEmbedded(a); - - Ref r = Ref.get(a).persist(); - assertEquals(h,r.getHash()); - assertEquals(a, r.getValue()); - - Blob encoding = Format.encodedBlob(a); - if (a==null) { - assertEquals(Blob.NULL_ENCODING,encoding); - } else { - assertEquals(a.getTag(),encoding.byteAt(0)); // Correct Tag - assertSame(encoding,a.getEncoding()); // should be same cached encoding - assertEquals(encoding.length,a.getEncodingLength()); - - if (a.isCVMValue()) { - assertNotNull(a.getType()); - } - - } - - - // Any encoding should be less than or equal to the limit - assertTrue(encoding.length <= Format.LIMIT_ENCODING_LENGTH); - - // If length exceeds MAX_EMBEDDED_LENGTH, cannot be an embedded value - if (encoding.length > Format.MAX_EMBEDDED_LENGTH) { - assertFalse(Format.isEmbedded(a),()->"Testing: "+Utils.getClassName(a)+ " = "+Utils.toString(a)); - } - - // tests for memory size - if (a!=null) { - long memorySize=a.getMemorySize(); - long encodingSize=a.getEncodingLength(); - int rc=a.getRefCount(); - long childMem=0; - for (int i=0; i childRef=a.getRef(i); - long cms=childRef.getMemorySize(); - childMem+=cms; - } - if (embedded) { - assertEquals(memorySize,childMem); - } else { - assertEquals(memorySize,encodingSize+childMem+Constants.MEMORY_OVERHEAD); - } - } - - - try { - ACell a2; - a2 = Format.read(encoding); - assertEquals(a, a2); - } catch (BadFormatException e) { - throw new Error("Can't read encoding: " + encoding.toHexString(), e); - } - - doCellTests(a); - } - - /** - * Tests for any value implementing the IRefContainer interface - * - * @param a - */ - private static void doRefContainerTests(ACell a) { - long tcount = Utils.totalRefCount(a); - int rcount = Utils.refCount(a); - - assertTrue(rcount <= tcount); - } - -} diff --git a/convex-core/src/test/java/convex/core/data/ParamTestBlobs.java b/convex-core/src/test/java/convex/core/data/ParamTestBlobs.java deleted file mode 100644 index 764fe47b2..000000000 --- a/convex-core/src/test/java/convex/core/data/ParamTestBlobs.java +++ /dev/null @@ -1,72 +0,0 @@ -package convex.core.data; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Random; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import convex.core.crypto.HashTest; -import convex.test.Samples; - -@RunWith(Parameterized.class) -public class ParamTestBlobs { - private ABlob data; - - public ParamTestBlobs(String label, ABlob data) { - this.data = data; - } - - private static Random rand = new Random(1234); - - @Parameterized.Parameters(name = "{index}: {0}") - public static Collection dataExamples() { - return Arrays.asList(new Object[][] { { "Empty bytes", Blob.wrap(new byte[0]) }, - { "Short hex string CAFEBABE", Blob.fromHex("CAFEBABE") }, - { "Long random BlobTree", BlobTree.create(Blob.createRandom(rand, 10000)) }, - { "Length 2 strict sublist of byte data", Blob.create(new byte[] { 1, 2, 3, 4 }, 1, 2) }, - { "Bitcoin genesis header block", Blob.fromHex(HashTest.GENESIS_HEADER) }, - { "Max size embedded blob", Samples.MAX_EMBEDDED_BLOB }, - { "Full Blob of random data", Samples.FULL_BLOB }, { "Big blob", Samples.BIG_BLOB_TREE } }); - } - - @Test - public void testHexRoundTrip() { - String hex = data.toHexString(); - ABlob d2 = Blobs.fromHex(hex); - assertEquals(data, d2); - assertEquals(data.hashCode(), d2.hashCode()); - } - - @Test - public void testSlice() { - long n=data.count(); - ABlob full = data.slice(0, n); - assertEquals(data, full); - BlobsTest.doBlobTests(full); - - ABlob half = data.slice(n/2,n/2); - BlobsTest.doBlobTests(half); - } - - @Test - public void testCompare() { - long len = data.count(); - assertEquals(0, data.compareTo(data)); - assertEquals(0, data.compareTo(Blob.create(data.getBytes()))); - - assertTrue(data.compareTo(data.append(Samples.ONE_ZERO_BYTE_DATA)) < 0); - - if (len > 0) { - // anything should be "larger" than empty data - assertTrue(data.compareTo(Blob.EMPTY) > 0); - assertTrue(Blob.EMPTY.compareTo(data) < 0); - } - - } -} diff --git a/convex-core/src/test/java/convex/core/data/ParamTestOps.java b/convex-core/src/test/java/convex/core/data/ParamTestOps.java deleted file mode 100644 index 4f0e25b46..000000000 --- a/convex-core/src/test/java/convex/core/data/ParamTestOps.java +++ /dev/null @@ -1,79 +0,0 @@ -package convex.core.data; - -import static convex.test.Assertions.assertCVMEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.Arrays; -import java.util.Collection; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import convex.core.State; -import convex.core.data.prim.CVMBool; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.exceptions.ValidationException; -import convex.core.init.InitTest; -import convex.core.lang.AOp; -import convex.core.lang.Context; -import convex.core.lang.RT; -import convex.core.lang.TestState; -import convex.core.lang.ops.Cond; -import convex.core.lang.ops.Constant; -import convex.core.lang.ops.Def; -import convex.core.lang.ops.Do; -import convex.core.lang.ops.Invoke; -import convex.core.lang.ops.Lookup; - -@RunWith(Parameterized.class) -public class ParamTestOps { - private AOp op; - private Object expected; - - private static final State INITIAL_STATE = TestState.STATE; - - public ParamTestOps(String label, AOp v, Object expected) { - this.op = v; - this.expected = expected; - } - - @Parameterized.Parameters(name = "{index}: {0}") - public static Collection dataExamples() throws BadFormatException { - return Arrays - .asList(new Object[][] { - { "Constant", Constant.of(1L), RT.cvm(1L) }, - { "Lookup", Do.create(Def.create("foo", Constant.of(13)), - Lookup.create("foo")), RT.cvm(13) }, - { "Def", Def.create("foo", Constant.createString("bar")), Strings.create("bar") }, - { "Vector", Invoke.create("vector", Constant.createString("foo"), Constant.createString("bar")), - Vectors.of(Strings.create("foo"), Strings.create("bar")) }, - - { "Do", Do.create(Constant.createString("foo"), Constant.createString("bar")), Strings.create("bar") }, - { "Cond", - Cond.create(Constant.of(CVMBool.TRUE), Constant.createString("truthy"), - Constant.createString("falsey")), - Strings.create("truthy") }, - { "Def", Def.create("foo", Constant.of(1L)), 1L } }); - } - - @Test - public void testExpectedResult() { - long JUICE = 10000; - Context c = Context.createInitial(INITIAL_STATE, InitTest.HERO, JUICE); - Context c2 = c.execute(op); - - assertCVMEquals(expected, c2.getResult()); - } - - @Test - public void testCanonical() { - assertTrue(op.isCanonical()); - } - - @Test - public void testGeneric() throws InvalidDataException, ValidationException { - ObjectsTest.doAnyValueTests(op); - } -} diff --git a/convex-core/src/test/java/convex/core/data/ParamTestRefs.java b/convex-core/src/test/java/convex/core/data/ParamTestRefs.java deleted file mode 100644 index 106cc6d26..000000000 --- a/convex-core/src/test/java/convex/core/data/ParamTestRefs.java +++ /dev/null @@ -1,81 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.Arrays; -import java.util.Collection; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import convex.core.data.prim.CVMLong; -import convex.core.store.AStore; -import convex.core.store.MemoryStore; -import convex.core.store.Stores; -import etch.EtchStore; - -@RunWith(Parameterized.class) -public class ParamTestRefs { - private AStore store; - - public ParamTestRefs(String label,AStore store) { - this.store = store; - } - - @Parameterized.Parameters(name = "{index}: {0}") - public static Collection dataExamples() { - return Arrays - .asList(new Object[][] { - { "Memory Store", new MemoryStore() }, - { "Temp Etch Store", EtchStore.createTemp() } }); - } - - @Test - public void testStoreUsage() { - AStore temp=Stores.current(); - - try { - Stores.setCurrent(store); - - { // single embedded value - CVMLong n=CVMLong.create(1567565765677L); - Ref r=Ref.get(n); - assertTrue(r.isEmbedded()); - Ref r2=r.persist(); - assertTrue(r.isEmbedded()); - assertSame(n,r2.getValue()); - } - - - { // structure with embedded value - AVector v=Vectors.of(6759578996496L); - Ref> r=v.getRef(); - assertEquals(Ref.UNKNOWN,r.getStatus()); - Ref> r2=r.persist(); - assertEquals(Ref.PERSISTED,r2.getStatus()); - assertEquals(v.getRef(0),r2.getValue().getRef(0)); - } - - { // map with embedded structure - AMap> m=Maps.of(156746748L,Vectors.of(8797987L)); - Ref>> r=m.getRef(); - assertEquals(Ref.UNKNOWN,r.getStatus()); - - Ref>> r2=r.persist(); - - assertEquals(Ref.PERSISTED,r2.getStatus()); - MapEntry> me2=r2.getValue().entryAt(0); - assertTrue(me2.getRef(0).isEmbedded()); - assertEquals(Ref.PERSISTED,me2.getRef(1).getStatus()); - } - - } finally { - Stores.setCurrent(temp); - } - } - - -} diff --git a/convex-core/src/test/java/convex/core/data/ParamTestValues.java b/convex-core/src/test/java/convex/core/data/ParamTestValues.java deleted file mode 100644 index 7e65816f5..000000000 --- a/convex-core/src/test/java/convex/core/data/ParamTestValues.java +++ /dev/null @@ -1,69 +0,0 @@ -package convex.core.data; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.util.Arrays; -import java.util.Collection; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import convex.core.data.prim.CVMByte; -import convex.core.data.prim.CVMDouble; -import convex.core.data.prim.CVMLong; -import convex.core.data.type.AType; -import convex.core.data.type.Types; -import convex.core.exceptions.InvalidDataException; -import convex.core.exceptions.ValidationException; -import convex.test.Samples; - -@RunWith(Parameterized.class) -public class ParamTestValues { - private ACell data; - - public ParamTestValues(String label, ACell v) { - this.data = v; - } - - @Parameterized.Parameters(name = "{index}: {0}") - public static Collection dataExamples() { - return Arrays.asList(new Object[][] { - { "Keyword :foo", Samples.FOO }, - { "Empty Vector", Vectors.empty() }, - { "Long", CVMLong.ONE }, - { "Double", CVMDouble.ONE }, - { "Byte", CVMByte.ZERO }, - { "Single value map", Maps.of(7, 8) }, - { "Account status", AccountStatus.create(1000L,Samples.ACCOUNT_KEY) }, - { "Peer status", PeerStatus.create(Address.create(11), 1000L, Maps.create(Keywords.URL,Strings.create("http://www.google.com:18888"))) }, - { "Signed value", SignedData.create(Samples.KEY_PAIR, Strings.create("foo")) }, - { "Length 300 vector", Samples.INT_VECTOR_300 } }); - } - - @Test - public void testCanonical() { - assertTrue(data.isCanonical()); - } - - @Test - public void testType() { - AType t=data.getType(); - assertNotNull(t); - assertTrue(t.check(data)); - assertTrue(Types.ANY.check(data)); - } - - @Test - public void testHexRoundTrip() throws InvalidDataException, ValidationException { - ACell.createPersisted(data); - String hex = data.getEncoding().toHexString(); - Blob d2 = Blob.fromHex(hex); - ACell rec = Format.read(d2); - rec.validate(); - assertEquals(data, rec); - assertEquals(data.getEncoding(), rec.getEncoding()); - } -} diff --git a/convex-core/src/test/java/convex/core/data/ParamTestVector.java b/convex-core/src/test/java/convex/core/data/ParamTestVector.java deleted file mode 100644 index 754ce8e9b..000000000 --- a/convex-core/src/test/java/convex/core/data/ParamTestVector.java +++ /dev/null @@ -1,77 +0,0 @@ -package convex.core.data; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Collection; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import convex.core.exceptions.BadFormatException; -import convex.test.Samples; - -/** - * Parameterised test class for a bunch of vectors. - * - */ -@RunWith(Parameterized.class) -public class ParamTestVector { - private AVector v; - - public ParamTestVector(String label, AVector v) { - this.v = v; - } - - @Parameterized.Parameters(name = "{index}: {0}") - public static Collection dataExamples() { - return Arrays - .asList(new Object[][] { { "Empty Vector", Vectors.empty() }, { "Single value vector", Vectors.of(7L) }, - { "MapEntry vector", MapEntry.of(1L, 2L) }, { "Nested vector", Vectors.of(Vectors.empty()) }, - { "Vector with Account status", Vectors.of(AccountStatus.create(1000L,Samples.ACCOUNT_KEY)) }, - { "Vector with Peer status", Vectors.of(PeerStatus.create(Address.create(11), 1000L)) }, - { "Length 10 vector", Samples.INT_VECTOR_10 }, { "Length 16 vector", Samples.INT_VECTOR_16 }, - { "Length 23 vector", Samples.INT_VECTOR_23 }, { "Length 32 vector", Samples.INT_VECTOR_32 }, - { "Length 300 vector", Samples.INT_VECTOR_300 }, - { "Length 256 tree vector", Samples.INT_VECTOR_256 } }); - } - - @Test - public void testGenericProperties() { - VectorsTest.doVectorTests(v); - } - - @Test - public void testCanonical() { - assertTrue(v.toCanonical().isCanonical()); - } - - @Test - public void testElements() { - int n = v.size(); - for (int i = 0; i < n; i++) { - Object o = v.get(i); - assertEquals(o, v.slice(i, 1).get(0)); - } - - assertThrows(Throwable.class, () -> v.get(-1)); - assertThrows(Throwable.class, () -> v.get(n)); - } - - @Test - public void testBuffer() throws BadFormatException { - int size = v.size(); - ByteBuffer b = ByteBuffer.allocate(3000); - v.write(b); - b.flip(); - AVector rec = Format.read(b); - assertEquals(0, b.remaining()); - assertEquals(size, rec.size()); - assertEquals(v, rec); - } - -} diff --git a/convex-core/src/test/java/convex/core/data/RecordTest.java b/convex-core/src/test/java/convex/core/data/RecordTest.java deleted file mode 100644 index af86fa9fa..000000000 --- a/convex-core/src/test/java/convex/core/data/RecordTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; - -import convex.core.Belief; -import convex.core.Result; -import convex.core.data.prim.CVMLong; -import convex.core.init.InitTest; -import convex.core.lang.TestState; -import convex.core.lang.impl.RecordFormat; - -public class RecordTest { - - @Test - public void testBelief() { - Belief b=Belief.createSingleOrder(InitTest.FIRST_PEER_KEYPAIR); - assertEquals(b.getRefCount(),b.getOrders().getRefCount()); - - doRecordTests(b); - - } - - public static void doRecordTests(ARecord r) { - - RecordFormat format=r.getFormat(); - - AVector keys=format.getKeys(); - int n=(int) keys.count(); - - AVector vals=r.values(); - assertEquals(n,vals.size()); - - ACell[] vs=new ACell[n]; // new array to extract values - for (int i=0; i me0=r.getEntry(k); - assertEquals(k,me0.getKey()); - assertEquals(v,me0.getValue()); - - // TODO: consider this invariant? - assertEquals(r.toHashMap(),r.assoc(k, v)); - - // indexed access - assertEquals(v,vals.get(i)); - - // indexed entry-wise access - MapEntry me=r.entryAt(i); - assertEquals(k,me.getKey()); - assertEquals(v,me.getValue()); - } - assertThrows(IndexOutOfBoundsException.class,()->r.entryAt(n)); - assertThrows(IndexOutOfBoundsException.class,()->r.entryAt(-1)); - - int rc=r.getRefCount(); - for (int i=0; ir.getRef(rc)); - - assertSame(r,r.updateAll(r.getValuesArray())); - assertSame(r,r.updateAll(r.values().toCellArray())); - - CollectionsTest.doDataStructureTests(r); - } - - @Test - public void testResult() { - String s="{:id 4,:result #44,:error-code nil,:trace nil}"; - AHashMap m=TestState.eval(s); - assertEquals(4,m.count); - assertEquals("0xa8a8f308df3cb0eab838b64a41c2534ad0a07f126ee1291f686182aa32862ae6",m.getHash().toString()); - - Result r=Result.create(CVMLong.create(4), Address.create(44), null, null); - assertEquals(s,r.toString()); - assertEquals("0x63e45711e949f3e9c026df0ba8d0896e17683955e0059423fa0f1238acdfacd8",r.getHash().toString()); - - assertEquals(m,r.toHashMap()); - - doRecordTests(r); - } -} diff --git a/convex-core/src/test/java/convex/core/data/RefTest.java b/convex-core/src/test/java/convex/core/data/RefTest.java deleted file mode 100644 index daa5ac0cc..000000000 --- a/convex-core/src/test/java/convex/core/data/RefTest.java +++ /dev/null @@ -1,182 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.Random; -import java.util.Set; - -import org.junit.jupiter.api.Test; - -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.InvalidDataException; -import convex.core.exceptions.MissingDataException; -import convex.core.lang.RT; -import convex.core.lang.Symbols; -import convex.core.util.Utils; -import convex.test.Samples; - -public class RefTest { - @Test - public void testMissingData() { - // create a Ref using just a bad hash - Ref ref = Ref.forHash(Samples.BAD_HASH); - - // equals comparison should work - assertEquals(ref, Ref.forHash(Samples.BAD_HASH)); - - // gneric properties of missing Ref - assertEquals(Samples.BAD_HASH, ref.getHash()); - assertEquals(Ref.UNKNOWN, ref.getStatus()); // shouldn't know anything about this Ref yet - assertFalse(ref.isDirect()); - - // we expect a failure here - assertThrows(MissingDataException.class, () -> ref.getValue()); - } - - @Test - public void testRefSet() { - // 10 element refs - assertEquals(10, Ref.accumulateRefSet(Samples.INT_VECTOR_10).size()); - assertEquals(10, Utils.totalRefCount(Samples.INT_VECTOR_10)); - - // 256 element refs, 16 tree branches - assertEquals(272, Ref.accumulateRefSet(Samples.INT_VECTOR_256).size()); - assertEquals(272, Utils.totalRefCount(Samples.INT_VECTOR_256)); - - // 11 = 10 element refs plus one for enclosing ref - assertEquals(11, Ref.accumulateRefSet(Samples.INT_VECTOR_10.getRef()).size()); - } - - @Test - public void testShallowPersist() { - Blob bb = Blob.createRandom(new Random(), 100); // unique blob but embedded - assertTrue(bb.isEmbedded()); - - AVector v = Vectors.of(bb,bb,bb,bb); // vector containing big blob four times. Shouldn't be embedded. - assertFalse(v.isEmbedded()); - - Hash bh = bb.getHash(); - Hash vh = v.getHash(); - - Ref> ref = v.getRef().persistShallow(); - assertEquals(Ref.STORED, ref.getStatus()); - - assertThrows(MissingDataException.class, () -> Ref.forHash(bh).getValue()); - - assertFalse(v.isEmbedded()); - assertEquals(v, Ref.forHash(vh).getValue()); - } - - @Test - public void testEmbedded() { - assertTrue(Ref.get(RT.cvm(1L)).isEmbedded()); // a primitive - assertTrue(Ref.NULL_VALUE.isEmbedded()); // singleton null ref - assertTrue(List.EMPTY_REF.isEmbedded()); // singleton null ref - assertFalse(Blob.create(new byte[Format.MAX_EMBEDDED_LENGTH]).getRef().isEmbedded()); // too big to embed - assertTrue(Samples.LONG_MAP_10.getRef().isEmbedded()); // a ref container - } - - @Test - public void testPersistEmbeddedNull() throws InvalidDataException { - Ref nr = Ref.get(null); - assertSame(Ref.NULL_VALUE, nr); - assertSame(nr, nr.persist()); - nr.validate(); - assertTrue(nr.isEmbedded()); - } - - @Test - public void testPersistEmbeddedLong() { - ACell val=RT.cvm(10001L); - Ref nr = Ref.get(val); - assertSame(nr.getValue(), nr.persist().getValue()); - assertTrue(nr.isEmbedded()); - } - - @Test - public void testGoodData() { - AVector value = Vectors.of(Keywords.FOO, Symbols.FOO); - // a good ref - Ref orig = value.getRef(); - assertEquals(Ref.UNKNOWN, orig.getStatus()); - assertFalse(orig.isPersisted()); - orig = orig.persist(); - assertTrue(orig.isPersisted()); - - // a ref using the same hash - if (!(value.isEmbedded())) { - Ref ref = Ref.forHash(orig.getHash()); - assertEquals(orig, ref); - assertEquals(value, ref.getValue()); - } - } - - @Test - public void testCompare() { - assertEquals(0, Ref.get(RT.cvm(1L)).compareTo(ACell.createPersisted(RT.cvm(1L)))); - assertEquals(1, Ref.get(RT.cvm(1L)).compareTo( - Ref.forHash(Hash.fromHex("0000000000000000000000000000000000000000000000000000000000000000")))); - assertEquals(-1, Ref.get(RT.cvm(1L)).compareTo( - Ref.forHash(Hash.fromHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")))); - } - - @Test - public void testVectorRefCounts() { - AVector v=Vectors.of(1,2,3); - assertEquals(3,v.getRefCount()); - - AVector zv=Vectors.repeat(CVMLong.create(0), 16); - assertEquals(16,zv.getRefCount()); - - // 3 tail elements after prefix ref - AVector zvv=zv.concat(v); - assertEquals(4,zvv.getRefCount()); - - } - - @Test - public void testToString() { - AVector v=Vectors.of(1,2,3,4); - Ref> ref=v.getRef(); - assertNotNull(ref.toString()); - } - - @Test - public void testMissing() { - Hash bad=Hash.fromHex("0000000000000000000000000000000000000000000000000000000000000000"); - Ref ref=Ref.forHash(bad); - assertTrue(ref.isMissing()); - - } - - @Test - public void testDiabolicalDeep() { - Ref a = Samples.DIABOLICAL_MAP_2_10000.getRef(); - // TODO: consider if this should be possible, currently not (stack overflow) - // Ref.accumulateRefSet(a); - assertTrue(a.isEmbedded()); - } - - @Test - public void testDiabolicalWide() { - Ref a = Samples.DIABOLICAL_MAP_30_30.getRef(); - // OK since we manage de-duplication - Set> set = Ref.accumulateRefSet(a); - assertEquals(31 + 30 * 16, set.size()); // 16 refs at each level after de-duping - assertFalse(a.isEmbedded()); - } - - @Test - public void testNullRef() { - Ref nullRef = Ref.get(null); - assertNotNull(nullRef); - assertSame(nullRef.getHash(), Hash.NULL_HASH); - assertTrue(nullRef.isEmbedded()); - assertFalse(nullRef.isMissing()); - } -} diff --git a/convex-core/src/test/java/convex/core/data/SetsTest.java b/convex-core/src/test/java/convex/core/data/SetsTest.java deleted file mode 100644 index ffe935bff..000000000 --- a/convex-core/src/test/java/convex/core/data/SetsTest.java +++ /dev/null @@ -1,209 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -import convex.core.data.prim.CVMByte; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.RT; -import convex.test.Samples; - -public class SetsTest { - - @Test - public void testEmptySet() { - ASet e = Sets.empty(); - assertEquals(0, e.size()); - assertFalse(e.contains(null)); - } - - @Test - public void testIncludeExclude() { - ASet s = Sets.empty(); - assertEquals("#{}", s.toString()); - s = s.include(RT.cvm(1L)); - assertEquals("#{1}", s.toString()); - s = s.include(RT.cvm(1L)); - assertEquals("#{1}", s.toString()); - s = s.include(RT.cvm(2L)); - assertEquals("#{1,2}", s.toString()); - s = s.exclude(RT.cvm(1L)); - assertEquals("#{2}", s.toString()); - s = s.exclude(RT.cvm(2L)); - assertTrue(s.isEmpty()); - assertSame(s, Sets.empty()); - } - - @Test - public void testPrimitiveEquality() { - // different primitive objects with same numeric value should not collide in set - CVMByte b=CVMByte.create(1); - ASet s=Sets.of(1L).include(b); - assertEquals(2L,s.count()); - - assertEquals(Sets.of(b, 1L), s); - } - - @Test - public void testSetToArray() { - assertEquals(3, Sets.of(1, 2, 3).toArray().length); - assertEquals(0, Sets.empty().toArray().length); - } - - @Test - public void testContainsAll() { - assertTrue(Sets.of(1, 2, 3).containsAll(Sets.of(2, 3))); - assertFalse(Sets.of(1, 2).containsAll(Sets.of(2, 3, 4))); - } - - @Test - public void testSubsets() { - ASet EM=Sets.empty(); - assertTrue(EM.isSubset(EM)); - assertTrue(EM.isSubset(Samples.INT_SET_300)); - assertTrue(EM.isSubset(Samples.INT_SET_10)); - assertFalse(Samples.INT_SET_10.isSubset(EM)); - assertFalse(Samples.INT_SET_300.isSubset(EM)); - - { - ASet s=Samples.createRandomSubset(Samples.INT_SET_300,0.5,1); - assertTrue(s.isSubset(Samples.INT_SET_300)); - } - { - ASet s=Samples.createRandomSubset(Samples.INT_SET_10,0.5,2); - assertTrue(s.isSubset(Samples.INT_SET_10)); - } - - assertTrue(Samples.INT_SET_300.isSubset(Samples.INT_SET_300)); - assertTrue(Samples.INT_SET_10.isSubset(Samples.INT_SET_300)); - assertTrue(Samples.INT_SET_10.isSubset(Samples.INT_SET_10)); - assertFalse(Samples.INT_SET_300.isSubset(Samples.INT_SET_10)); - } - - @Test - public void testMerging() { - ASet a = Sets.of(1, 2, 3); - ASet b = Sets.of(2, 4, 6); - assertTrue(a.contains(RT.cvm(3L))); - assertFalse(b.contains(RT.cvm(3L))); - - assertSame(Sets.empty(), a.disjAll(a)); - assertEquals(Sets.of(1, 2, 3, 4, 6), a.conjAll(b)); - assertEquals(Sets.of(1, 3), a.disjAll(b)); - } - - @Test - public void regressionRead() throws BadFormatException { - ASet v1=Sets.of(43); - Blob b1 = Format.encodedBlob(v1); - - ASet v2=Format.read(b1); - Blob b2 = Format.encodedBlob(v2); - - assertEquals(v1, v2); - assertEquals(b1,b2); - } - - @Test - public void regressionNils() throws InvalidDataException { - AMap m = Maps.of(null, null); - assertEquals(1, m.size()); - assertTrue(m.containsKey(null)); - - ASet s = Sets.of(m); - s.validate(); - s = s.include( m); - s.validate(); - } - - - - @Test - public void testMergingIdentity() { - ASet a = Sets.of(1L, 2L, 3L); - assertSame(a, a.include(RT.cvm(2L))); - assertSame(a, a.includeAll(Sets.of(1L, 3L))); - } - - @Test - public void testIntersection() { - ASet a = Sets.of(1, 2, 3); - - // (intersect a a) => a - assertSame(a,a.intersectAll(a)); - - // (intersect a #{}) => #{} - assertSame(Sets.empty(),a.intersectAll(Sets.of(5,6))); - - // (intersect a b) => a if (subset? a b) - assertEquals(a,a.intersectAll(Samples.INT_SET_10)); - assertEquals(a,a.intersectAll(Samples.INT_SET_300)); - - // regular intersection - assertEquals(Sets.of(2,3),a.intersectAll(Sets.of(2,3,4))); - - assertThrows(Throwable.class,()->a.intersectAll(null)); - } - - @Test - public void testBigMerging() { - ASet s = Sets.create(Samples.INT_VECTOR_300); - CollectionsTest.doSetTests(s); - - ASet s2 = s.includeAll(Sets.of(1, 2, 3, 100)); - assertEquals(s, s2); - assertSame(s, s2); - - ASet s3 = s.disjAll(Samples.INT_VECTOR_300); - assertSame(s3, Sets.empty()); - - ASet s4 = s.excludeAll(Sets.of(-1000)); - assertSame(s, s4); - - ASet s5a = Sets.of(1, 3, 7, -1000); - ASet s5 = s5a.disjAll(s); - assertEquals(Sets.of(-1000), s5); - } - - @Test - public void testIncrementalBuilding() { - ASet set=Sets.empty(); - for (int i=0; i<320; i++) { - assertEquals(i,set.size()); - - // extend set with one new element - CVMLong v=CVMLong.create(i); - ASet newSet=set.conj(v); - - // new Set contains previous set - assertTrue(newSet.containsAll(set)); - - assertNotEquals(set,newSet); - assertTrue(newSet.contains(v)); - assertFalse(set.contains(v)); - - // removing element should get back to original set - assertEquals(set,newSet.exclude(v)); - - // removing original set should leave one element - assertEquals(Sets.of(v),newSet.excludeAll(set)); - - set=newSet; - } - - doSetTests(set); - } - - public static void doSetTests(ASet a) { - - CollectionsTest.doSetTests(a); - } -} diff --git a/convex-core/src/test/java/convex/core/data/SignedDataTest.java b/convex-core/src/test/java/convex/core/data/SignedDataTest.java deleted file mode 100644 index 678286bd6..000000000 --- a/convex-core/src/test/java/convex/core/data/SignedDataTest.java +++ /dev/null @@ -1,110 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -import convex.core.crypto.AKeyPair; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadSignatureException; -import convex.core.init.InitTest; -import convex.core.lang.RT; -import convex.test.Samples; - -public class SignedDataTest { - @SuppressWarnings("unchecked") - @Test - public void testBadSignature() { - Ref dref = Ref.get(RT.cvm(13L)); - SignedData sd = SignedData.create(Samples.BAD_ACCOUNTKEY, Samples.BAD_SIGNATURE, dref); - - // should not yet be checked - assertFalse(sd.isSignatureChecked()); - - // Signature check should fail since bad signature - assertFalse(sd.checkSignature()); - - // should now be checked - assertTrue(sd.isSignatureChecked()); - - assertTrue((sd.getRef().getFlags()&Ref.BAD_MASK)!=0); - assertEquals(13L, sd.getValue().longValue()); - assertSame(Samples.BAD_ACCOUNTKEY, sd.getAccountKey()); - assertNotNull(sd.toString()); - - assertThrows(BadSignatureException.class, () -> sd.validateSignature()); - - ACell.createPersisted(sd); - - SignedData sd1 = (SignedData) Ref.forHash(sd.getHash()).getValue(); - // should have cached checked signature - assertTrue(sd1.isSignatureChecked()); - assertFalse(sd1.checkSignature()); - - ObjectsTest.doAnyValueTests(sd); - ObjectsTest.doAnyValueTests(sd1); - } - - @Test - public void testEmbeddedSignature() throws BadSignatureException { - CVMLong cl=RT.cvm(158587); - - AKeyPair kp = InitTest.HERO_KEYPAIR; - SignedData sd = kp.signData(cl); - - // should be checked by default - assertTrue(sd.isSignatureChecked()); - - assertTrue(sd.checkSignature()); - - sd.validateSignature(); - assertEquals(cl, sd.getValue()); - - assertTrue(sd.getDataRef().isEmbedded()); - } - - @SuppressWarnings("unchecked") - @Test - public void testSignatureCache() { - CVMLong cl=RT.cvm(1585856457); - AKeyPair kp = InitTest.HERO_KEYPAIR; - SignedData sd = kp.signData(cl); - ACell.createPersisted(sd); - - SignedData sd1 = (SignedData) Ref.forHash(sd.getHash()).getValue(); - // should have cached checked signature - assertTrue(sd1.isSignatureChecked()); - assertTrue(sd1.checkSignature()); - } - - @Test - public void testNullValueSignings() throws BadSignatureException { - SignedData sd = SignedData.create(InitTest.HERO_KEYPAIR, null); - assertNull(sd.getValue()); - assertTrue(sd.checkSignature()); - } - - @Test - public void testDataStructureSignature() throws BadSignatureException { - AKeyPair kp = InitTest.HERO_KEYPAIR; - AVector v = Vectors.of(1L, 2L, 3L); - SignedData> sd = kp.signData(v); - - assertEquals(1,sd.getRefCount()); - - assertTrue(sd.checkSignature()); - - sd.validateSignature(); - assertEquals(v, sd.getValue()); - - assertEquals(kp.getAccountKey(),sd.getAccountKey()); - - ObjectsTest.doAnyValueTests(sd); - } -} diff --git a/convex-core/src/test/java/convex/core/data/StreamsTest.java b/convex-core/src/test/java/convex/core/data/StreamsTest.java deleted file mode 100644 index e6d074490..000000000 --- a/convex-core/src/test/java/convex/core/data/StreamsTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.junit.jupiter.api.Test; - -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.InvalidDataException; -import convex.core.exceptions.ValidationException; -import convex.test.Samples; - -public class StreamsTest { - - @Test - public void testIntStream() throws InvalidDataException, ValidationException { - AVector v = Samples.INT_VECTOR_300; - Stream s = v.stream(); - - List list = s.map(i -> i).collect(Collectors.toList()); - assertEquals(v.size(), list.size()); - - AVector v2 = Vectors.create(list); - v2.validate(); - assertEquals(v.getClass(), v2.getClass()); - assertEquals(v, v2); - - } - -} diff --git a/convex-core/src/test/java/convex/core/data/StringsTest.java b/convex-core/src/test/java/convex/core/data/StringsTest.java deleted file mode 100644 index 12951b4c6..000000000 --- a/convex-core/src/test/java/convex/core/data/StringsTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; - -public class StringsTest { - - - @Test public void testStringShort() { - String t="Test"; - StringShort ss=StringShort.create(t); - - assertEquals(t.length(),ss.toString().length()); - assertEquals(t,ss.toString()); - } - - @Test public void testTreeShift() { - assertEquals(10,StringTree.calcShift(1025)); - assertEquals(10,StringTree.calcShift(4096)); - assertEquals(10,StringTree.calcShift(16384)); - assertEquals(14,StringTree.calcShift(16385)); - assertEquals(14,StringTree.calcShift(99999)); - assertEquals(14,StringTree.calcShift(262144)); - assertEquals(18,StringTree.calcShift(262145)); - - assertThrows(IllegalArgumentException.class,()->StringTree.calcShift(0)); - assertThrows(IllegalArgumentException.class,()->StringTree.calcShift(1024)); - } -} diff --git a/convex-core/src/test/java/convex/core/data/SymbolTest.java b/convex-core/src/test/java/convex/core/data/SymbolTest.java deleted file mode 100644 index 6afb655de..000000000 --- a/convex-core/src/test/java/convex/core/data/SymbolTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -import convex.core.Constants; -import convex.core.exceptions.BadFormatException; -import convex.core.lang.Symbols; -import convex.core.util.Text; - -public class SymbolTest { - - @Test - public void testBadSymbols() { - assertNotNull(Symbol.create(Text.whiteSpace(Constants.MAX_NAME_LENGTH))); - - assertNull(Symbol.create(Text.whiteSpace(Constants.MAX_NAME_LENGTH+1))); - assertNull(Symbol.create("")); - assertNull(Symbol.create((String)null)); - } - - @Test - public void testEmbedded() { - // max length Symbol should be embedded - Symbol s=Symbol.create(Text.whiteSpace(Constants.MAX_NAME_LENGTH)); - assertTrue(s.isEmbedded()); - } - - @Test - public void testToString() { - assertEquals("foo",Symbols.FOO.toString()); - } - - @Test - public void testBadFormat() { - // should fail because this is an empty String - assertThrows(BadFormatException.class, () -> Symbol.read(Blob.fromHex("00").toByteBuffer())); - } - - @Test - public void testNormalSymbol() { - Symbol k = Symbol.create("count"); - assertEquals(Symbols.COUNT, k); - - assertEquals("count", k.getName().toString()); - assertEquals("count", k.toString()); - assertEquals(7, k.getEncoding().length); // tag(1) + length(1) + name(5) - - } -} diff --git a/convex-core/src/test/java/convex/core/data/SyntaxTest.java b/convex-core/src/test/java/convex/core/data/SyntaxTest.java deleted file mode 100644 index 4bd27c0d1..000000000 --- a/convex-core/src/test/java/convex/core/data/SyntaxTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; - -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.RT; - -public class SyntaxTest { - - @Test public void testSyntaxEncodingEmptyMetaData() throws BadFormatException { - // check that empty metadata gets encoded as nil for efficiency. - Syntax emptyMeta=Syntax.create(RT.cvm(1L)); - Blob encoded = emptyMeta.getEncoding(); - assertEquals("88090100",encoded.toHexString()); - Syntax recovered = Format.read(encoded); - assertEquals(emptyMeta,recovered); - - // should be invalid to have an empty map as encoded metadata - assertThrows(BadFormatException.class,()->Format.read("880901820000")); - assertThrows(BadFormatException.class,()->Format.read("8800820000")); - } - - /** - * A Syntax wrapped in another Syntax should not be a valid encoding - * @throws BadFormatException - */ - @Test public void testNoDoubleWrapping() throws BadFormatException { - // A valid Syntax Object - Syntax inner=Syntax.create(null); - Syntax badSyntax=Syntax.createUnchecked(inner, Maps.empty()); - assertEquals(inner,badSyntax.getValue()); - assertSame(Maps.empty(),badSyntax.getMeta()); - - // Should fail validation - assertThrows(InvalidDataException.class,()->badSyntax.validate()); - } - - @Test public void testSyntaxMergingMetaData() { - // default to empty metadata - Syntax s1=Syntax.create(RT.cvm(1L)); - assertSame(Maps.empty(),s1.getMeta()); - - // Should wrap once only and merge metadata - Syntax s2=Syntax.create(s1,Maps.of(1,2,3,4)); - assertEquals(RT.cvm(1L),(CVMLong)s2.getValue()); - - // Should wrap once only and merge new metadata, overwriting original value - Syntax s3=Syntax.create(s2,Maps.of(3,7)); - assertEquals(RT.cvm(1L),(CVMLong)s3.getValue()); - assertEquals(Maps.of(1,2,3,7),s3.getMeta()); - - } -} diff --git a/convex-core/src/test/java/convex/core/data/TreeVectorTest.java b/convex-core/src/test/java/convex/core/data/TreeVectorTest.java deleted file mode 100644 index 1132a5793..000000000 --- a/convex-core/src/test/java/convex/core/data/TreeVectorTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package convex.core.data; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.NoSuchElementException; -import java.util.Spliterator; - -import org.junit.jupiter.api.Test; - -import convex.core.data.prim.CVMLong; -import convex.core.lang.RT; -import convex.test.Samples; - -public class TreeVectorTest { - - @Test - public void testComputeShift() { - assertEquals(0, VectorTree.computeShift(0)); - assertEquals(0, VectorTree.computeShift(1)); - assertEquals(0, VectorTree.computeShift(16)); - - assertEquals(4, VectorTree.computeShift(17)); // overflow to next shift level - assertEquals(4, VectorTree.computeShift(32)); - assertEquals(4, VectorTree.computeShift(107)); - assertEquals(4, VectorTree.computeShift(256)); - - assertEquals(8, VectorTree.computeShift(257)); // overflow to next shift level - - assertEquals(8, VectorTree.computeShift(4096)); - assertEquals(12, VectorTree.computeShift(4097)); // overflow to next shift level - } - - @Test - public void testArraySize() { - assertEquals(2, VectorTree.computeArraySize(32)); // two tree chunks - assertEquals(3, VectorTree.computeArraySize(33)); // needs 3 chunks - assertEquals(16, VectorTree.computeArraySize(256)); // full 16 chunks - assertEquals(2, VectorTree.computeArraySize(257)); // needs 17 chunks, two children at next level - - assertEquals(16, VectorTree.computeArraySize(4096)); // full 16 children - assertEquals(2, VectorTree.computeArraySize(4097)); // two children at next level - - assertTrue(16 >= VectorTree.computeArraySize(967827895416073414L)); - } - - @Test - public void testIterator() { - AVector v = Samples.INT_VECTOR_256; - assertEquals(v.get(0), v.iterator().next()); - assertEquals(v.get(255), v.listIterator(256).previous()); - - assertThrows(NoSuchElementException.class, () -> v.listIterator().previous()); - assertThrows(NoSuchElementException.class, () -> v.listIterator(256).next()); - } - - @Test - public void testMap() { - AVector orig = Samples.INT_VECTOR_300; - AVector inc = orig.map(i -> RT.cvm(i.longValue() + 5)); - assertEquals(orig.count(), inc.count()); - assertNotEquals(orig, inc); - AVector dec = inc.map(i -> RT.cvm(i.longValue() - 5)); - assertEquals(orig, dec); - } - - @Test - public void testSpliterator() { - AVector a = Samples.INT_VECTOR_300.subVector(0, 256); - assertEquals(VectorTree.class, a.getClass()); - Spliterator spliterator = a.spliterator(); - assertEquals(256, spliterator.estimateSize()); - - long[] sum = new long[1]; - spliterator.forEachRemaining(i -> sum[0] += i.longValue()); - assertEquals((255 * 256) / 2, sum[0]); - - } -} diff --git a/convex-core/src/test/java/convex/core/data/VectorsTest.java b/convex-core/src/test/java/convex/core/data/VectorsTest.java deleted file mode 100644 index e58792857..000000000 --- a/convex-core/src/test/java/convex/core/data/VectorsTest.java +++ /dev/null @@ -1,337 +0,0 @@ -package convex.core.data; - -import static convex.test.Assertions.assertCVMEquals; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.ListIterator; -import java.util.Spliterator; -import java.util.concurrent.atomic.AtomicLong; - -import org.junit.jupiter.api.Test; - -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.lang.RT; -import convex.test.Samples; - -/** - * Example based tests for vectors. - * - * Also doVectorTests(...) implements generic tests for any vector. - */ -public class VectorsTest { - - @Test - public void testEmptyVector() { - AVector lv = Vectors.empty(); - AArrayBlob d = lv.getEncoding(); - assertArrayEquals(new byte[] { Tag.VECTOR, 0 }, d.getBytes()); - - assertSame(lv,Vectors.empty()); - } - - @Test - public void testSubVectors() { - AVector v = Samples.INT_VECTOR_300; - - AVector v1 = v.subVector(10, Vectors.CHUNK_SIZE); - assertEquals(VectorLeaf.class, v1.getClass()); - assertEquals(RT.cvm(10), v1.get(0)); - - AVector v2 = v.subVector(10, Vectors.CHUNK_SIZE * 2); - assertEquals(VectorTree.class, v2.getClass()); - assertEquals(RT.cvm(10), v2.get(0)); - - AVector v3 = v.subVector(10, Vectors.CHUNK_SIZE * 2 - 1); - assertEquals(VectorLeaf.class, v3.getClass()); - assertEquals(RT.cvm(10), v3.get(0)); - - AVector v4 = v3.conj(RT.cvm(1000L)); - assertEquals(VectorTree.class, v4.getClass()); - assertEquals(RT.cvm(26), v4.get(16)); - assertEquals(v1, v4.subVector(0, Vectors.CHUNK_SIZE)); - } - - @Test - public void testCreateSpecialCases() { - assertSame(Vectors.empty(), VectorLeaf.create(new ACell[0])); - assertSame(Vectors.empty(), VectorLeaf.create(new ACell[10], 3, 0)); - } - - @Test - public void testChunks() { - assertEquals(Samples.INT_VECTOR_16, Samples.INT_VECTOR_300.getChunk(0)); - AVector v = Samples.INT_VECTOR_300.getChunk(0); - assertEquals(VectorTree.class, v.getChunk(0).concat(v).getClass()); - } - - @Test - public void testChunkConcat() { - VectorLeaf v = Samples.INT_VECTOR_300.getChunk(16); - AVector vv = v.concat(v); - assertEquals(VectorTree.class, vv.getClass()); - assertEquals(v, vv.getChunk(16)); - - assertSame(Samples.INT_VECTOR_16, Samples.INT_VECTOR_16.empty().appendChunk(Samples.INT_VECTOR_16)); - - assertThrows(IndexOutOfBoundsException.class, () -> vv.getChunk(3)); - - // can't append chunk unless initial size is correct - assertThrows(IllegalArgumentException.class, () -> Samples.INT_VECTOR_10.appendChunk(Samples.INT_VECTOR_16)); - assertThrows(IllegalArgumentException.class, () -> Samples.INT_VECTOR_300.appendChunk(Samples.INT_VECTOR_16)); - - - VectorLeaf tooSmall=VectorLeaf.create(new CVMLong[] {CVMLong.create(1),CVMLong.create(2)}); - // can't append wrong chunk size - assertThrows(IllegalArgumentException.class, - () -> Samples.INT_VECTOR_16.appendChunk(tooSmall)); - - } - - @Test - public void testIndexOf() { - AVector v = Samples.INT_VECTOR_300; - CVMLong last=v.get(v.count()-1); - assertEquals(299, v.indexOf(last)); - assertEquals(299L, v.longIndexOf(last)); - assertEquals(299, v.lastIndexOf(last)); - assertEquals(299L, v.longLastIndexOf(last)); - - CVMLong mid=v.get(29); - assertEquals(29, v.indexOf(mid)); - assertEquals(29L, v.longIndexOf(mid)); - assertEquals(29, v.lastIndexOf(mid)); - assertEquals(29L, v.longLastIndexOf(mid)); - - } - - @Test - public void testAppending() { - int SIZE = 300; - @SuppressWarnings("unchecked") - AVector lv = (VectorLeaf) VectorLeaf.EMPTY; - - for (int i = 0; i < SIZE; i++) { - CVMLong ci=RT.cvm(i); - lv = lv.append(ci); - assertEquals(i + 1L, lv.count()); - assertEquals(ci, lv.get(i)); - } - assertEquals(300L, lv.count()); - } - - @Test - public void testBigMatch() { - AVector v = Samples.INT_VECTOR_300; - assertTrue(v.anyMatch(i -> i.longValue() == 3)); - assertTrue(v.anyMatch(i -> i.longValue() == 299)); - assertFalse(v.anyMatch(i -> i.longValue() == -1)); - - assertFalse(v.allMatch(i -> i.longValue() == 3)); - assertTrue(v.allMatch(i -> i.longValue() >=0)); - } - - @Test - public void testAnyMatch() { - AVector v = Vectors.of(1, 2, 3, 4); - assertTrue(v.anyMatch(i -> i.longValue() == 3)); - assertFalse(v.anyMatch(i -> i.longValue() == 5)); - } - - @Test - public void testAllMatch() { - AVector v = Vectors.of(1, 2, 3, 4); - assertTrue(v.allMatch(i -> i instanceof CVMLong)); - assertFalse(v.allMatch(i -> i.longValue() < 3)); - } - - @Test - public void testMap() { - AVector v = Vectors.of(1, 2, 3, 4); - AVector exp = Vectors.of(2, 3, 4, 5); - assertEquals(exp, v.map(i -> CVMLong.create(i.longValue() + 1))); - } - - @Test - public void testSmallAssoc() { - AVector v = Vectors.of(1, 2, 3, 4); - AVector nv = v.assoc(2, RT.cvm(10L)); - assertEquals(Vectors.of(1, 2, 10, 4), nv); - } - - @Test - public void testBigAssoc() { - AVector v = Samples.INT_VECTOR_300; - AVector nv = v.assoc(100, RT.cvm(17L)); - assertEquals(RT.cvm(17L), nv.get(100)); - } - - @Test - public void testReduce() { - AVector vec = Vectors.of(1, 2, 3, 4); - assertEquals(110, (long) vec.reduce((s, v) -> s + v.longValue(), 100L)); - } - - @Test - public void testMapEntry() { - AVector v1 = Vectors.of(1L, 2L); - - MapEntry me=MapEntry.of(1L,2L); - assertEquals(v1, me); - assertEquals(v1, me.toVector()); - - assertEquals(me,me.toVector()); - assertFalse(me.isCanonical()); - - doVectorTests(me); - } - - @Test - public void testLastIndex() { - // regression test - AVector v = Samples.INT_VECTOR_300.concat(Vectors.of(1, null, 3, null)); - assertEquals(303L, v.longLastIndexOf(null)); - } - - @Test - public void testReduceBig() { - AVector vec = Samples.INT_VECTOR_300; - assertEquals(100 + (299 * 300) / 2, vec.reduce((s, v) -> s + v.longValue(), 100L)); - } - - // TODO: more sensible tests on embedded vector sizes - @Test - public void testEmbedding() { - // should embed, little values - AVector vec = Vectors.of(1, 2, 3, 4); - assertTrue(vec.isEmbedded()); - assertEquals(10L,vec.getEncoding().count()); - - // should embed, small enough - AVector vec2=Vectors.of(vec,vec); - assertTrue(vec2.isEmbedded()); - assertEquals(22L,vec2.getEncoding().count()); - - AVector vec3=Vectors.of(vec2,vec2,vec2,vec2,vec2,vec2,vec2,vec2); - assertFalse(vec3.isEmbedded()); - } - - @Test - public void testUpdateRefs() { - AVector vec = Vectors.of(1, 2, 3, 4); - AVector vec2 = vec.updateRefs(r -> CVMLong.create(1L+((CVMLong)r.getValue()).longValue()).getRef()); - assertEquals(Vectors.of(2, 3, 4, 5), vec2); - } - - @Test - public void testNext() { - AVector v1 = Samples.INT_VECTOR_256; - AVector v2 = v1.next(); - assertEquals(v1.get(1), v2.get(0)); - assertEquals(v1.get(255), v2.get(254)); - assertEquals(1L, v1.count() - v2.count()); - } - - @Test - public void testIterator() { - int SIZE = 100; - @SuppressWarnings("unchecked") - AVector lv = (VectorLeaf) VectorLeaf.EMPTY; - - for (int i = 0; i < SIZE; i++) { - lv = lv.append(RT.cvm(i)); - assertTrue(lv.isCanonical()); - } - assertEquals(4950L, lv.reduce((acc, v) -> acc + v.longValue(), 0L)); - - // forward iteration - ListIterator it = lv.listIterator(); - Spliterator split = lv.spliterator(); - AtomicLong splitAcc = new AtomicLong(0); - for (int i = 0; i < SIZE; i++) { - assertTrue(it.hasNext()); - assertTrue(split.tryAdvance(a -> splitAcc.addAndGet(a.longValue()))); - assertEquals(i, it.nextIndex()); - assertEquals(RT.cvm(i), it.next()); - } - assertEquals(100, it.nextIndex()); - assertEquals(4950, splitAcc.get()); - assertFalse(it.hasNext()); - - // backward iteration - ListIterator li = lv.listIterator(SIZE); - for (int i = SIZE - 1; i >= 0; i--) { - assertTrue(li.hasPrevious()); - assertEquals(i, li.previousIndex()); - assertCVMEquals(i, li.previous()); - } - assertEquals(-1, li.previousIndex()); - assertFalse(li.hasPrevious()); - } - - @Test - public void testEmptyVectorHash() { - AVector e = Vectors.empty(); - - // test the byte layout of the empty vector - assertEquals(e.getEncoding(), Blob.fromHex("8000")); - assertEquals(e.getHash(), Vectors.of((Object[])new VectorLeaf[0]).getHash()); - } - - @Test - public void testSmallVectorSerialisation() { - // test the byte layout of the vector - // value should be an int VLC encoded to two bytes (0x0701) - assertEquals(Blob.fromHex("80010901"), Vectors.of(1).getEncoding()); - - // value should be a negative int VLC encoded to two bytes (0x077F) - assertEquals(Blob.fromHex("8001097F"), Vectors.of(-1).getEncoding()); - } - - @Test - public void testPrefixLength() throws BadFormatException { - assertEquals(2, Vectors.of(1, 2, 3).commonPrefixLength(Vectors.of(1, 2))); - assertEquals(2, Vectors.of(1, 2).commonPrefixLength(Vectors.of(1, 2, 8))); - assertEquals(0, Vectors.of(1, 2, 3).commonPrefixLength(Vectors.of(2, 2, 3))); - - AVector v1 = Vectors.of(0, 1, 2, 3, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1); - assertEquals(5, v1.commonPrefixLength(Samples.INT_VECTOR_300)); - assertEquals(5, Samples.INT_VECTOR_300.commonPrefixLength(v1)); - assertEquals(v1.count(), v1.commonPrefixLength(v1)); - - assertEquals(10, Samples.INT_VECTOR_10.commonPrefixLength(Samples.INT_VECTOR_23)); - assertEquals(256, Samples.INT_VECTOR_300.commonPrefixLength(Samples.INT_VECTOR_256)); - assertEquals(256, Samples.INT_VECTOR_300.commonPrefixLength(Samples.INT_VECTOR_256.append(RT.cvm(17L)))); - } - - /** - * Generic tests for any vector - * @param v Any Vector - */ - public static void doVectorTests(AVector v) { - long n = v.count(); - - if (n == 0) { - assertSame(Vectors.empty(), v); - } else { - T last = v.get(n - 1); - T first = v.get(0); - assertEquals(n - 1, v.longLastIndexOf(last)); - assertEquals(0L, v.longIndexOf(first)); - - AVector v2 = v.append(first); - assertEquals(first, v2.get(n)); - } - - assertEquals(v.toVector(), Vectors.of(v.toArray())); - - CollectionsTest.doSequenceTests(v); - } - -} diff --git a/convex-core/src/test/java/convex/core/data/prim/ByteTest.java b/convex-core/src/test/java/convex/core/data/prim/ByteTest.java deleted file mode 100644 index 2340ea045..000000000 --- a/convex-core/src/test/java/convex/core/data/prim/ByteTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package convex.core.data.prim; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; - -import org.junit.jupiter.api.Test; - -import convex.core.data.Hash; -import convex.core.data.ObjectsTest; -import convex.core.lang.RT; - -public class ByteTest { - - @Test public void testCache() { - assertSame(CVMByte.create(1),CVMByte.create(1)); - } - - @Test public void testValues() { - for (int i=0; i<256; i++) { - CVMByte b=CVMByte.create(i); - assertSame(b,CVMByte.create((byte)i)); - - Hash h=b.getHash(); - assertNotNull(h); - - // check hash is cached correctly - assertSame(h,b.getHash()); - - ObjectsTest.doAnyValueTests(b); - } - - } - - @Test public void testCVMCast() { - // CVM converts all numbers to Long - assertEquals(RT.cvm(1L),(CVMLong)RT.cvm((byte)1)); - } -} diff --git a/convex-core/src/test/java/convex/core/data/prim/LongTest.java b/convex-core/src/test/java/convex/core/data/prim/LongTest.java deleted file mode 100644 index 8aa2cc70d..000000000 --- a/convex-core/src/test/java/convex/core/data/prim/LongTest.java +++ /dev/null @@ -1,14 +0,0 @@ -package convex.core.data.prim; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -public class LongTest { - - @Test - public void testEquality() { - long v=666666; - assertEquals(CVMLong.create(v),CVMLong.create(v)); - } -} diff --git a/convex-core/src/test/java/convex/core/data/type/TypesTest.java b/convex-core/src/test/java/convex/core/data/type/TypesTest.java deleted file mode 100644 index 0a36e9e6f..000000000 --- a/convex-core/src/test/java/convex/core/data/type/TypesTest.java +++ /dev/null @@ -1,183 +0,0 @@ -package convex.core.data.type; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.stream.Stream; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; - -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.data.ObjectsTest; -import convex.core.data.prim.CVMByte; -import convex.core.data.prim.CVMDouble; -import convex.core.data.prim.CVMLong; -import convex.core.lang.RT; -import convex.core.util.Utils; -import convex.test.Samples; -import convex.test.Samples.ValueArgumentsProvider; - -public class TypesTest { - - - - @Test - public void testNil() { - AType t=Types.NIL; - assertTrue(t.check(null)); - assertFalse(t.check(CVMLong.ONE)); - } - - @Test - public void testLong() { - AType t=Types.LONG; - assertFalse(t.check(null)); - assertTrue(t.check(CVMLong.ONE)); - assertFalse(t.check(CVMDouble.ONE)); - } - - @Test - public void testAddress() { - AType t=Types.ADDRESS; - assertFalse(t.check(null)); - assertFalse(t.check(convex.core.data.Blob.EMPTY)); - assertTrue(t.check(Address.ZERO)); - } - - @Test - public void testAny() { - AType t=Types.ANY; - assertTrue(t.check(null)); - assertTrue(t.check(CVMLong.ONE)); - assertTrue(t.check(CVMDouble.ONE)); - } - - @Test - public void testCollection() { - AType t=Types.COLLECTION; - assertFalse(t.check(null)); - assertFalse(t.check(CVMLong.ONE)); - assertFalse(t.check(CVMDouble.ONE)); - assertTrue(t.check(Samples.LONG_SET_10)); - assertTrue(t.check(Samples.INT_VECTOR_300)); - assertTrue(t.check(Samples.INT_LIST_10)); - } - - @Test - public void testVector() { - AType t=Types.VECTOR; - assertFalse(t.check(null)); - assertFalse(t.check(CVMLong.ONE)); - assertFalse(t.check(CVMDouble.ONE)); - assertFalse(t.check(Samples.LONG_SET_10)); - assertTrue(t.check(Samples.INT_VECTOR_300)); - assertFalse(t.check(Samples.INT_LIST_10)); - } - - @Test - public void testSet() { - AType t=Types.SET; - assertFalse(t.check(null)); - assertFalse(t.check(CVMLong.ONE)); - assertTrue(t.check(Samples.LONG_SET_100)); - assertFalse(t.check(Samples.INT_VECTOR_300)); - } - - @Test - public void testList() { - AType t=Types.LIST; - assertFalse(t.check(null)); - assertFalse(t.check(CVMDouble.ONE)); - assertFalse(t.check(Samples.INT_VECTOR_300)); - assertTrue(t.check(Samples.INT_LIST_10)); - } - - @Test - public void testNumber() { - AType t=Types.NUMBER; - assertFalse(t.check(null)); - assertTrue(t.check(CVMLong.ONE)); - assertTrue(t.check(CVMByte.ONE)); - assertTrue(t.check(CVMDouble.ONE)); - } - - @ParameterizedTest - @ArgumentsSource(ValueArgumentsProvider.class) - public void testSampleValues(ACell a) { - AType t=RT.getType(a); - assertTrue(t.check(a)); - assertSame(a,t.implicitCast(a),"Implicit cast to same runtime type should not change a value"); - - assertNotSame(t,Types.ANY,"Runtime type of a value should not be Any"); - - Class klass=t.getJavaClass(); - assertTrue((a==null)||klass.isInstance(a)); - } - - @Test - public void testTypeNames() { - HashMap names=new HashMap<>(); - Stream.of(Types.ALL_TYPES).forEach(t -> { - String name=t.toString(); - assertFalse(names.containsKey(name),"Name clash "+Utils.getClassName(t)+" has same name ("+name+" ) as type "+Utils.getClassName(names.get(name))); - names.put(name, t); - }); - } - - @Test - public void testTypeCoverage() { - HashSet types=new HashSet<>(); - Stream.of(Types.ALL_TYPES).forEach(t -> { - assertFalse(types.contains(t),"Duplicate type: "+t); - types.add(t); - }); - - Stream.of(Samples.VALUES).forEach(v -> { - AType t=RT.getType(v); - types.remove(t); - }); - - // TODO: differentiate between concrete types and superclasses - // assertTrue(types.isEmpty(),"Types not covered with test values: "+types); - } - - @ParameterizedTest - @ArgumentsSource(TypeArgumentsProvider.class) - public void testAllTypes(AType t) { - ACell a=t.defaultValue(); - - assertTrue(t.check(a)); - assertSame(a,t.implicitCast(a)); - - if (t.allowsNull()) { - assertTrue(t.check(null)); - } else { - assertFalse(t.check(null)); - } - - Class klass=t.getJavaClass(); - assertNotNull(klass); - assertTrue((a==null)||klass.isInstance(a)); - - ObjectsTest.doAnyValueTests(a); - } - - - public static class TypeArgumentsProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(Types.ALL_TYPES).map(t -> Arguments.of(t)); - } - } -} diff --git a/convex-core/src/test/java/convex/core/init/InitTest.java b/convex-core/src/test/java/convex/core/init/InitTest.java deleted file mode 100644 index db9b025dd..000000000 --- a/convex-core/src/test/java/convex/core/init/InitTest.java +++ /dev/null @@ -1,124 +0,0 @@ -package convex.core.init; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.Test; - -import convex.core.Constants; -import convex.core.State; -import convex.core.crypto.AKeyPair; -import convex.core.data.AccountKey; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.core.exceptions.InvalidDataException; -import convex.core.lang.ACVMTest; - -/** - * Tests for Init functionality - * - * Also includes static State instances for Testing - */ -public class InitTest extends ACVMTest { - - public static final AKeyPair[] KEYPAIRS = new AKeyPair[] { - AKeyPair.createSeeded(2), - AKeyPair.createSeeded(3), - AKeyPair.createSeeded(5), - AKeyPair.createSeeded(7), - AKeyPair.createSeeded(11), - AKeyPair.createSeeded(13), - AKeyPair.createSeeded(17), - AKeyPair.createSeeded(19), - }; - - public static ArrayList PEER_KEYPAIRS=(ArrayList) Arrays.asList(KEYPAIRS).stream().collect(Collectors.toList()); - public static ArrayList PEER_KEYS=(ArrayList) Arrays.asList(KEYPAIRS).stream().map(kp->kp.getAccountKey()).collect(Collectors.toList()); - - public static final AKeyPair FIRST_PEER_KEYPAIR = KEYPAIRS[0]; - public static final AccountKey FIRST_PEER_KEY = FIRST_PEER_KEYPAIR.getAccountKey(); - - public static final AKeyPair HERO_KEYPAIR = KEYPAIRS[0]; - public static final AKeyPair VILLAIN_KEYPAIR = KEYPAIRS[1]; - - public static final AccountKey HERO_KEY = HERO_KEYPAIR.getAccountKey(); - - - /** - * Standard Genesis state used for testing - */ - public static final State STATE= createState(); - public static final State BASE = Init.createBaseState(PEER_KEYS); - - - public static State createState() { - return Init.createState(PEER_KEYS); - } - - public static Address HERO=Init.getGenesisAddress(); - public static Address VILLAIN=Init.getGenesisPeerAddress(1); - - public static final Address FIRST_PEER_ADDRESS = Init.getGenesisPeerAddress(0); - - - protected InitTest() { - super(STATE); - } - - @Test - public void testDeploy() { - assertTrue(evalA("(call *registry* (cns-resolve 'asset.box))")); - assertTrue(evalA("(call *registry* (cns-resolve 'asset.box.actor))")); - assertTrue(evalA("(call *registry* (cns-resolve 'asset.nft.simple))")); - assertTrue(evalA("(call *registry* (cns-resolve 'asset.nft.tokens))")); - assertTrue(evalA("(call *registry* (cns-resolve 'convex.asset))")); - assertTrue(evalA("(call *registry* (cns-resolve 'convex.fungible))")); - assertTrue(evalA("(call *registry* (cns-resolve 'convex.play))")); - assertTrue(evalA("(call *registry* (cns-resolve 'convex.trusted-oracle.actor))")); - assertTrue(evalA("(call *registry* (cns-resolve 'convex.trusted-oracle))")); - assertTrue(evalA("(call *registry* (cns-resolve 'torus.exchange))")); - - assertEquals(Init.CORE_ADDRESS, eval("(call *registry* (cns-resolve 'convex.core))")); - assertEquals(Init.REGISTRY_ADDRESS, eval("(call *registry* (cns-resolve 'convex.registry))")); - assertEquals(Init.TRUST_ADDRESS, eval("(call *registry* (cns-resolve 'convex.trust))")); - } - - @Test - public void testInitState() throws InvalidDataException { - STATE.validate(); - assertEquals(0,context().getDepth()); - assertNull(context().getResult()); - - assertEquals(Constants.MAX_SUPPLY, STATE.computeTotalFunds()); - } - - @Test - public void testMemoryExchange() { - AccountStatus as = STATE.getAccount(Init.MEMORY_EXCHANGE_ADDRESS); - assertNotNull(as); - assertTrue(as.getMemory() > 0L); - } - - @Test - public void testHero() { - AccountStatus as=STATE.getAccount(HERO); - assertNotNull(as); - assertEquals(Constants.INITIAL_ACCOUNT_ALLOWANCE,as.getMemory()); - } - - @Test - public void testVILLAIN() { - AccountStatus as=STATE.getAccount(VILLAIN); - assertNotNull(as); - assertEquals(Constants.INITIAL_ACCOUNT_ALLOWANCE,as.getMemory()); - assertNotEquals(HERO,VILLAIN); - } - -} diff --git a/convex-core/src/test/java/convex/core/lang/ACVMTest.java b/convex-core/src/test/java/convex/core/lang/ACVMTest.java deleted file mode 100644 index afd42ec1d..000000000 --- a/convex-core/src/test/java/convex/core/lang/ACVMTest.java +++ /dev/null @@ -1,240 +0,0 @@ -package convex.core.lang; - -import convex.core.State; -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.data.prim.CVMBool; -import convex.core.data.prim.CVMDouble; -import convex.core.data.prim.CVMLong; -import convex.core.init.Init; -import convex.core.init.InitTest; -import convex.core.util.Utils; - -/** - * Base class for CVM tests that work from a given initial state and context. - * - * Provides utility functions for CVM code execution. - */ -public abstract class ACVMTest { - - protected State INITIAL; - private Context CONTEXT; - protected long INITIAL_JUICE; - - /** - * Address of the HERO, equal to the genesis address - */ - protected final Address HERO; - - /** - * Address of the villain: has compromised peer at index 1 (i.e. the second peer) - */ - protected final Address VILLAIN; - - /** - * Balance of hero's account before spending any juice / funds - */ - public final long HERO_BALANCE; - - /** - * Balance of villain's account before spending any juice / funds - */ - public final long VILLAIN_BALANCE; - - /** - * Constructor using a specified Genesis State - * @param genesis Genesis State to use for this CVM test - */ - protected ACVMTest(State genesis) { - this.INITIAL=genesis; - CONTEXT=Context.createFake(genesis,Init.GENESIS_ADDRESS); - HERO=InitTest.HERO; - VILLAIN=InitTest.VILLAIN; - INITIAL_JUICE=CONTEXT.getJuice(); - HERO_BALANCE = INITIAL.getAccount(InitTest.HERO).getBalance(); - VILLAIN_BALANCE = INITIAL.getAccount(InitTest.VILLAIN).getBalance(); - } - - /** - * Default Constructor uses standard testing Genesis State - */ - protected ACVMTest() { - this(InitTest.STATE); - } - - @SuppressWarnings("unchecked") - protected Context context() { - return (Context) CONTEXT.fork(); - } - - /** - * Steps execution in a new forked Context - * @param Type of result - * @param ctx Initial context to fork - * @param source Source form to read - * @return New forked context containing step result - */ - public Context step(Context ctx, String source) { - ACell form = Reader.read(source); - return step(ctx,form); - } - - /** - * Steps execution in a new forked Context - * @param Type of result - * @param ctx Initial context to fork - * @param form Form to compile and execute execute - * @return New forked context containing step result - */ - @SuppressWarnings("unchecked") - public Context step(Context ctx, ACell form) { - // Run form in separate forked context to get result context - Context rctx = ctx.fork(); - rctx=(Context) rctx.run(form); - assert(rctx.getDepth()==0):"Invalid depth after step: "+rctx.getDepth(); - return rctx; - } - - - @SuppressWarnings("unchecked") - public AOp compile(Context c, String source) { - c=c.fork(); - try { - ACell form = Reader.read(source); - AOp op = (AOp) c.expandCompile(form).getResult(); - return op; - } catch (Exception e) { - throw Utils.sneakyThrow(e); - } - } - - - - public T read(String source) { - return Reader.read(source); - } - - /** - * Runs an execution step as a different address. Returns value after restoring - * the original address. - * @param address Address to run as - * @param c Initial Context. Will not be modified. - * @param source Source form - * @return Updates Context - */ - @SuppressWarnings("unchecked") - public Context stepAs(Address address, Context c, String source) { - Context rc = Context.createFake(c.getState(), address); - rc = step(rc, source); - return (Context) Context.createFake(rc.getState(), c.getAddress()).withValue(rc.getValue()); - } - - public boolean evalA(String source) { - return evalA(CONTEXT, source); - } - - public boolean evalA(Context ctx, String source) { - return eval(ctx, source) instanceof Address; - - } - - public boolean evalB(String source) { - return ((CVMBool)eval(source)).booleanValue(); - } - - public boolean evalB(Context ctx, String source) { - return ((CVMBool)eval(ctx, source)).booleanValue(); - } - - public double evalD(Context ctx, String source) { - ACell result=eval(ctx,source); - CVMDouble d=RT.castDouble(result); - if (d==null) throw new ClassCastException("Expected Double, but got: "+RT.getType(result)); - return d.doubleValue(); - } - - public double evalD(String source) { - return evalD(CONTEXT,source); - } - - public long evalL(Context ctx, String source) { - ACell result=eval(ctx,source); - CVMLong d=RT.castLong(result); - if (d==null) throw new ClassCastException("Expected Long, but got: "+RT.getType(result)); - return d.longValue(); - } - - public long evalL(String source) { - return evalL(CONTEXT,source); - } - - public String evalS(String source) { - return eval(source).toString(); - } - - @SuppressWarnings("unchecked") - public T eval(String source) { - return (T) step(source).getResult(); - } - - @SuppressWarnings("unchecked") - public T eval(ACell form) { - return (T) step(CONTEXT,form).getResult(); - } - - public T eval(Context c, String source) { - Context rc = step(c,source); - return rc.getResult(); - } - - public Context step(String source) { - return step(CONTEXT, source); - } - - @SuppressWarnings("unchecked") - public > T comp(ACell form, Context context) { - context=context.fork(); // fork to avoid corrupting original context - AOp code = context.expandCompile(form).getResult(); - return (T) code; - } - - public > T comp(String source, Context context) { - return comp(Reader.read(source),context); - } - - /** - * Compiles source code to a CVM Op - * @param - * @param source - * @return CVM Op - */ - public > T comp(String source) { - return comp(Reader.read(source),CONTEXT); - } - - /** - * Compiles source code to a CVM Op - * @param - * @param code Source code to compile as a form - * @return CVM Op - */ - public > T comp(ACell code) { - return comp(code,CONTEXT); - } - - public ACell expand(ACell form) { - Context ctx=CONTEXT.fork(); - ACell expanded =ctx.expand(form).getResult(); - return expanded; - } - - public ACell expand(String source) { - try { - ACell form=Reader.read(source); - return expand(form); - } - catch (Exception e) { - throw Utils.sneakyThrow(e); - } - } -} diff --git a/convex-core/src/test/java/convex/core/lang/AliasTest.java b/convex-core/src/test/java/convex/core/lang/AliasTest.java deleted file mode 100644 index 381ddeaf4..000000000 --- a/convex-core/src/test/java/convex/core/lang/AliasTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package convex.core.lang; - -import static convex.core.lang.TestState.eval; -import static convex.core.lang.TestState.evalL; -import static convex.core.lang.TestState.step; -import static convex.test.Assertions.assertArityError; -import static convex.test.Assertions.assertAssertError; -import static convex.test.Assertions.assertCastError; -import static convex.test.Assertions.assertStateError; -import static convex.test.Assertions.assertUndeclaredError; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import org.junit.jupiter.api.Test; - -import convex.core.data.Address; - -public class AliasTest { - - @Test public void testLibraryAlias() { - Context ctx=step("(def lib (deploy '(do (def foo 100) (defn bar [] (inc foo)) (defn baz [f] (f foo)))))"); - Address libAddress=eval(ctx,"lib"); - assertNotNull(libAddress); - - // no alias should exist yet - assertUndeclaredError(step(ctx,"foo")); - assertUndeclaredError(step(ctx,"mylib/foo")); - - ctx=step(ctx,"(def mylib lib)"); - - // Alias should now work - assertEquals(100L,evalL(ctx,"mylib/foo")); - - // Use of function with access to library namespace should work - assertEquals(101L,evalL(ctx,"(mylib/bar)")); - assertEquals(101L,evalL(ctx,"(let [f mylib/bar] (f))")); - - // Shouldn't be able to call as an actor - assertStateError(step(ctx,"(call lib (bar))")); - - // should be able to pass a closure to the library - assertEquals(10000L, evalL(ctx,"(let [f (fn [x] (* x x))] (mylib/baz f))")); - assertEquals(99L, evalL(ctx,"(do (def f (fn [x] (dec x))) (mylib/baz f))")); - } - - @Test - public void testImport() { - Context ctx = step("(def lib (deploy '(def foo 100)))"); - Address libAddress = eval(ctx, "lib"); - assertNotNull(libAddress); - - // no alias should exist yet - assertUndeclaredError(step(ctx, "foo")); - assertUndeclaredError(step(ctx, "mylib/foo")); - - ctx = step(ctx, "(import ~lib :as mylib)"); - - // Alias should now work - assertEquals(100L, evalL(ctx, "mylib/foo")); - } - - @Test - public void testBadImports() { - Context ctx = step("(def lib (deploy `(def foo 100)))"); - Address lib = (Address) ctx.getResult(); - assertNotNull(lib); - - assertArityError(step(ctx,"(import)")); - assertArityError(step(ctx,"(import ~lib)")); - assertArityError(step(ctx,"(import ~lib :as)")); - assertArityError(step(ctx,"(import ~lib :as foo bar)")); - - // check for bad keyword - assertAssertError(step(ctx,"(import ~lib :blazzzz mylib)")); - - // can't have bad alias - assertAssertError(step(ctx,"(import ~lib :as nil)")); - - // can't have non-address first argument - assertCastError(step(ctx,"(import :foo :as mylib)")); - } - - @Test - public void testTransitiveImports() { - // create first library - Context ctx = step("(def lib1 (deploy '(do (def foo 101))))"); - Address lib1 = (Address) ctx.getResult(); - assertNotNull(lib1); - - ctx = step(ctx,"(def lib2 (deploy '(do (import 0x"+lib1.toHexString()+" :as lib1) (def foo (inc lib1/foo)))))"); - Address lib2 = (Address) ctx.getResult(); - assertNotNull(lib2); - - ctx=step(ctx,"(do (import 0x"+lib1.toHexString()+" :as mylib1) (import 0x"+lib2.toHexString()+" :as mylib2))"); - - assertEquals(101,evalL(ctx,"mylib1/foo")); - assertEquals(102,evalL(ctx,"mylib2/foo")); - assertUndeclaredError(step(ctx,"foo")); - assertUndeclaredError(step(ctx,"mylib1/baddy")); - } - - @Test - public void testLibraryAssumptions() { - Context ctx = step("(def lib (deploy '(defn run [code] (eval code))))"); - Address lib = (Address) ctx.getResult(); - ctx=step(ctx,"(do (import 0x"+lib.toHexString()+" :as lib))"); - - // context setup should not change - assertEquals(ctx.getAddress(),eval(ctx,"(lib/run '*address*)")); - assertEquals(ctx.getOrigin(),eval(ctx,"(lib/run '*origin*)")); - assertEquals(ctx.getCaller(),eval(ctx,"(lib/run '*caller*)")); - assertEquals(ctx.getState(),eval(ctx,"(lib/run '*state*)")); - - // library def should define values in the current user's environment. - assertEquals(1337L,evalL(ctx,"(do (lib/run '(def x 1337)) x)")); - - // shouldn't be possible by default to call on library code. Could be dangerous! - assertStateError(step(ctx,"(call lib (run 1))")); - } -} diff --git a/convex-core/src/test/java/convex/core/lang/CompilerTest.java b/convex-core/src/test/java/convex/core/lang/CompilerTest.java deleted file mode 100644 index 8d67998b3..000000000 --- a/convex-core/src/test/java/convex/core/lang/CompilerTest.java +++ /dev/null @@ -1,595 +0,0 @@ -package convex.core.lang; - -import static convex.test.Assertions.assertArityError; -import static convex.test.Assertions.assertBoundsError; -import static convex.test.Assertions.assertCVMEquals; -import static convex.test.Assertions.assertCastError; -import static convex.test.Assertions.assertCompileError; -import static convex.test.Assertions.assertDepthError; -import static convex.test.Assertions.assertJuiceError; -import static convex.test.Assertions.assertNotError; -import static convex.test.Assertions.assertUndeclaredError; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; - -import org.junit.jupiter.api.Test; - -import convex.core.data.ACell; -import convex.core.data.AHashMap; -import convex.core.data.AList; -import convex.core.data.AVector; -import convex.core.data.Address; -import convex.core.data.Keywords; -import convex.core.data.Lists; -import convex.core.data.Maps; -import convex.core.data.Sets; -import convex.core.data.Strings; -import convex.core.data.Symbol; -import convex.core.data.Syntax; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMBool; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.ParseException; -import convex.core.init.InitTest; -import convex.core.lang.ops.Constant; -import convex.core.lang.ops.Def; -import convex.core.lang.ops.Do; -import convex.core.lang.ops.Invoke; -import convex.core.lang.ops.Lambda; -import convex.core.lang.ops.Local; -import convex.core.lang.ops.Lookup; -import convex.core.util.Utils; -import convex.test.Samples; - -/** - * Tests for basic language features and compiler functionality. - * - * State setup includes only basic accounts and core library. - */ -public class CompilerTest extends ACVMTest { - - protected CompilerTest() { - super(InitTest.STATE); - } - - @Test - public void testConstants() { - assertEquals(1L,evalL("1")); - assertEquals(Samples.FOO,eval(":foo")); - assertCVMEquals('d',eval("\\d")); - assertCVMEquals("baz",eval("\"baz\"")); - - assertSame(Vectors.empty(),eval("[]")); - assertSame(Lists.empty(),eval("()")); - - assertNull(eval("nil")); - assertSame(CVMBool.TRUE,eval("true")); - assertSame(CVMBool.FALSE,eval("false")); - } - - @Test public void testDo() { - assertEquals(1L,evalL("(do 2 1)")); - assertEquals(1L,evalL("(do *depth*)")); // Adds one level to initial depth - assertEquals(2L,evalL("(do (do *depth*))")); - } - - @Test public void testMinCompileRegression() throws IOException { - Context c=context(); - String src=Utils.readResourceAsString("testsource/min.con"); - ACell form=Reader.read(src); - Context exp=c.expand(form); - assertNotError(exp); - Context> com=c.compile(exp.getResult()); - - assertNotError(com); - } - - @Test public void testFnCasting() { - assertEquals(1L,evalL("({2 1} 2)")); - assertNull(eval("({2 1} 1)")); - assertEquals(3L,evalL("({2 1} 1 3)")); - assertCVMEquals(Boolean.TRUE,eval("(#{2 1} 1)")); - assertCVMEquals(Boolean.TRUE,eval("(#{nil 1} nil)")); - assertCVMEquals(Boolean.FALSE,eval("(#{2 1} 7)")); - assertCVMEquals(Boolean.FALSE,eval("(#{2 1} nil)")); - assertEquals(7L,evalL("([] 3 7)")); - - assertEquals(3L,evalL("(:foo {:bar 1 :foo 3})")); - assertNull(eval("(:foo {:bar 1})")); - assertEquals(7L,evalL("(:baz {:bar 1 :foo 3} 7)")); - assertEquals(2L,evalL("(:foo nil 2)")); // TODO: is this sane? treat nil as empty? - - // zero arity failing - assertArityError(step("(:foo)")); - assertArityError(step("({})")); - assertArityError(step("(#{})")); - assertArityError(step("([])")); - - // non-associative lookup - assertCastError(step("(:foo 1 2)")); - - // too much arity - assertArityError(step("({} 1 2 3)")); - assertBoundsError(step("([] 1)")); - assertArityError(step("([] 1 2 3)")); - assertArityError(step("(:foo 1 2 3)")); // arity > type - } - - @Test public void testApply() { - assertCVMEquals(true,eval("(apply = nil)")); - assertCVMEquals(true,eval("(apply = [1 1])")); - assertCVMEquals(false,eval("(apply = [1 1 nil])")); - - assertArityError(step("(apply)")); - - } - - @Test public void testLambda() { - assertEquals(2L,evalL("((fn [a] 2) 3)")); - assertEquals(3L,evalL("((fn [a] a) 3)")); - - assertEquals(1L,evalL("((fn [a] *depth*) 3)")); // Level of invoke depth - } - - - @Test public void testDef() { - assertEquals(2L,evalL("(do (def a 2) (def b 3) a)")); - assertEquals(7L,evalL("(do (def a 2) (def a 7) a)")); - - // TODO: check if these are most logical error types? - assertCompileError(step("(def :a 1)")); - assertCompileError(step("(def a)")); - assertCompileError(step("(def a 2 3)")); - } - - @Test public void testDefMetadataOnLiteral() { - Context ctx=step("(def a ^:foo 2)"); - assertNotError(ctx); - AHashMap m=ctx.getMetadata().get(Symbol.create("a")); - assertSame(CVMBool.TRUE,m.get(Keywords.FOO)); - } - - @Test public void testDefMetadataOnForm() { - String code="(def a ^:foo (+ 1 2))"; - Symbol sym=Symbol.create("a"); - - Context ctx=step(code); - assertNotError(ctx); - ACell v=ctx.getEnvironment().get(sym); - assertCVMEquals(3L,v); - assertSame(CVMBool.TRUE,ctx.getMetadata().get(sym).get(Keywords.FOO)); - } - - @Test public void testDefMetadataOnSymbol() { - Context ctx=step("(def ^{:foo true} a (+ 1 2))"); - assertNotError(ctx); - - Symbol sym=Symbol.create("a"); - ACell v=ctx.getEnvironment().get(sym); - assertCVMEquals(3L,v); - assertSame(CVMBool.TRUE,ctx.getMetadata().get(sym).get(Keywords.FOO)); - } - - @Test public void testCond() { - assertEquals(1L,evalL("(cond nil 2 1)")); - assertEquals(4L,evalL("(cond nil 2 false 3 4)")); - assertEquals(2L,evalL("(cond 1 2 3 4)")); - assertNull(eval("(cond)")); - assertNull(eval("(cond false true)")); - } - - @Test public void testIf() { - assertEquals(read("(cond false 4)"),expand("(if false 4)")); - assertNull(eval("(if false 4)")); - assertEquals(4L,evalL("(if true 4)")); - assertEquals(2L,evalL("(if 1 2 3)")); - assertEquals(3L,evalL("(if nil 2 3)")); - assertEquals(7L,evalL("(if :foo 7)")); - assertEquals(1L,evalL("(if true *depth*)")); - - // test that if macro expansion happens correctly inside vector - assertEquals(Vectors.of(3L,2L),eval("[(if nil 2 3) (if 1 2 3)]")); - - // test that if macro expansion happens correctly inside other macro - assertEquals(3L,evalL("(if (if 1 nil 3) 2 3)")); - - // ARITY error if too few or too many branches - assertArityError(step("(if :foo)")); - assertArityError(step("(if :foo 1 2 3 4 5)")); - } - - @Test - public void testStackOverflow() { - // fake state with default juice - Context c=context(); - - AOp op=Do.create( - // define a nasty function that calls its argument recursively on itself - Def.create("fubar", - Lambda.create(Vectors.of(Symbol.create("func")), - Invoke.create(Local.create(0),Local.create(0)))), - // call the nasty function on itself - Invoke.create(Invoke.create(Lookup.create("fubar"),Lookup.create("fubar"))) - ); - - assertDepthError(c.execute(op)); - } - - @Test - public void testMissingVar() { - assertUndeclaredError(step("this-should-not-resolve")); - } - - @Test - public void testBadEval() { - assertThrows(ParseException.class,()->eval("((")); - } - - @Test - public void testUnquote() { - // Unquote used to execute code at compile time - assertEquals(RT.cvm(3L),eval("~(+ 1 2)")); - assertEquals(Constant.of(3L),comp("~(+ 1 2)")); - - assertEquals(RT.cvm(3L),eval("`~`~(+ 1 2)")); - - // Misc cases - assertNull(eval("`~nil")); - assertEquals(Keywords.STATE,eval("(let [a :state] `~a)")); - assertEquals(Vectors.of(1L,3L),eval("`[1 ~(+ 1 2)]")); - assertEquals(Lists.of(Symbols.INC,3L),eval("`(inc ~(+ 1 2))")); - assertUndeclaredError(step("`~undefined-1")); - assertUndeclaredError(step("~'undefined-1")); - - // not we require compilation down to a single constant - assertEquals(Constant.of(7L),comp("~(+ 7)")); - - assertArityError(step("~(inc)")); - assertCastError(step("~(inc :foo)")); - - // TODO: what are right error types here? - assertCompileError(step("(unquote)")); - assertCompileError(step("(unquote 1 2)")); - } - - @Test - public void testSetHandling() { - // sets used as functions act as a predicate - assertCVMEquals(Boolean.TRUE,eval("(#{1 2} 1)")); - - // get returns value or nil - assertEquals(1L,evalL("(get #{1 2} 1)")); - assertSame(CVMBool.FALSE,eval("(get #{1 2} 3)")); - } - - @Test - public void testQuote() { - assertEquals(Symbols.FOO,eval("(quote foo)")); - assertEquals(Symbols.COUNT,eval("'count")); - assertNull(eval("'nil")); - assertEquals(Lists.of(Symbols.QUOTE,Symbols.COUNT),eval("''count")); - assertEquals(Lists.of(Symbols.QUOTE,Lists.of(Symbols.UNQUOTE,Symbols.COUNT)),eval("''~count")); - - assertEquals(Keywords.STATE,eval("':state")); - assertEquals(Lists.of(Symbols.INC,3L),eval("'(inc 3)")); - - assertEquals(Vectors.of(Symbols.INC,Symbols.DEC),eval("'[inc dec]")); - - assertSame(CVMBool.TRUE,eval("(= (quote a/b) 'a/b)")); - - assertEquals(Symbol.create("undefined-1"),eval("'undefined-1")); - } - - @Test - public void testQuoteDataStructures() { - assertEquals(Maps.of(1,2,3,4), eval("`{~(inc 0) 2 3 ~(dec 5)}")); - assertEquals(Sets.of(1,2,3),eval("`#{1 2 ~(dec 4)}")); - - // TODO: unquote-splicing in data structures. - } - - @Test - public void testQuoteCases() { - // Tests from Racket / Scheme - Context ctx=step("(def x 1)"); - assertEquals(read("(a b c)"),eval(ctx,"`(a b c)")); - assertEquals(read("(a b 1)"),eval(ctx,"`(a b ~x)")); - assertEquals(read("(a b 3)"),eval(ctx,"`(a b ~(+ x 2))")); - assertEquals(read("(a `(b ~x))"),eval(ctx,"`(a `(b ~x))")); - assertEquals(read("(a `(b ~1))"),eval(ctx,"`(a `(b ~~x))")); - assertEquals(read("(a `(b ~1))"),eval(ctx,"`(a `(b ~~`~x))")); - assertEquals(read("(a `(b ~x))"),eval(ctx,"`(a `(b ~~'x))")); - - // Unquote does nothing inside a regular quote - assertEquals(read("(a b (unquote x))"),eval(ctx,"'(a b ~x)")); - assertEquals(read("(unquote x)"),eval(ctx,"'~x")); - - // Unquote escapes surrounding quasiquote - assertEquals(read("(a b (quote 1))"),eval(ctx,"`(a b '~x)")); - } - - - @Test - public void testNestedQuote() { - assertEquals(RT.cvm(10L),eval("(+ (eval `(+ 1 ~2 ~(eval 3) ~(eval `(+ 0 4)))))")); - - assertEquals(RT.cvm(10L),eval("(let [a 2 b 3] (eval `(+ 1 ~a ~(+ b 4))))")); - } - - @Test - public void testQuotedMacro() { - - assertEquals(2L,evalL("(eval '(if true ~(if true 2 3)))")); - } - - @Test - public void testQuotedMetadata() { - // From issue #267 - assertEquals(eval("'(defn foo ^{:a :b} [])"),eval("`(defn foo ^{:a :b} [])")); - assertEquals(eval("'(defn foo ^{:a :b} [a])"),eval("`(defn foo ^{:a :b} [a])")); - } - - - @Test - public void testLet() { - assertEquals(Vectors.of(1L,3L),eval("(let [[a b] [3 1]] [b a])")); - assertEquals(Vectors.of(2L,3L),eval("(let [[a & more] [1 2 3]] more)")); - - // results of bindings should be available for subsequent bindings - assertEquals(Vectors.of(1L,2L,3L),eval("(let [a [1 2] b (conj a 3)] b)")); - - // Result of binding _ is ignored, though side effects must still happen - assertUndeclaredError(step("(let [_ 1] _)")); - assertEquals(Vectors.of(1L,2L),eval("(let [_ (def v [1 2])] v)")); - - // shouldn't be legal to let-bind qualified symbols - assertCompileError(step("(let [foo/bar 1] _)")); - - // ampersand edge cases - assertEquals(Vectors.of(1L,Vectors.of(2L),3L),eval("(let [[a & b c] [1 2 3]] [a b c])")); - - // bad uses of ampersand - assertCompileError(step("(let [[a &] [1 2 3]] a)")); // ampersand at end - assertCompileError(step("(let [[a & b & c] [1 2 3]] [a b c])")); // too many Cooks! - - } - - @Test - public void testLetRebinding() { - assertEquals(6L,evalL("(let [a 1 a (inc a) a (* a 3)] a)")); - - assertUndeclaredError(step("(do (let [a 1] a) a)")); - } - - @Test - public void testLoopBinding() { - assertEquals(Vectors.of(1L,3L),eval("(loop [[a b] [3 1]] [b a])")); - assertEquals(Vectors.of(2L,3L),eval("(loop [[a & more] [1 2 3]] more)")); - } - - @Test - public void testLoopRecur() { - // infinite loop should run out of juice - assertJuiceError(step("(loop [] (recur))")); - - // infinite loop with wrong arity should fail with arity error first - assertArityError(step("(loop [] (recur 1))")); - - assertEquals(Vectors.of(3L,2L,1L),eval ("(loop [v [] n 3] (cond (> n 0) (recur (conj v n) (dec n)) v))")); - - } - - @Test - public void testLookupAddress() { - Lookup l=comp("foo"); - assertEquals(Constant.of(HERO),l.getAddress()); - } - - @Test - public void testFnArity() { - assertNull(eval("((fn []))")); - assertEquals(Vectors.of(2L,3L),eval("((fn [a] a) [2 3])")); - assertArityError(step("((fn))")); - } - - @Test - public void testFnBinding() { - assertEquals(Vectors.of(1L,3L),eval("(let [f (fn [[a b]] [b a])] (f [3 1]))")); - assertEquals(Vectors.of(2L,3L),eval("(let [f (fn [[a & more]] more)] (f [1 2 3]))")); - assertEquals(Vectors.of(2L,3L),eval("(let [f (fn [[_ & more]] more)] (f [1 2 3]))")); - - // Test that parameter binding of outer fn is accessible in inner fn closure. - assertEquals(10L,evalL("(let [f (fn [g] (fn [x] (g x)))] ((f inc) 9))")); - - // this should fail because g is not in lexical bindings of f when defined - assertUndeclaredError(step("(let [f (fn [x] (g x)) g (fn [y] (inc y))] (f 3))")); - } - - @Test - public void testMultiFn() { - assertEquals(CVMLong.ZERO,eval("((fn ([] 0) ([x] 1)) )")); - assertEquals(CVMLong.ONE,eval("((fn ([] 0) ([x] 1)) :foo)")); - - // Test closing over lexical environment in MultiFn - assertEquals(43L,evalL("(let [a 42 f (fn ([b] (+ a b)) ([] 666)) ] (f 1))")); - } - - @Test - public void testBindings() { - assertEquals (2L,evalL("(let [[] nil] 2)")); // nil acts like empty sequence here? - assertEquals (2L,evalL("(let [[] []] 2)")); // empty binding vector is OK - - // empty binding vector (vararg length zero) - assertEquals (Vectors.empty(),eval("(let [[& more] []] more)")); - assertEquals (Vectors.empty(),eval("(let [[& more] nil] more)")); // nil acts like empty sequence? - - assertEquals (2L,evalL("(let [[a] #{2}] a)")); - assertEquals (Sets.of(1L, 2L),eval("(into #{} (let [[a b] #{1 2}] [a b]))")); - - // TODO: should we allow this? Technically just one vararg... - assertEquals (Vectors.of(1,2,3),eval("(let [[& &] [1 2 3]] &)")); - - } - - @Test - public void testBindingError() { - - - - // this should fail because of insufficient arguments - assertArityError(step("(let [[a b] [1]] a)")); - - // this should fail because of insufficient arguments - assertArityError(step("(let [[a b] #{2}] a)")); - - // these should fail because of incorrect argument type - assertArityError(step("(let [[a b] nil] a)")); // treated as empty sequence - assertCastError(step("(let [[a b] :foo] a)")); - - // this should fail because of too many arguments - assertArityError(step("(let [[a b] [1 2 3]] a)")); - - // this should fail because of bad ampersand usage - assertCompileError(step("((fn [a &]) 1 2)")); - - // this should fail because of multiple ampersand usage - assertCompileError(step("(let [[a & b & c] [1 2 3 4]] b)")); - - // insufficient arguments for variadic binding - assertArityError(step("(let [[a & b c d] [1 2]])")); - } - - @Test - public void testBindingParamPriority() { - // if closure is constructed correctly, fn param overrides previous lexical binding - assertEquals(2L,evalL("(let [a 3 f (fn [a] a)] (f 2))")); - - // likewise, lexical parameter should override definition in environment - assertEquals(2L,evalL("(do (def a 3) ((fn [a] a) 2))")); - } - - @Test - public void testLetVsDef() { - assertEquals(Vectors.of(3L,12L,11L),eval("(do (def a 2) [(let [a 3] a) (let [a (+ a 10)] (def a (dec a)) a) a])")); - } - - @Test - public void testDiabolicals() { - // 2^10000 map, too deep to expand - assertDepthError(context().expand(Samples.DIABOLICAL_MAP_2_10000)); - // 30^30 map, too much data to expand - assertJuiceError(context().expand(Samples.DIABOLICAL_MAP_30_30)); - } - - @Test - public void testDefExpander() { - Context c=context(); - String source="(defexpander bex [x e] (syntax \"foo\"))"; - ACell exp=expand(source); - assertTrue(exp instanceof AList); - - AOp compiled=comp(exp,c); - - c=c.execute(compiled); - assertNotError(c); - assertTrue(c.getEnvironment().get(Symbol.create("bex")) instanceof AFn); - assertTrue(c.getMetadata().get(Symbol.create("bex")).containsKey(Keywords.EXPANDER_Q)); - - compiled=comp("(bex 2)",c); - c=c.execute(compiled); - assertEquals(Strings.create("foo"),c.getResult()); - } - - @Test public void testExpansion() { - assertEquals(Keywords.FOO,expand(":foo")); - - assertEquals(Syntax.create(Keywords.FOO,Maps.of(Keywords.BAR,CVMBool.TRUE)),Reader.read("^:bar :foo")); - assertEquals(Syntax.create(Keywords.FOO,Maps.of(Keywords.BAR,CVMBool.TRUE)),expand("^:bar :foo")); - } - - @Test - public void testExpandQuote() { - assertEquals(null,expand("nil")); - assertEquals(Lists.of(Symbols.QUOTE,Symbols.FOO),expand("'foo")); - assertEquals(Lists.of(Symbols.QUOTE,Lists.of(Symbols.UNQUOTE,Symbols.FOO)),expand("'~foo")); - assertEquals(Lists.of(Symbols.QUOTE,Lists.of(Symbols.QUOTE,Lists.of(Symbols.UNQUOTE,Symbols.FOO))),expand("''~foo")); - - assertEquals(Lists.of(Symbols.QUASIQUOTE,Symbols.FOO),expand("`foo")); - - } - - @Test - public void testQuoteCompile() { - assertEquals(Constant.create((ACell)null),comp("nil")); - assertEquals(Lookup.create(HERO,Symbols.FOO),comp("foo")); - assertEquals(Lookup.create(HERO,Symbols.FOO),comp("`~foo")); - } - - @Test - public void testMacrosInMaps() { - assertEquals(Maps.of(1L,2L),eval("(eval '{(if true 1 2) (if false 1 2)})")); - assertEquals(Maps.of(1L,2L),eval("(eval `{(if true 1 2) ~(if false 1 2)})")); - } - - @Test - public void testMacrosNested() { - AVector expected=Vectors.of(1L,2L); - assertEquals(expected,eval("(when (or nil true) (and [1 2]))")); - } - - @Test - public void testMacrosInActor() { - Context ctx=context(); - ctx=step(ctx,"(def lib (deploy `(do (defmacro foo [x] :foo))))"); - Address addr=(Address) ctx.getResult(); - assertNotNull(addr); - - ctx=step(ctx,"(def baz (lib/foo 1))"); - assertEquals(Keywords.FOO,ctx.getResult()); - - ctx=step(ctx,"(def bar ("+addr+"/foo 2))"); - assertEquals(Keywords.FOO,ctx.getResult()); - } - - @Test - public void testMacrosInSets() { - assertEquals(Sets.of(1L,2L),eval("(eval '#{(if true 1 2) (if false 1 2)})")); - assertEquals(Sets.of(1L,2L),eval("(eval `#{(if true 1 2) ~(if false 1 2)})")); - } - - @Test - public void testEdgeCases() { - assertFalse(evalB("(= *juice* *juice*)")); - assertEquals(Maps.of(1L,2L),eval("{1 2 1 2}")); - - // TODO: sanity check? Does/should this depend on map ordering? - assertEquals(1L,evalL("(count {~(inc 1) 3 ~(dec 3) 4})")); - - assertEquals(Maps.of(11L,5L),eval("{~((fn [x] (do (return (+ x 7)) 100)) 4) 5}")); - assertEquals(Maps.of(1L,2L),eval("{(inc 0) 2}")); - - // TODO: figure out correct behaviour for this. Depends on read vs. readSyntax? - //assertEquals(4L,evalL("(count #{*juice* *juice* *juice* *juice*})")); - //assertEquals(2L,evalL("(count {*juice* *juice* *juice* *juice*})")); - } - - @Test - public void testInitialEnvironment() { - // list should be a core function - ACell eval=eval("list"); - assertTrue(eval instanceof AFn); - - // if should be a macro implemented as an expander - // assertTrue(eval("if") instanceof AExpander); - - // def should be a special form, and evaluate to a symbol - assertEquals(Symbols.DEF,eval("def")); - } -} diff --git a/convex-core/src/test/java/convex/core/lang/ContextTest.java b/convex-core/src/test/java/convex/core/lang/ContextTest.java deleted file mode 100644 index 70d974a7c..000000000 --- a/convex-core/src/test/java/convex/core/lang/ContextTest.java +++ /dev/null @@ -1,201 +0,0 @@ -package convex.core.lang; - -import static convex.test.Assertions.assertCVMEquals; -import static convex.test.Assertions.assertDepthError; -import static convex.test.Assertions.assertJuiceError; -import static convex.test.Assertions.assertUndeclaredError; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -import convex.core.Constants; -import convex.core.ErrorCodes; -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.Address; -import convex.core.data.BlobMaps; -import convex.core.data.Keyword; -import convex.core.data.Strings; -import convex.core.data.Symbol; -import convex.core.data.Vectors; -import convex.core.init.InitTest; -import convex.core.lang.ops.Special; - -/** - * Tests for basic execution Context mechanics and internals - */ -public class ContextTest extends ACVMTest { - - protected ContextTest() { - super(InitTest.BASE); - } - - private final Address ADDR=context().getAddress(); - - @Test - public void testDefine() { - Symbol sym = Symbol.create("the-test-symbol"); - - final Context c2 = context().define(sym, Strings.create("buffy")); - assertCVMEquals("buffy", c2.lookup(sym).getResult()); - - assertUndeclaredError(c2.lookup(Symbol.create("some-bad-symbol"))); - } - - @Test - public void testQuery() { - Context c2 = context(); - c2=c2.query(Reader.read("(+ 1 2)")); - assertNotSame(c2,context()); - assertCVMEquals(3L,c2.getResult()); - assertEquals(context().getDepth(),c2.getDepth(),"Query should preserve context depth"); - - c2=c2.query(Reader.read("*address*")); - assertEquals(c2.getAddress(),c2.getResult()); - } - - @Test - public void testSymbolLookup() { - Context CTX=context(); - Symbol sym1=Symbol.create("count"); - assertEquals(Core.COUNT,CTX.lookup(sym1).getResult()); - - } - - @Test - public void testUndefine() { - Symbol sym = Symbol.create("the-test-symbol"); - - final Context c2 = context().define(sym, Strings.create("vampire")); - assertCVMEquals("vampire", c2.lookup(sym).getResult()); - - final Context c3 = c2.undefine(sym); - assertUndeclaredError(c3.lookup(sym)); - - final Context c4 = c3.undefine(sym); - assertSame(c3,c4); - } - - @Test - public void testExceptionalState() { - Context ctx=context(); - - assertFalse(ctx.isExceptional()); - assertTrue(ctx.withError(ErrorCodes.ASSERT).isExceptional()); - assertTrue(ctx.withError(ErrorCodes.ASSERT,"Assert Failed").isExceptional()); - - assertThrows(IllegalArgumentException.class,()->ctx.withError((Keyword)null)); - - assertThrows(Error.class,()->ctx.withError(ErrorCodes.ASSERT).getResult()); - } - - @Test - public void testJuice() { - Context c=context(); - assertTrue(c.checkJuice(1000)); - - // get a juice error if too much juice consumed - assertJuiceError(c.consumeJuice(c.getJuice() + 1)); - - // no error if all juice is consumed - c=context(); - assertFalse(c.consumeJuice(c.getJuice()).isExceptional()); - } - - @Test - public void testDepth() { - Context c=context(); - assertEquals(0L,c.getDepth()); - assertEquals(0L,evalL("*depth*")); - assertEquals(1L,evalL("(do *depth*)")); - assertEquals(2L,evalL("(do (do *depth*))")); - - // functions should add one level of depth - assertEquals(1L,evalL("((fn [] *depth*))")); // invoke only - assertEquals(2L,evalL("(do (defn f [] *depth*) (f))")); // do + invoke - - // In compiler unquote - assertEquals(2L,evalL("~*depth*")); // compile, unquote - assertEquals(3L,evalL("~(do *depth*)")); // compile+ unquote + do - - // in custom expander - assertEquals(2L,evalL("(expand :foo (fn [x e] *depth*))")); // in expand, invoke - assertEquals(1L,evalL("(expand *depth* (fn [x e] x))")); // in expand arg - - - // In expansion, should be equivalent to expanded code - assertEquals(evalL("*depth*"),evalL("`~*depth*")); - assertEquals(evalL("(do *depth*)"),evalL("`~(do *depth*)")); - - } - - @Test - public void testDepthLimit() { - Context c=context().withDepth(Constants.MAX_DEPTH-1); - assertEquals(Constants.MAX_DEPTH-1,c.getDepth()); - - // Can run 1 deep at this depth - assertEquals(RT.cvm(Constants.MAX_DEPTH-1),c.execute(comp("*depth*")).getResult()); - assertNull(c.execute(comp("(do)")).getResult()); - - // Shouldn't be possible to execute any Op beyond max depth - assertDepthError(c.execute(comp("(do *depth*)"))); - } - - - @Test - public void testSpecial() { - Context ctx=context(); - assertEquals(ADDR, eval(Symbols.STAR_ADDRESS)); - assertEquals(ADDR, eval(Symbols.STAR_ORIGIN)); - assertNull(eval(Symbols.STAR_CALLER)); - - // Compiler returns Special Op - assertEquals(Special.forSymbol(Symbols.STAR_BALANCE),comp("*balance*")); - - assertNull(eval(Symbols.STAR_RESULT)); - assertCVMEquals(ctx.getJuice(), eval(Special.forSymbol(Symbols.STAR_JUICE))); - assertCVMEquals(0L,eval(Symbols.STAR_DEPTH)); - assertCVMEquals(ctx.getBalance(ADDR),eval(Symbols.STAR_BALANCE)); - assertCVMEquals(0L,eval(Symbols.STAR_OFFER)); - - assertCVMEquals(0L,eval(Symbols.STAR_SEQUENCE)); - - assertCVMEquals(Constants.INITIAL_TIMESTAMP,eval(Symbols.STAR_TIMESTAMP)); - - assertSame(ctx.getState(), eval(Symbols.STAR_STATE)); - assertSame(BlobMaps.empty(),eval(Symbols.STAR_HOLDINGS)); - - assertUndeclaredError(ctx.eval(Symbol.create("*bad-special-symbol*"))); - } - - @Test - public void testLog() { - Context c = context(); - assertTrue(c.getLog().isEmpty()); - - AVector v=Vectors.of(1,2,3); - c.appendLog(v); - - AVector> log=c.getLog(); - assertFalse(c.getLog().isEmpty()); - - - assertEquals(1,log.count()); - assertEquals(v,log.get(0).get(1)); - } - - @Test - public void testReturn() { - Context ctx=context(); - ctx = ctx.withResult(RT.cvm(100)); - assertEquals(ctx.getDepth(), ctx.getDepth()); - } - -} diff --git a/convex-core/src/test/java/convex/core/lang/CoreTest.java b/convex-core/src/test/java/convex/core/lang/CoreTest.java deleted file mode 100644 index a8674f787..000000000 --- a/convex-core/src/test/java/convex/core/lang/CoreTest.java +++ /dev/null @@ -1,3793 +0,0 @@ -package convex.core.lang; - -import static convex.test.Assertions.assertArgumentError; -import static convex.test.Assertions.assertArityError; -import static convex.test.Assertions.assertAssertError; -import static convex.test.Assertions.assertBoundsError; -import static convex.test.Assertions.assertCVMEquals; -import static convex.test.Assertions.assertCastError; -import static convex.test.Assertions.assertCompileError; -import static convex.test.Assertions.assertDepthError; -import static convex.test.Assertions.assertError; -import static convex.test.Assertions.assertFundsError; -import static convex.test.Assertions.assertJuiceError; -import static convex.test.Assertions.assertMemoryError; -import static convex.test.Assertions.assertNobodyError; -import static convex.test.Assertions.assertNotError; -import static convex.test.Assertions.assertStateError; -import static convex.test.Assertions.assertTrustError; -import static convex.test.Assertions.assertUndeclaredError; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; - -import org.junit.jupiter.api.Test; - -import convex.core.Block; -import convex.core.BlockResult; -import convex.core.Constants; -import convex.core.ErrorCodes; -import convex.core.State; -import convex.core.crypto.AKeyPair; -import convex.core.data.ABlob; -import convex.core.data.ACell; -import convex.core.data.AHashMap; -import convex.core.data.ASet; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.core.data.Blob; -import convex.core.data.BlobMap; -import convex.core.data.BlobMaps; -import convex.core.data.Format; -import convex.core.data.Hash; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.List; -import convex.core.data.Lists; -import convex.core.data.MapEntry; -import convex.core.data.Maps; -import convex.core.data.Sets; -import convex.core.data.Strings; -import convex.core.data.Symbol; -import convex.core.data.Syntax; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMBool; -import convex.core.data.prim.CVMByte; -import convex.core.data.prim.CVMDouble; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.BadSignatureException; -import convex.core.exceptions.InvalidDataException; -import convex.core.init.Init; -import convex.core.init.InitTest; -import convex.core.lang.impl.CorePred; -import convex.core.lang.impl.ICoreDef; -import convex.core.lang.ops.Constant; -import convex.core.lang.ops.Do; -import convex.core.lang.ops.Invoke; -import convex.core.lang.ops.Lookup; -import convex.core.lang.ops.Special; - -/** - * Test class for core functions in the initial environment. - * - * The state setup included core libraries such as the registry and trust monitors - * which require integration with core language features. - * - * Needs completely deterministic, fully specified behaviour if we want - * consistent results so we need to do a lot of negative testing here. - */ -public class CoreTest extends ACVMTest { - - - protected CoreTest() throws IOException { - super(InitTest.BASE); - } - - @Test - public void testAddress() { - Address a = HERO; - assertEquals(a, eval("(address \"" + a.toHexString() + "\")")); - assertEquals(a, eval("(address 0x" + a.toHexString() + ")")); - assertEquals(a, eval("(address (address \"" + a.toHexString() + "\"))")); - assertEquals(a, eval("(address (blob \"" + a.toHexString() + "\"))")); - assertEquals(a, eval("(address "+a.longValue()+")")); - - // bad arities - assertArityError(step("(address 1 2)")); - assertArityError(step("(address)")); - - // invalid address lengths - not a cast error since argument types (in general) are valid - assertArgumentError(step("(address \"1234abcd\")")); - assertArgumentError(step("(address 0x1234abcd)")); - - // invalid conversions - assertCastError(step("(address :foo)")); - assertCastError(step("(address nil)")); - } - - @Test - public void testBlob() { - Blob b = Blob.fromHex("cafebabe"); - assertEquals(b, eval("(blob \"Cafebabe\")")); - assertEquals(b, eval("(blob (blob \"cafebabe\"))")); - - assertEquals("cafebabe", evalS("(str (blob \"Cafebabe\"))")); - - assertEquals(eval("0x"),eval("(blob (str))")); // blob literal - - assertEquals(eval("*address*"),eval("(address (blob *address*))")); - - // Account key should be a Blob - assertEquals(eval("*key*"),eval("(blob *key*)")); - - - // round trip back to Blob - assertTrue(evalB("(blob? (blob (hash (encoding [1 2 3]))))")); - - assertArityError(step("(blob 1 2)")); - assertArityError(step("(blob)")); - - assertCastError(step("(blob \"f\")")); // odd length hex string bug #54 special case - assertCastError(step("(blob :foo)")); - assertCastError(step("(blob nil)")); - } - - @Test - public void testByte() { - assertSame(CVMByte.create(0x01), eval("(byte 1)")); - assertSame(CVMByte.create(0xff), eval("(byte 255)")); - assertSame(CVMByte.create(0xff), eval("(byte -1)")); - assertSame(CVMByte.create(0xff), eval("(byte (byte -1))")); - - assertCastError(step("(byte nil)")); - assertCastError(step("(byte :foo)")); - - assertArityError(step("(byte)")); - assertArityError(step("(byte nil nil)")); // arity before cast - } - - @Test - public void testDoc() { - assertEquals(42L, evalL("(do (def foo ^{:doc 42} nil) (doc foo))")); - assertEquals(42L, evalL("(do (def a (deploy '(def foo ^{:doc 42} nil))) (doc a/foo))")); - } - - @Test - public void testLet() { - - assertCastError(step("(let [[a b] :foo] b)")); - - assertArityError(step("(let [[a b] nil] b)")); - assertArityError(step("(let [[a b] [1]] b)")); - assertEquals(2L,evalL("(let [[a b] [1 2]] b)")); - assertEquals(2L,evalL("(let [[a b] '(1 2)] b)")); - - assertCompileError(step("(let ['(a b) '(1 2)] b)")); - - // badly formed lets - Issue #80 related - assertCompileError(step("(let)")); - assertCompileError(step("(let :foo)")); - assertCompileError(step("(let [a])")); - - - } - - @Test - public void testGet() { - assertEquals(2L, evalL("(get {1 2} 1)")); - assertEquals(4L, evalL("(get {1 2 3 4} 3)")); - assertEquals(4L, evalL("(get {1 2 3 4} 3 7)")); - assertNull(eval("(get {1 2} 2)")); // null if not present - assertEquals(7L, evalL("(get {1 2 3 4} 5 7)")); // fallback arg - - assertSame(CVMBool.TRUE, eval("(get #{1 2} 1)")); - assertSame(CVMBool.TRUE, eval("(get #{1 2} 2)")); - assertSame(CVMBool.FALSE, eval("(get #{1 2} 3)")); // null if not present - assertEquals(4L, evalL("(get #{1 2} 3 4)")); // fallback - - assertEquals(2L, evalL("(get [1 2 3] 1)")); - assertEquals(2L, evalL("(get [1 2 3] 1 7)")); - assertEquals(7L, evalL("(get [1 2 3] 4 7)")); - assertEquals(7L, evalL("(get [1 2] nil 7)")); - assertEquals(7L, evalL("(get [1 2] -5 7)")); - assertNull(eval("(get [1 2] :foo)")); - assertNull(eval("(get [1 2] 10)")); - assertNull(eval("(get [1 2] -1)")); - assertNull(eval("(get [1 2] 1.0)")); - - assertNull(eval("(get [1 2 3] (byte 1))")); // TODO: is this sane? - - assertNull(eval("(get nil nil)")); - assertNull(eval("(get nil 10)")); - assertEquals(3L, evalL("(get nil 2 3)")); - assertEquals(3L, evalL("(get nil nil 3)")); - - assertArityError(step("(get 1)")); // arity > cast - assertArityError(step("(get)")); - assertArityError(step("(get 1 2 3 4)")); - - assertCastError(step("(get 1 2 3)")); // 3 arg could work, so cast error on 1st arg - assertCastError(step("(get 1 1)")); // 2 arg could work, so cast error on 1st arg - } - - @Test - public void testGetIn() { - assertEquals(2L, evalL("(get-in {1 2} [1])")); - assertEquals(4L, evalL("(get-in {1 {2 4} 3 5} [1 2])")); - assertEquals(1L, evalL("(get-in #{1 2} [1])")); - assertEquals(2L, evalL("(get-in [1 2 3] [1])")); - assertEquals(2L, evalL("(get-in [1 2 3] [1] :foo)")); - assertEquals(3L, evalL("(get-in [1 2 3] '(2))")); - assertEquals(3L, evalL("(get-in (list 1 2 3) [2])")); - assertEquals(4L, evalL("(get-in [1 2 {:foo 4} 3 5] [2 :foo])")); - - // special case: don't coerce to collection if empty sequence of keys - // so non-collection value may be used safely - assertEquals(3L, evalL("(get-in 3 [])")); - - assertEquals(Maps.of(1L, 2L), eval("(get-in {1 2} nil)")); - assertEquals(Maps.of(1L, 2L), eval("(get-in {1 2} [])")); - assertEquals(Vectors.empty(), eval("(get-in [] [])")); - assertEquals(Lists.empty(), eval("(get-in (list) nil)")); - - assertEquals(Keywords.FOO, eval("(get-in {1 2} [3] :foo)")); - assertEquals(Keywords.FOO, eval("(get-in nil [3] :foo)")); - - assertNull(eval("(get-in nil nil)")); - assertNull(eval("(get-in [1 2 3] [:foo])")); - assertNull(eval("(get-in nil [])")); - assertNull(eval("(get-in nil [1 2])")); - assertNull(eval("(get-in #{} [1 2 3])")); - - assertArityError(step("(get-in 1)")); // arity > cast - assertArityError(step("(get-in 1 2 3 4)")); // arity > cast - - assertCastError(step("(get-in 1 2 3)")); - assertCastError(step("(get-in 1 [1])")); - assertCastError(step("(get-in [1] [0 2])")); - assertCastError(step("(get-in 1 {1 2})")); // keys not a sequence - - assertCastError(step("(get-in [1] 1)")); - } - - @Test - public void testLong() { - assertCVMEquals(1L, eval("(long 1)")); - assertEquals(128L, evalL("(long (byte 128))")); - assertEquals(97L, evalL("(long \\a)")); - assertEquals(2147483648L, evalL("(long 2147483648)")); - - assertEquals(4096L, evalL("(long 0x1000)")); - assertEquals(255L, evalL("(long 0xff)")); - assertEquals(4294967295L, evalL("(long 0xffffffff)")); - assertEquals(-1L, evalL("(long 0xffffffffffffffff)")); - assertEquals(255L, evalL("(long 0xff00000000000000ff)")); // only taking last 8 bytes - assertEquals(-1L, evalL("(long 0xcafebabeffffffffffffffff)")); // interpret as big endian big integer - - // Currently we allow bools to cast to longs like this. TODO: maybe reconsider? - assertEquals(1L, evalL("(long true)")); - assertEquals(0L, evalL("(long false)")); - - - assertArityError(step("(long)")); - assertArityError(step("(long 1 2)")); - assertCastError(step("(long nil)")); - assertCastError(step("(long [])")); - assertCastError(step("(long :foo)")); - } - - @Test - public void testChar() { - assertCVMEquals('a', eval("\\a")); - assertCVMEquals('a', eval("(char 97)")); - assertCVMEquals('a', eval("(nth \"bar\" 1)")); - - assertCastError(step("(char nil)")); - assertCastError(step("(char {})")); - - assertArityError(step("(char)")); - assertArityError(step("(char nil nil)")); // arity before cast - - } - - @Test - public void testBoolean() { - // test precise values - assertSame(CVMBool.TRUE, eval("(boolean 1)")); - assertSame(CVMBool.FALSE, eval("(boolean nil)")); - - // nil and false should be falsey - assertFalse(evalB("(boolean false)")); - assertFalse(evalB("(boolean nil)")); - - // anything else should be truthy - assertTrue(evalB("(boolean true)")); - assertTrue(evalB("(boolean [])")); - assertTrue(evalB("(boolean #{})")); - assertTrue(evalB("(boolean 1)")); - assertTrue(evalB("(boolean :foo)")); - - assertArityError(step("(boolean)")); - assertArityError(step("(boolean 1 2)")); - } - - @Test public void testIf() { - // basic branching - assertEquals(1L,evalL("(if true 1 2)")); - assertEquals(2L,evalL("(if false 1 2)")); - - // expressions - assertEquals(6L,evalL("(if (= 1 1) (* 2 3) (* 3 4))")); - assertEquals(12L,evalL("(if (nil? false) (* 2 3) (* 3 4))")); - - - // null return for missing false branch - assertNull(eval("(if false 1)")); - - // TODO: should these be arity errors? - assertArityError(step("(if)")); - assertArityError(step("(if 1)")); - assertArityError(step("(if 1 2 3 4)")); - } - - @Test - public void testEquals() { - assertTrue(evalB("(= \\a)")); - assertTrue(evalB("(= 1 1)")); - assertFalse(evalB("(= 1 2)")); - assertFalse(evalB("(= 1 nil)")); - assertFalse(evalB("(= 1 1.0)")); - assertFalse(evalB("(= \\a \\b)")); - assertFalse(evalB("(= :foo :baz)")); - assertFalse(evalB("(= :foo 'foo)")); - assertTrue(evalB("(= :bar :bar :bar)")); - assertFalse(evalB("(= :bar :bar :bar 2)")); - assertFalse(evalB("(= *juice* *juice*)")); - assertTrue(evalB("(=)")); - assertTrue(evalB("(= = =)")); - assertTrue(evalB("(= nil nil)")); - assertTrue(evalB("(= ##NaN ##NaN)")); // value equality, but not numeric equality - - } - - @Test - public void testEqualsNumeric() { - assertTrue(evalB("(==)")); - assertTrue(evalB("(== ##Inf)")); - assertTrue(evalB("(== ##NaN)")); - assertTrue(evalB("(== 1 1.0)")); - assertFalse(evalB("(== ##NaN ##NaN)")); // value equality, but not numeric equality - - assertCastError(step("(== :foo)")); - assertCastError(step("(== 1 :foo)")); - assertCastError(step("(== #4)")); - assertCastError(step("(== [])")); - } - - @Test - public void testComparisons() { - assertTrue(evalB("(== 0 0.0)")); - assertTrue(evalB("(< 1)")); - assertTrue(evalB("(> 3 2 1)")); - assertTrue(evalB("(>= 3 2)")); - assertTrue(evalB("(<= 1 2.0 2)")); - - assertTrue(evalB("(==)")); - assertTrue(evalB("(<=)")); - assertTrue(evalB("(>=)")); - assertTrue(evalB("(<)")); - assertTrue(evalB("(>)")); - - assertCastError(step("(== nil nil)")); - assertCastError(step("(> nil)")); - assertCastError(step("(< 1 :foo)")); - assertCastError(step("(<= 1 3 \"hello\")")); - assertCastError(step("(>= nil 1.0)")); - - // ##NaN behaviour - assertFalse(evalB("(<= ##NaN ##NaN)")); - assertFalse(evalB("(<= ##NaN 42)")); - assertFalse(evalB("(< ##NaN 42)")); - assertFalse(evalB("(> 42 ##NaN)")); - assertFalse(evalB("(<= 42 ##NaN)")); - assertFalse(evalB("(== ##NaN 42)")); - assertFalse(evalB("(== ##NaN ##NaN)")); - assertFalse(evalB("(>= ##NaN 42)")); - - // TODO: decide if we want short-circuiting behaviour on casts? Probably not? - // assertCastError(step("(>= 1 2 3 '*balance*)")); - assertFalse(evalB("(>= 1 2 3 '*balance*)")); - } - - @Test - public void testLog() { - AVector v0=Vectors.of(1L, 2L); - - Context c=step("(log 1 2)"); - assertEquals(v0,c.getResult()); - AVector> log=c.getLog(); - assertEquals(1,log.count()); // only one address did a log - assertNotNull(log); - - assertEquals(1,log.count()); // one log entry only - assertEquals(v0,log.get(0).get(1)); - - // do second log in same context - AVector v1=Vectors.of(3L, 4L); - c=step(c,"(log 3 4)"); - log=c.getLog(); - - assertEquals(2,log.count()); // should be two entries now - assertEquals(v0,log.get(0).get(1)); - assertEquals(v1,log.get(1).get(1)); - } - - - @Test - public void testLogInActor() { - AVector v0=Vectors.of(1L, 2L); - - Context c=step("(deploy '(do (defn event ^{:callable? true} [& args] (apply log args)) (defn non-event ^{:callable? true} [& args] (rollback (apply log args)))))"); - Address actor=(Address) c.getResult(); - - assertEquals(0,c.getLog().count()); // Nothing logged so far - - // call actor function - c=step(c,"(call "+actor+" (event 1 2))"); - AVector> log = c.getLog(); - - assertEquals(1,log.count()); // should be one entry by the actor - assertEquals(v0,log.get(0).get(1)); - - // call actor function which rolls back - should also roll back log - c=step(c,"(call "+actor+" (non-event 3 4))"); - log = c.getLog(); - assertEquals(1,log.count()); // should be one entry by the actor - assertEquals(v0,log.get(0).get(1)); - - } - - @Test - public void testVector() { - assertEquals(Vectors.of(1L, 2L), eval("(vector 1 2)")); - assertEquals(Vectors.empty(), eval("(vector)")); - } - - @Test - public void testVectorTypes() { - assertEquals(Vectors.of(1L, 2L).getEncoding(),MapEntry.of(1L, 2L).getEncoding()); // should be same Hash / Encoding - assertEquals(Vectors.of(1L, 2L), eval("(first {1 2})")); // map entry is a vector - } - - @Test - public void testIdentity() { - assertNull(eval("(identity nil)")); - assertEquals(Vectors.of(1L, 2L), eval("(identity [1 2])")); - - assertArityError(step("(identity)")); - assertArityError(step("(identity 1 2)")); - } - - @Test - public void testConcat() { - assertNull(eval("(concat)")); - assertNull(eval("(concat nil nil nil)")); - - // singleton identity preservation - assertSame(Vectors.empty(), eval("(concat [])")); - assertSame(Vectors.empty(), eval("(concat nil [])")); - assertSame(Lists.empty(), eval("(concat () nil)")); - assertSame(Lists.empty(), eval("(concat nil ())")); - - assertCastError(step("(concat 1 2)")); - assertCastError(step("(concat \"Foo\" \"Bar\")")); - - assertEquals(Vectors.of(1L, 2L, 3L, 4L), eval("(concat [1 2] [3 4])")); - assertEquals(Vectors.of(1L, 2L, 3L, 4L), eval("(concat nil [1 2] '(3) [] [4])")); - assertEquals(List.of(1L, 2L, 3L, 4L), eval("(concat nil '(1 2) [3 4] nil)")); - } - - @Test - public void testMapcat() { - assertNull(eval("(mapcat (fn[x] x) nil)")); - assertEquals(Vectors.of(2L, 2L), eval("(mapcat (fn [x] [x x]) [2])")); - assertEquals(Vectors.empty(), eval("(mapcat (fn [x] nil) [1 2 3 4])")); - assertEquals(Lists.empty(), eval("(mapcat (fn [x] nil) '(1 2 3 4))")); - assertEquals(Vectors.of(1L, 2L, 3L), eval("(mapcat vector [1 2 3])")); - assertEquals(Vectors.of(1L, 2L, 2L, 3L, 3L, 4L), eval("(mapcat (fn [a b] [a b]) [1 2 3] [2 3 4])")); - - assertArityError(step("(mapcat identity)")); - assertCastError(step("(mapcat nil [1 2])")); - } - - - @Test - public void testHashMap() { - assertEquals(Maps.empty(), eval("(hash-map)")); - assertEquals(Maps.of(1L, 2L), eval("(hash-map 1 2)")); - assertEquals(Maps.of(null, null), eval("(hash-map nil nil)")); - assertEquals(Maps.of(1L, 2L, 3L, 4L), eval("(hash-map 3 4 1 2)")); - - // Check last value of equal keys is used - assertEquals(Maps.of(1L, 4L), eval("(hash-map 1 2 1 3 1 4)")); - assertEquals(Maps.of(1L, 2L), eval("(hash-map 1 4 1 3 1 2)")); - - assertArityError(step("(hash-map 1)")); - assertArityError(step("(hash-map 1 2 3)")); - assertArityError(step("(hash-map 1 2 3 4 5)")); - } - - @Test - public void testBlobMap() { - assertSame(BlobMaps.empty(), eval("(blob-map)")); - - assertEquals(eval("(blob-map 0xa2 :foo)"),eval("(assoc (blob-map) 0xa2 :foo)")); - assertEquals(eval("(blob-map 0xa2 :foo 0xb3 :bar)"),eval("(assoc (blob-map) 0xa2 :foo 0xb3 :bar)")); - - // Bad key types should result in argument errors - assertArgumentError(step("(blob-map :foo :bar)")); // See issue #162 - assertArgumentError(step("(assoc (blob-map) :foo 10)")); // See Issue #101 - - assertArityError(step("(blob-map 0xabcd)")); - assertArityError(step("(blob-map 0xa2 :foo 0xb3)")); - } - - @Test - public void testKeys() { - assertEquals(Vectors.empty(), eval("(keys {})")); - assertEquals(Vectors.of(1L), eval("(keys {1 2})")); - assertEquals(Sets.of(1L, 3L, 5L), eval("(set (keys {1 2 3 4 5 6}))")); - - assertEquals(Vectors.empty(),RT.keys(BlobMaps.empty())); - assertEquals(Vectors.of(HERO),RT.keys(BlobMap.of(HERO, 1L))); - - assertCastError(step("(keys 1)")); - assertCastError(step("(keys [])")); - assertCastError(step("(keys nil)")); // TODO: maybe empty set? - - assertArityError(step("(keys)")); - assertArityError(step("(keys {} {})")); - } - - @Test - public void testValues() { - assertEquals(Vectors.empty(), eval("(values {})")); - assertEquals(Vectors.of(2L), eval("(values {1 2})")); - assertEquals(Sets.of(2L, 4L, 6L), eval("(set (values {1 2 3 4 5 6}))")); - - assertCastError(step("(values 1)")); - assertCastError(step("(values [])")); - assertCastError(step("(values nil)")); // TODO: maybe empty set? - - assertArityError(step("(values)")); - assertArityError(step("(values {} {})")); - } - - @Test - public void testHashSet() { - assertEquals(Sets.empty(), eval("(hash-set)")); - assertEquals(Sets.of(1L, 2L), eval("(hash-set 1 2)")); - assertEquals(Sets.of((Object) null), eval("(hash-set nil nil)")); - assertEquals(Sets.of(1L, 2L, 3L, 4L), eval("(hash-set 3 4 1 2)")); - - // de-duplication - assertEquals(Sets.of(1L, 2L, 3L), eval("(hash-set 1 2 3 1)")); - assertEquals(Sets.of((Long)null), eval("(hash-set nil nil nil)")); - assertEquals(Sets.of(Sets.empty()), eval("(hash-set (hash-set) (hash-set))")); - - assertEquals(Sets.of((Object) null), eval("(hash-set nil)")); - } - - @Test - public void testStr() { - assertEquals("", evalS("(str)")); - assertEquals("1", evalS("(str 1)")); - assertEquals("12", evalS("(str 1 2)")); - assertEquals("42.0", evalS("(str 42.0)")); - assertEquals("##Inf", evalS("(str ##Inf)")); - assertEquals("##-Inf", evalS("(str ##-Inf)")); - assertEquals("##NaN", evalS("(str ##NaN)")); - assertEquals("255", evalS("(str (byte 0xff))")); - assertEquals("baz", evalS("(str \"baz\")")); - assertEquals("bazbar", evalS("(str \"baz\" \"bar\")")); - assertEquals("baz", evalS("(str \\b \\a \\z)")); - assertEquals(":foo", evalS("(str :foo)")); - assertEquals("nil", evalS("(str nil)")); - assertEquals("true", evalS("(str true)")); - assertEquals("cafebabe", evalS("(str (blob \"CAFEBABE\"))")); - - // Standalone chars are stringified Java-style whereas chars embedded in a container (eg. in a vector) - // must be EDN-style readable representations. - // - assertEquals("a", evalS("(str \\a)")); - assertEquals("conve x", evalS("(str \\c \\o \\n \"ve\" \\space \\x)")); - assertEquals("[\\a \\b (fn [] \\newline) (\\return {\\space \\tab})]", - evalS("(str [\\a \\b (fn [] \\newline) (list \\return {\\space \\tab})])")); - } - - @Test - public void testAssert() { - assertNull(eval("(assert)")); - assertNull(eval("(assert true)")); - assertNull(eval("(assert (= 1 1))")); - assertNull(eval("(assert '(= 1 2))")); // form itself is truthy, not evaluated - assertNull(eval("(assert '(assert false))")); // form itself is truthy, not evaluated - assertNull(eval("(assert 1 2 3)")); - - assertAssertError(step("(assert false)")); - assertAssertError(step("(assert true false)")); - assertAssertError(step("(assert (= 1 2))")); - } - - - @Test - public void testCeil() { - // Double cases - assertEquals(1.0,evalD("(ceil 0.001)")); - assertEquals(-1.0,evalD("(ceil -1.25)")); - - // Integral cases - assertEquals(-1.0,evalD("(ceil -1)")); - assertEquals(0.0,evalD("(ceil 0)")); - assertEquals(1.0,evalD("(ceil 1)")); - - // Special cases - assertEquals(Double.NaN,evalD("(ceil ##NaN)")); - assertEquals(Double.POSITIVE_INFINITY,evalD("(ceil (/ 1 0))")); - assertEquals(Double.NEGATIVE_INFINITY,evalD("(ceil (/ -1 0))")); - - assertCastError(step("(ceil #3)")); - assertCastError(step("(ceil :foo)")); - assertCastError(step("(ceil nil)")); - assertCastError(step("(ceil [])")); - - assertArityError(step("(ceil)")); - assertArityError(step("(ceil :foo :bar)")); // arity > cast - } - - @Test - public void testFloor() { - // Double cases - assertEquals(0.0,evalD("(floor 0.001)")); - assertEquals(-2.0,evalD("(floor -1.25)")); - - // Integral cases - assertEquals(-1.0,evalD("(floor -1)")); - assertEquals(0.0,evalD("(floor 0)")); - assertEquals(1.0,evalD("(floor 1)")); - - // Special cases - assertEquals(Double.NaN,evalD("(floor ##NaN)")); - assertEquals(Double.POSITIVE_INFINITY,evalD("(floor (/ 1 0))")); - assertEquals(Double.NEGATIVE_INFINITY,evalD("(floor (/ -1 0))")); - - assertCastError(step("(floor #666)")); - assertCastError(step("(floor :foo)")); - assertCastError(step("(floor nil)")); - assertCastError(step("(floor [])")); - - assertArityError(step("(floor)")); - assertArityError(step("(floor :foo :bar)")); // arity > cast - } - - @Test - public void testAbs() { - // Integer cases - assertEquals(1L,evalL("(abs 1)")); - assertEquals(10L,evalL("(abs (byte 10))")); - assertEquals(17L,evalL("(abs -17)")); - assertEquals(Long.MAX_VALUE,evalL("(abs 9223372036854775807)")); - - // Double cases - assertEquals(1.0,evalD("(abs 1.0)")); - assertEquals(13.0,evalD("(abs (double -13))")); - assertEquals(Math.pow(10,100),evalD("(abs (pow 10 100))")); - - // Fun Double cases - assertEquals(Double.NaN,evalD("(abs ##NaN)")); - assertEquals(Double.POSITIVE_INFINITY,evalD("(abs (/ 1 0))")); - assertEquals(Double.POSITIVE_INFINITY,evalD("(abs (/ -1 0))")); - - // long overflow case - assertEquals(Long.MIN_VALUE,evalL("(abs -9223372036854775808)")); - assertEquals(Long.MAX_VALUE,evalL("(abs -9223372036854775807)")); - - // Needs a numeric type, else CAST error - assertCastError(step("(abs :foo)")); - assertCastError(step("(abs nil)")); - assertCastError(step("(abs #78)")); - assertCastError(step("(abs [1])")); - - assertArityError(step("(abs)")); - assertArityError(step("(abs :foo :bar)")); // arity > cast - } - - @Test - public void testSignum() { - // Integer cases - assertEquals(1L,evalL("(signum 1)")); - assertEquals(1L,evalL("(signum (byte 10))")); - assertEquals(-1L,evalL("(signum -17)")); - assertEquals(1L,evalL("(signum 9223372036854775807)")); - assertEquals(-1L,evalL("(signum -9223372036854775808)")); - - // Double cases - assertEquals(0.0,evalD("(signum 0.0)")); - assertEquals(1.0,evalD("(signum 1.0)")); - assertEquals(-1.0,evalD("(signum (double -13))")); - assertEquals(1.0,evalD("(signum (pow 10 100))")); - - // Needs a numeric type, else cast error - assertCastError(step("(signum #1)")); - assertCastError(step("(signum 0xabab)")); - assertCastError(step("(signum nil)")); - assertCastError(step("(signum :foo)")); - - // Fun Double cases - assertEquals(Double.NaN,evalD("(signum ##NaN)")); - assertEquals(1.0,evalD("(signum ##Inf)")); - assertEquals(-1.0,evalD("(signum ##-Inf)")); - - assertArityError(step("(signum)")); - assertArityError(step("(signum :foo :bar)")); // arity > cast - } - - @Test - public void testNot() { - assertFalse(evalB("(not 1)")); - assertTrue(evalB("(not false)")); - assertTrue(evalB("(not nil)")); - assertFalse(evalB("(not [])")); - - assertArityError(step("(not)")); - assertArityError(step("(not true false)")); - } - - @Test - public void testNth() { - assertEquals(2L, evalL("(nth [1 2] 1)")); - assertEquals(2L, evalL("(nth [1 2] (byte 1))")); - assertCVMEquals('c', eval("(nth \"abc\" 2)")); - assertEquals(CVMByte.create(10), eval("(nth 0xff0a0b 1)")); // Blob nth byte - - assertArityError(step("(nth)")); - assertArityError(step("(nth [])")); - assertArityError(step("(nth [] 1 2)")); - assertArityError(step("(nth 1 1 2)")); // arity > cast - - // nth on Blobs - assertEquals(CVMByte.create(255),eval("(nth 0xFF 0)")); - assertFalse (evalB("(= 16 (nth 0x0010 1))")); - assertTrue(evalB("(== 16 (nth 0x0010 1))")); - - // cast errors for bad indexes - assertCastError(step("(nth [] :foo)")); - assertCastError(step("(nth [] nil)")); - - // cast errors for non-sequential objects - assertCastError(step("(nth :foo 0)")); - assertCastError(step("(nth 12 13)")); - - // BOUNDS error because treated as empty sequence - assertBoundsError(step("(nth nil 10)")); - - assertBoundsError(step("(nth 0x 0)")); - assertBoundsError(step("(nth nil 0)")); - assertBoundsError(step("(nth (str) 0)")); - assertBoundsError(step("(nth {} 10)")); - - assertBoundsError(step("(nth [1 2] 10)")); - assertBoundsError(step("(nth [1 2] -1)")); - assertBoundsError(step("(nth \"abc\" 3)")); - assertBoundsError(step("(nth \"abc\" -1)")); - } - - @Test - public void testList() { - assertEquals(Lists.of(1L, 2L), eval("(list 1 2)")); - assertEquals(Lists.of(Symbols.LIST), eval("(list 'list)")); - assertEquals(Lists.of(Symbols.LIST), eval("'(list)")); - assertEquals(Lists.empty(), eval("(list)")); - } - - @Test - public void testVec() { - assertSame(Vectors.empty(), eval("(vec nil)")); - assertSame(Vectors.empty(), eval("(vec [])")); - assertSame(Vectors.empty(), eval("(vec {})")); - assertSame(Vectors.empty(), eval("(vec (blob-map))")); - - assertEquals( eval("[\\a \\b \\c]"), eval("(vec \"abc\")")); - - assertEquals(Vectors.of(1,2,3,4), eval("(vec (list 1 2 3 4))")); - assertEquals(Vectors.of(MapEntry.of(1,2)), eval("(vec {1,2})")); - - assertCastError(step("(vec 1)")); - assertCastError(step("(vec :foo)")); - - assertArityError(step("(vec)")); - assertArityError(step("(vec 1 2)")); - } - - @Test - public void testReverse() { - assertSame(Lists.empty(), eval("(reverse nil)")); - assertSame(Lists.empty(), eval("(reverse [])")); - assertSame(Vectors.empty(), eval("(reverse ())")); - assertEquals(Vectors.of(1,2,3), eval("(reverse '(3 2 1))")); - assertEquals(Lists.of(1,2,3), eval("(reverse [3 2 1])")); - - assertCastError(step("(reverse #{})")); - assertCastError(step("(reverse {:foo :bar})")); - assertCastError(step("(reverse 0x1234)")); - - assertArityError(step("(reverse)")); - assertArityError(step("(reverse 1 2)")); - } - - @Test - public void testAssocNull() { - // nil is treated as an empty map - assertSame(Maps.empty(),eval("(assoc nil)")); - - // assoc promotes nil to maps - assertEquals(Maps.of(1L, 2L), eval("(assoc nil 1 2)")); - assertEquals(Maps.of(1L, 2L, 3L, 4L), eval("(assoc nil 1 2 3 4)")); - } - - @Test - public void testAssocMaps() { - // no key/values is OK - assertEquals(Maps.empty(), eval("(assoc {})")); - - assertEquals(Maps.of(1L, 2L), eval("(assoc {} 1 2)")); - assertEquals(Maps.of(1L, 2L), eval("(assoc {1 2})")); - assertEquals(Maps.of(1L, 2L, 3L, 4L), eval("(assoc {} 1 2 3 4)")); - assertEquals(Maps.of(1L, 2L), eval("(assoc {1 2} 1 2)")); - assertEquals(Maps.of(1L, 2L, 3L, 4L), eval("(assoc {1 2} 3 4)")); - assertEquals(Maps.of(1L, 2L, 3L, 4L, 5L, 6L), eval("(assoc {1 2} 3 4 5 6)")); - - assertArityError(step("(assoc {} 1 2 3)")); - assertArityError(step("(assoc {} 1)")); - - } - - @Test - public void testAssocIn() { - // empty index cases - type of first arg not checked since no idexing happens - assertEquals(2L, evalL("(assoc-in {} [] 2)")); // empty indexes returns value - assertEquals(2L, evalL("(assoc-in nil [] 2)")); // empty indexes returns value - assertEquals(2L, evalL("(assoc-in :old [] 2)")); // empty indexes returns value - assertEquals(2L, evalL("(assoc-in 13 nil 2)")); // empty indexes returns value (nil considered empty seq) - - // map cases - assertEquals(Maps.of(1L,2L), eval("(assoc-in {} [1] 2)")); - assertEquals(Maps.of(1L,2L,3L,4L), eval("(assoc-in {3 4} [1] 2)")); - assertEquals(Maps.of(1L,2L), eval("(assoc-in nil [1] 2)")); - assertEquals(Maps.of(1L,Maps.of(5L,6L),3L,4L), eval("(assoc-in {3 4} [1 5] 6)")); - - // vector cases - assertEquals(Vectors.of(1L, 5L, 3L),eval("(assoc-in [1 2 3] [1] 5)")); - assertEquals(Vectors.of(5L),eval("(assoc-in [1] [0] 5)")); - assertEquals(MapEntry.of(1L, 5L),eval("(assoc-in (first {1 2}) [1] 5)")); - - // Set cases - assertEquals(Sets.of(1L),eval("(assoc-in #{} [1] true)")); - assertEquals(Sets.of(1L),eval("(assoc-in #{1} [1] true)")); - assertEquals(Sets.of(1L,2L),eval("(assoc-in #{1} [2] true)")); - assertArgumentError(step("(assoc-in #{3} [2] :fail)")); // bad value type - assertCastError(step("(assoc-in #{3} [3 2] :fail)")); // 'true' is not a data structure - - // Cast error - wrong key types - assertCastError(step("(assoc-in (blob-map) :foo :bar)")); - - // Cast errors - not associative collections - assertCastError(step("(assoc-in 1 [2] 3)")); - - // Invalid keys - assertArgumentError(step("(assoc-in [1] [:foo] 3)")); - assertArgumentError(step("(assoc-in [] [42] :foo)")); // Issue #119 - - // cast errors - paths not sequences - assertCastError(step("(assoc-in {} #{:a :b} 42)")); // See Issue 95 - assertCastError(step("(assoc-in {} :foo 42)")); // See Issue 95 - - // Arity error - assertArityError(step("(assoc-in)")); - assertArityError(step("(assoc-in nil)")); - assertArityError(step("(assoc-in nil 1)")); - assertArityError(step("(assoc-in nil 1 2 3)")); - assertArityError(step("(assoc-in :bad-struct [1] 2 :blah)")); // ARITY before CAST - } - - @Test - public void testAssocFailures() { - assertCastError(step("(assoc 1 1 2)")); - assertCastError(step("(assoc :foo)")); - - - // assertCastError(step("(assoc #{} :foo true)")); - - // Invalid keys - assertArgumentError(step("(assoc [1 2 3] 1.4 :foo)")); - assertArgumentError(step("(assoc [1 2 3] nil :foo)")); - assertArgumentError(step("(assoc [] 2 :foo)")); - assertArgumentError(step("(assoc (list) 2 :fail)")); - assertArgumentError(step("(assoc (blob-map) 2 :fail)")); - - // Arity error - assertArityError(step("(assoc)")); - assertArityError(step("(assoc nil 1)")); - assertArityError(step("(assoc nil 1 2 3)")); - assertArityError(step("(assoc 1 1)")); // ARITY before CAST - } - - @Test - public void testAssocVectors() { - assertEquals(Vectors.empty(), eval("(assoc [])")); - assertEquals(Vectors.of(2L, 1L), eval("(assoc [1 2] 0 2 1 1)")); - - // Invalid keys - assertArgumentError(step("(assoc [] 1 7)")); - assertArgumentError(step("(assoc [] -1 7)")); - assertArgumentError(step("(assoc [1 2] :a 2)")); - - assertArityError(step("(assoc [] 1 2 3)")); - assertArityError(step("(assoc [] 1)")); - } - - @Test - public void testDissoc() { - assertEquals(Maps.empty(), eval("(dissoc {1 2} 1)")); - assertEquals(Maps.of(1L, 2L), eval("(dissoc {1 2 3 4} 3)")); - assertEquals(Maps.of(1L, 2L), eval("(dissoc {1 2} 3)")); - assertEquals(Maps.of(1L, 2L), eval("(dissoc {1 2})")); - assertEquals(Maps.empty(), eval("(dissoc nil 1)")); - assertEquals(Maps.empty(), eval("(dissoc {1 2 3 4} 1 3)")); - assertEquals(Maps.of(3L, 4L), eval("(dissoc {1 2 3 4} 1 2)")); - - // blob-map dissocs. Regression tests for #140 (fatal error in dissoc with non-blob keys) - assertSame(BlobMap.EMPTY,eval("(dissoc (blob-map) 1)")); - assertEquals(BlobMap.of(Blob.fromHex("a2"), Keywords.FOO),eval("(dissoc (into (blob-map) [[0xa2 :foo] [0xb3 :bar]]) 0xb3)")); - assertEquals(BlobMap.of(Blob.fromHex("a2"), Keywords.FOO),eval("(dissoc (into (blob-map) [[0xa2 :foo]]) :foo)")); - - assertCastError(step("(dissoc 1 1 2)")); - assertCastError(step("(dissoc #{})")); - assertCastError(step("(dissoc [])")); - - assertArityError(step("(dissoc)")); - } - - @Test - public void testContainsKey() { - assertFalse(evalB("(contains-key? {} 1)")); - assertFalse(evalB("(contains-key? {} nil)")); - assertTrue(evalB("(contains-key? {1 2} 1)")); - - assertFalse(evalB("(contains-key? #{} 1)")); - assertFalse(evalB("(contains-key? #{1 2 3} nil)")); - assertFalse(evalB("(contains-key? #{false} true)")); - assertTrue(evalB("(contains-key? #{1 2} 1)")); - assertTrue(evalB("(contains-key? #{nil 2 3} nil)")); - - assertFalse(evalB("(contains-key? [] 1)")); - assertFalse(evalB("(contains-key? [0 1 2] :foo)")); - assertTrue(evalB("(contains-key? [3 4] 1)")); - - assertFalse(evalB("(contains-key? nil 1)")); - - assertArityError(step("(contains-key? 3)")); - assertArityError(step("(contains-key? {} 1 2)")); - assertCastError(step("(contains-key? 3 4)")); - } - - @Test - public void testDisj() { - assertEquals(Sets.of(2L), eval("(disj #{1 2} 1)")); - assertEquals(Sets.of(1L, 2L), eval("(disj #{1 2} 1.0)")); - assertSame(Sets.empty(), eval("(disj #{1} 1)")); - assertSame(Sets.empty(), eval("(reduce disj #{1 2} [1 2])")); - assertEquals(Sets.empty(), eval("(disj #{} 1)")); - assertEquals(Sets.of(1L, 2L, 3L), eval("(disj (set [3 2 1 2 4]) 4)")); - assertEquals(Sets.of(1L), eval("(disj (set [3 2 1 2 4]) 2 3 4)")); - assertEquals(Sets.empty(), eval("(disj #{})")); - - // nil is treated as empty set - assertSame(Sets.empty(), eval("(disj nil 1)")); - assertSame(Sets.empty(), eval("(disj nil nil)")); - - assertCastError(step("(disj [] 1)")); - assertArityError(step("(disj)")); - } - - @Test - public void testSet() { - assertEquals(Sets.of(1L, 2L, 3L), eval("(set [3 2 1 2])")); - assertEquals(Sets.of(1L, 2L, 3L), eval("(set #{1 2 3})")); - - // equivalent of get with set-as-function - assertEquals(eval("(#{2 3} 2)"),eval("(get #{2 3} 2)")); - assertEquals(eval("(#{2 3} 1)"),eval("(get #{2 3} 1)")); - - assertEquals(Sets.empty(), eval("(set nil)")); // nil treated as empty set of elements - - assertArityError(step("(set)")); - assertArityError(step("(set 1 2)")); - - assertCastError(step("(set 1)")); - } - - @SuppressWarnings("unchecked") - @Test - public void testSetRegression153() throws InvalidDataException { - // See issue #153 - Context c=context(); - c=step(c, "(def s1 #{#5477106 \\*})"); - ASet s1=(ASet) c.getResult(); - s1.validate(); - c=step(c, "(def s2 #{#2 #0 true #3 0x61a049 #242411 #3478095 #9275832328719 #1489754187855142})"); - ASet s2=(ASet) c.getResult(); - s2.validate(); - - ASet u1=s2.includeAll(s1); - u1.validate(); - - c=step(c, "(def union1 (union s2 s1))"); - ASet u2=(ASet) c.getResult(); - u2.validate(); - - } - - - - @Test - public void testSubsetQ() { - assertTrue(evalB("(subset? #{} #{})")); - assertTrue(evalB("(subset? #{} #{1 2 3})")); - assertTrue(evalB("(subset? #{2 3} #{1 2 3 4})")); - - // check nil is handled as empty set - assertTrue(evalB("(subset? nil #{})")); - assertTrue(evalB("(subset? #{} nil)")); - assertTrue(evalB("(subset? nil #{1 2 3})")); - assertFalse(evalB("(subset? #{1 2 3} nil)")); - - - assertFalse(evalB("(subset? #{2 3} #{1 2})")); - assertFalse(evalB("(subset? #{1 2 3} #{0})")); - assertFalse(evalB("(subset? #{#{}} #{#{1}})")); - - assertArityError(step("(subset?)")); - assertArityError(step("(subset? 1)")); - assertArityError(step("(subset? 1 2 3)")); - - assertCastError(step("(subset? 1 2)")); - assertCastError(step("(subset? #{} [2])")); - } - - @Test - public void testSetUnion() { - assertEquals(Sets.empty(),eval("(union)")); - - assertEquals(Sets.empty(),eval("(union nil)")); - assertEquals(Sets.empty(),eval("(union #{})")); - assertEquals(Sets.of(1L,2L),eval("(union #{1 2})")); - - // nil treated as empty set in all cases - assertEquals(Sets.of(1L,2L),eval("(union nil #{1 2})")); - assertEquals(Sets.of(1L,2L),eval("(union #{1 2} nil)")); - - assertEquals(Sets.of(1L,2L,3L),eval("(union #{1 2} #{3})")); - - assertEquals(Sets.of(1L,2L,3L,4L,5L),eval("(union #{1 2} #{3} #{4 5})")); - - assertCastError(step("(union :foo)")); - assertCastError(step("(union [1] [2 3])")); - } - - @Test - public void testSetIntersection() { - assertEquals(Sets.empty(),eval("(intersection nil)")); - assertEquals(Sets.empty(),eval("(intersection #{})")); - assertEquals(Sets.of(1L,2L),eval("(intersection #{1 2})")); - - assertEquals(Sets.empty(),eval("(intersection #{1 2} #{3})")); - assertEquals(Sets.empty(),eval("(intersection #{1 2 3} nil)")); - - assertEquals(Sets.of(2L,3L),eval("(intersection #{1 2 3} #{2 3 4})")); - - assertEquals(Sets.of(3L),eval("(intersection #{1 2 3} #{2 3 4} #{3 4 5})")); - - assertArityError(step("(intersection)")); - - assertCastError(step("(intersection :foo)")); - assertCastError(step("(intersection [1] [2 3])")); - } - - @Test - public void testSetDifference() { - assertEquals(Sets.empty(),eval("(difference nil)")); - assertEquals(Sets.empty(),eval("(difference #{})")); - assertEquals(Sets.of(1L,2L),eval("(difference #{1 2})")); - - assertEquals(Sets.of(1L,2L),eval("(difference #{1 2} #{3})")); - - assertEquals(Sets.of(2L,3L),eval("(difference #{1 2 3} #{1 4})")); - - assertEquals(Sets.of(3L),eval("(difference #{1 2 3} #{2 4} #{1 5})")); - - assertArityError(step("(difference)")); - - assertCastError(step("(difference :foo)")); - assertCastError(step("(difference [1] [2 3])")); - } - - @Test - public void testDifferenceRegression155() { - Context c=step("(do (def arg+ [#{nil #5 #4 #2 #0 #7 #6 #3 #1} #{nil 0x500360a6 :B2Qrb9d1U5WH00h6c \"1pC\" true \\Ñ (quote A/aHAb7K2) #5278509802049781 #515}]) (def u (apply union arg+)) (def d (apply difference arg+)) (= #{} (difference d u)) )"); - assertEquals(CVMBool.TRUE,c.getResult()); - } - - - @Test - public void testFirst() { - assertEquals(1L, evalL("(first [1 2])")); - assertEquals(1L, evalL("(first '(1 2 3 4))")); - - assertBoundsError(step("(first [])")); - assertBoundsError(step("(first nil)")); - - assertArityError(step("(first)")); - assertArityError(step("(first [1] 2)")); - assertCastError(step("(first 1)")); - assertCastError(step("(first :foo)")); - } - - @Test - public void testSecond() { - assertEquals(2L, evalL("(second [1 2])")); - - assertBoundsError(step("(second [2])")); - assertBoundsError(step("(second nil)")); - - assertArityError(step("(second)")); - assertArityError(step("(second [1] 2)")); - assertCastError(step("(second 1)")); - } - - @Test - public void testLast() { - assertEquals(2L, evalL("(last [1 2])")); - assertEquals(4L, evalL("(last [4])")); - - assertBoundsError(step("(last [])")); - assertBoundsError(step("(last nil)")); - - assertArityError(step("(last)")); - assertArityError(step("(last [1] 2)")); - assertCastError(step("(last 1)")); - } - - @Test - public void testNext() { - assertEquals(Vectors.of(2L, 3L), eval("(next [1 2 3])")); - assertEquals(Lists.of(2L, 3L), eval("(next '(1 2 3))")); - - assertNull(eval("(next nil)")); - assertNull(eval("(next [1])")); - assertNull(eval("(next {1 2})")); - - assertArityError(step("(next)")); - assertArityError(step("(next [1] [2 3])")); - - assertCastError(step("(next 1)")); - assertCastError(step("(next :foo)")); - } - - @Test - public void testConj() { - assertEquals(Vectors.of(1L, 2L, 3L), eval("(conj [1 2] 3)")); - assertEquals(Lists.of(3L, 1L, 2L), eval("(conj (list 1 2) 3)")); - - // nil works like empty vector - assertEquals(Vectors.of(1L), eval("(conj nil 1)")); - assertEquals(Vectors.of(3L), eval("(conj nil 3)")); - assertEquals(Sets.of(3L), eval("(conj #{} 3)")); - assertEquals(Sets.of(3L), eval("(conj #{3} 3)")); - - // Maps conj with map entry vectors - assertEquals(Maps.of(1L, 2L), eval("(conj {} [1 2])")); - assertEquals(Maps.of(1L, 2L, 5L, 6L), eval("(conj {1 3 5 6} [1 2])")); - - assertEquals(Lists.of(1L), eval("(conj (list) 1)")); - assertEquals(Lists.of(1L, 2L), eval("(conj (list 2) 1)")); - assertEquals(Sets.of(1L, 2L, 3L), eval("(conj #{2 3} 1)")); - assertEquals(Sets.of(1L, 2L, 3L), eval("(conj #{2 3 1} 1)")); - - // arity 1 OK, no change - assertEquals(Vectors.of(1L, 2L), eval("(conj [1 2])")); - - // Blobmaps - assertEquals(BlobMaps.create(Blob.fromHex("a1"), Blob.fromHex("a2")),eval("(conj (blob-map) [0xa1 0xa2])")); - - // bad data structures - assertCastError(step("(conj :foo)")); - assertCastError(step("(conj :foo 1)")); - - // bad types of elements - assertArgumentError(step("(conj {} 2)")); // can't cast long to a map entry - assertArgumentError(step("(conj {} [1 2 3])")); // wrong size vector for a map entry - assertArgumentError(step("(conj {} '(1 2))")); // wrong type for a map entry - assertArgumentError(step("(conj (blob-map) [:foo 0xa2])")); // bad key type for blobmap - - assertCastError(step("(conj 1 2)")); - assertCastError(step("(conj (str :foo) 2)")); - - assertArityError(step("(conj)")); - } - - @Test - public void testCons() { - assertEquals(Lists.of(3L, 1L, 2L), eval("(cons 3 (list 1 2))")); - assertEquals(Lists.of(3L, 1L, 2L), eval("(cons 3 [1 2])")); - assertEquals(Lists.of(3L, 1L, 2L), eval("(cons 3 1 [2])")); - - assertEquals(Lists.of(3L), eval("(cons 3 nil)")); - assertEquals(Lists.of(1L, 3L), eval("(cons 1 #{3})")); - assertEquals(Lists.of(1L), eval("(cons 1 [])")); - - assertCastError(step("(cons 1 2)")); - - assertArityError(step("(cons [])")); - assertArityError(step("(cons 1)")); - assertArityError(step("(cons)")); - - assertCastError(step("(cons 1 2 3 4 5)")); - } - - @Test - public void testComp() { - assertEquals(43L, evalL("((comp inc) 42)")); - assertEquals(44L, evalL("((comp inc inc) 42)")); - assertEquals(45L, evalL("((comp inc inc inc) 42)")); - assertEquals(46L, evalL("((comp inc inc inc inc) 42)")); - - assertEquals(3.0, evalD("((comp sqrt +) 4 5)")); - - assertArityError(step("(comp)")); - - } - - @Test - public void testInto() { - // nil as data structure - assertNull(eval("(into nil nil)")); - assertEquals(Maps.of(1L,2L),eval("(into nil {1 2})")); - assertEquals(Vectors.empty(), eval("(into nil [])")); - assertEquals(Vectors.of(1L, 2L, 3L), eval("(into nil [1 2 3])")); - - assertEquals(Vectors.of(1L, 2L, 3L), eval("(into [1 2] [3])")); - assertEquals(Vectors.of(1L, 2L, 3L), eval("(into [1 2 3] nil)")); - assertEquals(Vectors.of(1L, 2L, 3L), eval("(into nil [1 2 3])")); - - assertEquals(Lists.of(2L, 1L, 3L, 4L), eval("(into '(3 4) '(1 2))")); - - assertEquals(Sets.of(1L, 2L, 3L), eval("(into #{} [1 2 1 2 3])")); - - // map as data structure - assertEquals(Maps.empty(), eval("(into {} [])")); - assertEquals(Maps.of(1L, 2L, 3L, 4L), eval("(into {} [[1 2] [3 4] [1 2]])")); - - assertEquals(Vectors.of(MapEntry.of(1L, 2L)), eval("(into [] {1 2})")); - - assertCastError(step("(into 1 [2 3])")); // long is not a conjable data structure - assertCastError(step("(into nil :foo)")); // keyword is not a sequence of elements - - // See #151 - assertCastError(step("(into (list) #0)")); // Address is not a sequential data structure - assertCastError(step("(into #0 [])")); // Address is not a conjable data structure - assertCastError(step("(into #0 [1 2])")); // Address is not a conjable data structure - assertCastError(step("(into 0 [])")); // Long is not a conjable data structure - - // bad element types - assertArgumentError(step("(into {} [nil])")); // nil is not a MapEntry - assertArgumentError(step("(into {} [[:foo]])")); // length 1 vector shouldn't convert to MapEntry - assertArgumentError(step("(into {} [[:foo :bar :baz]])")); // length 1 vector shouldn't convert to MapEntry - assertArgumentError(step("(into {1 2} [2 3])")); // longs are not map entries - assertArgumentError(step("(into {1 2} [[] []])")); // empty vectors are not map entries - assertArgumentError(step("(into (blob-map) [[1 2]])")); - - assertArityError(step("(into)")); - assertArityError(step("(into inc)")); - assertArityError(step("(into 1 2 3)")); // arity > cast - } - - @Test - public void testMerge() { - assertEquals(Maps.empty(),eval("(merge)")); - assertEquals(Maps.empty(),eval("(merge nil)")); - assertEquals(Maps.empty(),eval("(merge nil nil)")); - - assertEquals(Maps.of(1L,2L,3L,4L),eval("(merge {1 2} {3 4})")); - assertEquals(Maps.of(1L,2L,3L,4L),eval("(merge {1 2 3 4} {})")); - assertEquals(Maps.of(1L,2L,3L,4L),eval("(merge nil {1 2 3 4})")); - - assertEquals(Maps.of(1L,3L),eval("(merge {1 2} {1 3})")); - assertEquals(Maps.of(1L,3L),eval("(merge nil {1 2} nil {1 3} nil)")); - - assertCastError(step("(merge [])")); - assertCastError(step("(merge {} [1 2 3])")); - assertCastError(step("(merge nil :foo)")); - } - - @Test - public void testDotimes() { - assertEquals(Vectors.of(0L, 1L, 2L), eval("(do (def a []) (dotimes [i 3] (def a (conj a i))) a)")); - assertEquals(Vectors.empty(), eval("(do (def a []) (dotimes [i 0] (def a (conj a i))) a)")); - assertEquals(Vectors.empty(), eval("(do (def a []) (dotimes [i -1.5] (def a (conj a i))) a)")); - assertEquals(Vectors.empty(), eval("(do (def a []) (dotimes [i -1.5]) a)")); - - assertCastError(step("(dotimes [1 10])")); - assertCastError(step("(dotimes [i :foo])")); - assertCastError(step("(dotimes [:foo 10])")); - assertCastError(step("(dotimes :foo)")); - - assertArityError(step("(dotimes)")); - assertArityError(step("(dotimes [i])")); - assertArityError(step("(dotimes [i 2 3])")); - - } - - @Test - public void testDoouble() { - assertEquals(-13.0,evalD("(double -13)")); - assertEquals(1.0,evalD("(double true)")); // ?? cast OK? - - assertEquals(255.0,evalD("(double (byte -1))")); // byte should be 0-255 - - - assertCastError(step("(double :foo)")); - - assertArityError(step("(double)")); - assertArityError(step("(double :foo :bar)")); - } - - @Test - public void testMap() { - assertEquals(Vectors.of(2L, 3L), eval("(map inc [1 2])")); - assertEquals(Vectors.of(2L, 3L), eval("(map inc '(1 2))")); // TODO is this right? - assertEquals(Vectors.empty(), eval("(map inc nil)")); // TODO is this right? - assertEquals(Vectors.of(4L, 6L), eval("(map + [1 2] [3 4 5])")); - assertEquals(Vectors.of(3L), eval("(map + [1 2 3] [2])")); - assertEquals(Vectors.of(1L, 2L, 3L), eval("(map identity [1 2 3])")); - - assertCastError(step("(map 1 [1])")); - assertCastError(step("(map 1 [] [] [])")); - assertCastError(step("(map inc 1)")); - - assertArityError(step("(map)")); - assertArityError(step("(map inc)")); - assertArityError(step("(map 1)")); // arity > cast - } - - @Test - public void testFor() { - assertEquals(Vectors.empty(), eval("(for [x nil] (inc x))")); - assertEquals(Vectors.empty(), eval("(for [x []] (inc x))")); - assertEquals(Vectors.of(2L,3L), eval("(for [x '(1 2)] (inc x))")); - assertEquals(Vectors.of(2L,3L), eval("(for [x [1 2]] (inc x))")); - - // TODO: maybe dubious error types? - - assertCastError(step("(for 1 1)")); // bad binding form - assertArityError(step("(for [x] 1)")); // bad binding form - assertArityError(step("(for [x [1 2] [2 3]] 1)")); // bad binding form length - assertCastError(step("(for [x :foo] 1)")); // bad sequence - - assertArityError(step("(for)")); - assertArityError(step("(for [] nil nil)")); - assertCastError(step("(for 1)")); // arity > cast - } - - @Test - public void testMapv() { - assertEquals(Vectors.empty(), eval("(map inc nil)")); - assertEquals(Vectors.of(2L, 3L), eval("(mapv inc [1 2])")); - assertEquals(Vectors.of(4L, 6L), eval("(mapv + '(1 2) '(3 4 5))")); - - assertArityError(step("(mapv)")); - assertArityError(step("(mapv inc)")); - } - - @Test - public void testFilter() { - assertEquals(Vectors.of(1,2,3), eval("(filter number? [1 :foo 2 :bar 3])")); - assertEquals(Lists.of(Keywords.FOO), eval("(filter #{:foo} '(:foo 2 3))")); - assertNull(eval("(filter keyword? nil)")); - assertEquals(Maps.empty(), eval("(filter nil? {1 2 3 4})")); - assertEquals(Maps.of(Keywords.FOO,1), eval("(filter (fn [[k v]] (keyword? k)) {:foo 1 'bar 2})")); - assertEquals(Sets.of(1,2,3), eval("(filter number? #{1 2 3 :foo})")); - - assertCastError(step("(filter nil? 1)")); - assertCastError(step("(filter 1 [1 2 3])")); - - assertArityError(step("(filter +)")); - assertArityError(step("(filter 1 2 3)")); - } - - @Test - public void testLang() { - { - Context ctx=context(); - ctx=step(ctx,"(def *lang* (fn [x] x))"); - assertEquals(Symbols.COUNT,eval(ctx,"count")); - } - } - - @Test - public void testReduce() { - assertEquals(24L, evalL("(reduce * 1 [1 2 3 4])")); - assertEquals(2L, evalL("(reduce + 2 [])")); - assertEquals(2L, evalL("(reduce + 2 nil)")); - - // add values, indexing into map entries as vectors - assertEquals(10.0, evalD("(reduce (fn [acc me] (+ acc (me 1))) 0.0 {:a 1, :b 2, 107 3, nil 4})")); - // reduce over map, destructuring keys and values - assertEquals(100.0, evalD( - "(reduce (fn [acc [k v]] (let [x (double (v nil))] (+ acc (* x x)))) 0.0 {true {nil 10}})")); - - assertEquals(Lists.of(3,2,1), eval("(reduce conj '() '(1 2 3))")); - - // 2-arg reduce forms - assertEquals(24L, evalL("(reduce * [1 2 3 4])")); - assertEquals(1L, evalL("(reduce * nil)")); - assertEquals(1L, evalL("(reduce + [1])")); - assertEquals(0L, evalL("(reduce + [])")); - assertEquals(Keywords.FOO, eval("(reduce (fn [] :foo) [])")); // 0 arity - assertEquals(Keywords.FOO, eval("(reduce (fn [v] :foo) [:bar])")); // 1 arity - assertEquals(Keywords.FOO, eval("(reduce (fn [a b] :foo) [:bar :baz])")); // 2 arity - - // Errors in reduce function - assertCastError(step("(reduce + [:foo])")); - assertCastError(step("(reduce + 1 [:foo])")); - - assertCastError(step("(reduce 1 2 [])")); - assertCastError(step("(reduce + 2 :foo)")); - assertCastError(step("(reduce + 1)")); - - assertArityError(step("(reduce +)")); - assertArityError(step("(reduce + 1 [2] [3])")); - } - - @Test public void testReduceFail() { - // shouldn't fail because function never called - assertEquals(2L,evalL("(reduce address 2 [])")); - - assertArityError(step("(reduce address 2 [:foo :bar])")); - assertCastError(step("(reduce (fn [a x] (address x)) 2 [:foo :bar])")); - } - - @Test - public void testReduced() { - assertEquals(Vectors.of(2L,3L), eval("(reduce (fn [i v] (if (== v 3) (reduced [i v]) v)) 1 [1 2 3 4 5])")); - - // 2 arg reduce - assertEquals(Vectors.of(2L,3L), eval("(reduce (fn [i v] (if (== v 3) (reduced [i v]) v)) [1 2 3 4 5])")); - assertEquals(CVMLong.create(5L), eval("(reduce (fn [a b] (if (== b 1) (reduced :foo) b)) [1 2 3 4 5])")); // b is never 1 - assertEquals(CVMLong.create(1L), eval("(reduce (fn [v] (reduced v)) [1])")); // fn called with arity 1 - assertEquals(Keywords.FOO, eval("(reduce (fn [] (reduced :foo)) [])")); // fn called with arity 0 - - - assertArityError(step("(reduced)")); - assertArityError(step("(reduced 1 2)")); - - // reduced on its own is an :EXCEPTION Error - assertError(ErrorCodes.EXCEPTION,step("(reduced 1)")); - - // reduced cannot escape actor call boundary - { - Context ctx=context(); - ctx=step(ctx,"(def act (deploy `(do (defn foo ^{:callable? true} [] (reduced 1)))))"); - ctx=step(ctx,"(reduce (fn [_ _] (call act (foo))) nil [nil])"); - assertError(ErrorCodes.EXCEPTION,ctx); - } - - // reduced can escape nested function call - { - Context ctx=context(); - ctx=step(ctx,"(defn foo [x] (reduced x))"); - ctx=step(ctx,"(reduce (fn [i x] (foo x)) nil [1 2])"); - assertCVMEquals(1L,ctx.getResult()); - } - - } - - @Test - public void testReturn() { - // basic return mechanics - assertEquals(1L,evalL("(return 1)")); - - assertEquals(Vectors.empty(), eval("(let [f (fn [x] (+ 1 (return x)))] (f []))")); // return in function body - assertEquals(Vectors.empty(), eval("(let [f (fn [x] (let [a (return x)] 2))] (f []))")); // return in let - // binding - assertEquals(Vectors.empty(), eval("(let [f (fn [x] (let [a 2] (return x) a))] (f []))")); // return in let body - assertEquals(Vectors.empty(), eval("(let [f (fn [x] (do (return x) 2))] (f []))")); // return in do - assertEquals(Vectors.empty(), eval("(let [f (fn [x] (return (return x)))] (f []))")); // nested returns - assertEquals(Vectors.empty(), eval("(let [f (fn [x] ((return x) 2 3))] (f []))")); // return in function call - // position - assertEquals(Vectors.empty(), eval("(let [f (fn [x] (= 2 (return x) 3))] (f []))")); // return in function arg - assertEquals(Vectors.empty(), eval("(let [f (fn [x] (if (return x) 2 3))] (f []))")); // return in cond test - assertEquals(Vectors.empty(), eval("(let [f (fn [x] (if true (return x) 3))] (f []))")); // return in cond - // result - assertEquals(Vectors.empty(), eval("(let [f (fn [x] (if true [] (return x)))] (f 3))")); // return in cond - // default - assertArityError(step("(return)")); - assertArityError(step("(return 1 2)")); - } - - @Test - public void testLoop() { - assertNull(eval("(loop [])")); - assertEquals(Keywords.FOO,eval("(loop [] :foo)")); - assertEquals(Keywords.FOO,eval("(loop [] 1 2 3 :foo)")); - assertEquals(Keywords.FOO,eval("(loop [a :foo] :bar a)")); - - assertCompileError(step("(loop [a])")); - assertCompileError(step("(loop)")); // Issue #80 - assertCompileError(step("(loop :foo)")); // Not a binting vector - } - - @Test - public void testRecordLookup() { - assertEquals(INITIAL.getAccounts(),eval("(*state* :accounts)")); - assertEquals(Keywords.FOO,eval("(*state* [ 982788 ] :foo )")); - assertNull(eval("(*state* [1 2 3])")); // Issue #85 - - assertArityError(step("(*state* :accounts :foo :bar)")); - } - - @Test - public void testRecur() { - // test factorial with accumulator - assertEquals(120L, evalL("(let [f (fn [a x] (if (> x 1) (recur (* a x) (dec x)) a))] (f 1 5))")); - - assertArityError(step("(let [f (fn [x] (recur x x))] (f 1))")); - assertJuiceError(step("(let [f (fn [x] (recur x))] (f 1))")); - - // should hit depth limits before running out of juice - // TODO: think about letrec? - assertDepthError(step("(do (def f (fn [x] (recur (f x)))) (f 1))")); - - // Recur on its own is an :EXCEPTION Error - assertError(ErrorCodes.EXCEPTION,step("(recur 1)")); - } - - @Test - public void testRecurMultiFn() { - // test function that should exit on recur with value 13 - Context ctx=step("(defn f ([] 13) ([a] (inc (recur))))"); - - assertEquals(13L,evalL(ctx,"(f)")); - assertEquals(13L,evalL(ctx,"(f :foo)")); - assertArityError(step(ctx,"(f 1 2)")); - } - - @Test - public void testTailcall() { - assertEquals(Keywords.FOO,eval("(do (defn f [x] :foo) (defn g [] (tailcall (f 1))) (g))")); - assertEquals(RT.cvm(3L),eval("((fn [] (tailcall (+ 1 2))))")); - - // Undeclared function in tailcall - assertUndeclaredError(step("(do (defn f [x] :foo) (defn g [] (tailcall (h 1))) (g))")); - - // Arity error in tailcall - assertArityError(step("(do (defn g [] :foo) (defn f [x] (tailcall (g 1))) (f 1))")); - - // check we aren't consuming stack, should fail with :JUICE not :DEPTH - assertJuiceError(step("(do (def f (fn [x] (tailcall (f x)))) (f 1))")); - - // tailcall on its own is an :EXCEPTION Error - assertError(ErrorCodes.EXCEPTION,step("(tailcall (count 1))")); - } - - @Test - public void testHalt() { - assertEquals(1L, evalL("(do (halt 1) (assert false))")); - assertNull(eval("(do (halt) (assert false))")); - - // halt should not roll back state changes - { - Context ctx = step("(do (def a 13) (halt 2))"); - assertCVMEquals(2L, ctx.getResult()); - assertEquals(13L, evalL(ctx, "a")); - } - - // Halt should return from a smart contract call but still have state changes - { - Context ctx=step("(def act (deploy '(do (def g :foo) (defn f ^{:callable? true} [] (def g 3) (halt 2) 1))))"); - assertTrue(ctx.getResult() instanceof Address); - assertEquals(Keywords.FOO, eval(ctx,"(lookup act g)")); // initial value of g - ctx=step(ctx,"(call act (f))"); - assertCVMEquals(2L, ctx.getResult()); // halt value returned - assertCVMEquals(3L, eval(ctx,"(lookup act g)")); // g has been updated - } - - assertArityError(step("(halt 1 2)")); - } - - @Test - public void testFail() { - assertAssertError(step("(fail)")); - assertAssertError(step("(fail \"Foo\")")); - assertAssertError(step("(fail :ASSERT \"Foo\")")); - assertAssertError(step("(fail :foo)")); - - assertError(step("(fail 1 :bar)")); - assertCastError(step("(fail :CAST :bar)")); - - - assertAssertError(step("(fail)")); - - { // need to double-step this: can't define macro and use it in the same expression? - Context ctx=step("(defmacro check [condition reaction] `(if (not ~condition) ~reaction))"); - assertAssertError(step(ctx,"(check (= (+ 2 2) 5) (fail \"Laws of arithmetic violated\"))")); - } - - // cannot have null error code - assertArgumentError(step("(fail nil \"Hello\")")); - - assertArityError(step("(fail 1 \"Message\" 3)")); - } - - @Test - public void testFailContract() { - Context ctx=step("(def act (deploy '(do (defn set-and-fail ^{:callable? true} [x] (def foo x) (fail :NOPE (str x))))))"); - Address act=(Address) ctx.getResult(); - assertNotNull(act); - - ctx=step(ctx,"(call act (set-and-fail 100))"); - assertError(Keyword.create("NOPE"),ctx); - - // Foo shouldn't be defined - assertNull(ctx.getAccountStatus(act).getEnvironmentValue(Symbols.FOO)); - } - - @Test - public void testRollback() { - assertEquals(1L, evalL("(do (rollback 1) (assert false))")); - assertEquals(1L, evalL("(do (def a 1) (rollback a) (assert false))")); - - // rollback should roll back state changes - Context ctx = step("(def a 17)"); - ctx = step(ctx, "(do (def a 13) (rollback 2))"); - assertEquals(17L, evalL(ctx, "a")); - } - - @Test - public void testWhen() { - assertNull(eval("(when false 2)")); - assertNull(eval("(when true)")); - assertEquals(Vectors.empty(), eval("(when 2 3 4 5 [])")); - - // TODO: needs to fix / check? - assertArityError(step("(when)")); - } - - @Test - public void testIfLet() { - assertEquals(1L,evalL("(if-let [a 1] a)")); - assertEquals(2L,evalL("(if-let [a true] 2)")); - assertEquals(3L,evalL("(if-let [a []] 3 4)")); - assertEquals(4L,evalL("(if-let [a nil] 3 4)")); - assertEquals(5L,evalL("(if-let [a false] 3 5)")); - - // TODO: fix destructuring examples - //assertEquals(Vectors.of(2L,1L),eval("(if-let [[a b] [1 2]] [b a])")); - //assertNull(eval("(if-let [[a b] nil] [b a])")); - - assertNull(eval("(if-let [a false] 1)")); // null on false branch - - assertArityError(step("(if-let [:foo 1])")); - - // TODO: needs to fix / check? - assertArityError(step("(if-let [a true])")); // no branches - assertArityError(step("(if-let [a true] 1 2 3)")); // too many branches - assertArityError(step("(if-let)")); - assertArityError(step("(if-let [])")); - assertArityError(step("(if-let [foo] 1)")); - } - - @Test - public void testWhenLet() { - assertEquals(1L,evalL("(when-let [a 1] a)")); - assertEquals(2L,evalL("(when-let [a true] 2)")); - assertEquals(3L,evalL("(when-let [a 1] 2 3)")); - - assertNull(eval("(when-let [a true])")); // empty trye branch - assertNull(eval("(when-let [a false] 1)")); // null on false branch - assertNull(eval("(when-let [a false])")); // null on false branch - - assertCompileError(step("(when-let [:foo 1])")); - - // TODO: needs to fix / check? - assertArityError(step("(when-let)")); - assertArityError(step("(when-let [])")); - assertArityError(step("(when-let [foo] 1)")); - } - - @Test - public void testKeyword() { - assertEquals(Keywords.STATE, eval("(keyword 'state)")); - assertEquals(Keywords.STATE, eval("(keyword (name :state))")); - assertEquals(Keywords.STATE, eval("(keyword (str 'state))")); - assertEquals(Keywords.STATE, eval("(keyword 'state)")); - - // keyword lookups - assertNull(eval("((keyword :foo) nil)")); - - assertArgumentError(step("(keyword (str))")); // too short - - assertCastError(step("(keyword nil)")); - assertCastError(step("(keyword 1)")); - assertCastError(step("(keyword [])")); - - assertArityError(step("(keyword)")); - assertArityError(step("(keyword 1 3)")); - } - - @Test - public void testKeywordAsFunction() { - // lookups in maps - assertNull(eval("(:foo {})")); - assertNull(eval("(:foo {:bar 1} nil)")); - assertEquals(1L,evalL("(:foo {} 1)")); - assertEquals(1L,evalL("(:foo {:foo 1} 2)")); - - // lookups in sets - assertSame(CVMBool.FALSE,eval("(:foo #{})")); - assertEquals(1L,evalL("(:foo #{} 1)")); - assertSame(CVMBool.TRUE,eval("(:foo #{:foo})")); - assertNull(eval("(:foo #{:bar} nil)")); - assertSame(CVMBool.TRUE,eval("(:foo #{:foo} 2)")); - - // lookups in vectors - assertNull(eval("(:foo [])")); - assertNull(eval("(:foo [] nil)")); - assertEquals(1L,evalL("(:foo [1 2 3] 1)")); - - // lookups on nil - assertNull(eval("((keyword :foo) nil)")); - assertNull(eval("(:foo nil)")); - assertEquals(1L,evalL("(:foo nil 1)")); - } - - @Test - public void testName() { - assertEquals("count", evalS("(name :count)")); - assertEquals("count", evalS("(name 'count)")); - assertEquals("foo", evalS("(name \"foo\")")); - - // should extract symbol name - assertEquals("bar", evalS("(name 'bar)")); - - // longer strings OK for name - assertEquals("duicgidvgefiucefiuvfeiuvefiuvgifegvfuievgiuefgviuefgviufegvieufgviuefvgevevgi", evalS("(name \"duicgidvgefiucefiuvfeiuvefiuvgifegvfuievgiuefgviuefgviufegvieufgviuefvgevevgi\")")); - - assertCastError(step("(name nil)")); - assertCastError(step("(name [])")); - assertCastError(step("(name 12)")); - - assertArityError(step("(name)")); - assertArityError(step("(name 1 3)")); - } - - @Test - public void testSymbol() { - assertEquals(Symbols.COUNT, eval("(symbol :count)")); - assertEquals(Symbols.COUNT, eval("(symbol (name 'count))")); - assertEquals(Symbols.COUNT, eval("(symbol (str 'count))")); - assertEquals(Symbols.COUNT, eval("(symbol (name :count))")); - assertEquals(Symbols.COUNT, eval("(symbol (name \"count\"))")); - - // too short or too long results in ARGUMENT error - assertArgumentError(step("(symbol (str))")); - assertArgumentError( - step("(symbol \"duicgidvgefiucefiuvfeiuvefiuvgifegvfuievgiuefgviuefgviufegvieufgviuefvgevevgi\")")); - - assertCastError(step("(symbol nil)")); - assertCastError(step("(symbol [])")); - - assertArityError(step("(symbol)")); - assertArityError(step("(symbol 1 2 3)")); - } - - @Test - public void testImport() { - Context
ctx = step("(def lib (deploy '(do (def foo 100))))"); - Address libAddress=ctx.getResult(); - - { // tests with a typical import - Context ctx2=step(ctx,"(import ~lib :as mylib)"); - assertEquals(libAddress,ctx2.getResult()); - - assertEquals(100L, evalL(ctx2, "mylib/foo")); - assertUndeclaredError(step(ctx2, "mylib/bar")); - assertTrue(evalB(ctx2,"(map? (lookup-meta mylib 'foo))")); - } - - { // test deploy and CNS import in a single form. See #107 - Context ctx2=step(ctx,"(do (let [addr (deploy nil)] (call *registry* (cns-update 'foo addr)) (import foo :as foo2)))"); - assertNotError(ctx2); - } - - assertArityError(step(ctx,"(import)")); - assertArityError(step(ctx,"(import ~lib)")); - assertArityError(step(ctx,"(import ~lib :as)")); - assertArityError(step(ctx,"(import ~lib :as mylib :blah)")); - } - - @Test - public void testImportCore() { - Context ctx = step("(import convex.core :as cc)"); - assertNotError(ctx); - assertEquals(eval(ctx,"count"),eval(ctx,"cc/count")); - } - - - @Test - public void testLookup() { - assertSame(Core.COUNT, eval("(lookup count)")); - assertSame(Core.COUNT, eval("(lookup *address* count)")); - assertSame(Core.COUNT, eval("(lookup "+Init.CORE_ADDRESS+" count)")); - - // Lookups after def - assertEquals(1L,evalL("(do (def foo 1) (lookup foo))")); - assertEquals(1L,evalL("(do (def foo 1) (lookup *address* foo))")); - - // UNDECLARED if not declared - assertUndeclaredError(step("(lookup non-existent-symbol)")); - - // NOBODY for lookups in non-existent environment - assertNobodyError(step("(lookup #77777777 count)")); - assertNobodyError(step("(do (def foo 1) (lookup #66666666 foo))")); - - // COMPILE Errors for bad symbols - assertCompileError(step("(lookup :count)")); - assertCompileError(step("(lookup \"count\")")); - assertCompileError(step("(lookup :non-existent-symbol)")); - assertCompileError( - step("(lookup \"cdiubcidciuecgieufgvuifeviufegviufeviuefbviufegviufevguiefvgfiuevgeufigv\")")); - assertCompileError(step("(lookup nil)")); - assertCompileError(step("(lookup 10)")); - assertCompileError(step("(lookup [])")); - - // CAST Errors for bad Addresses - assertCastError(step("(lookup 8 count)")); - assertCastError(step("(lookup :foo count)")); - - assertArityError(step("(lookup)")); - assertArityError(step("(lookup 1 2 3)")); - } - - @Test - public void testLookupSyntax() { - AHashMap countMeta=Core.METADATA.get(Symbols.COUNT); - assertSame(countMeta, (eval("(lookup-meta 'count)"))); - assertSame(countMeta, (eval("(lookup-meta "+Init.CORE_ADDRESS+ " 'count)"))); - - assertNull(eval("(lookup-meta 'non-existent-symbol)")); - assertNull(eval("(lookup-meta #666666 'count)")); // invalid address - - assertSame(Maps.empty(),eval("(do (def foo 1) (lookup-meta 'foo))")); - assertSame(Maps.empty(),eval("(do (def foo 1) (lookup-meta *address* 'foo))")); - assertNull(eval("(do (def foo 1) (lookup-meta #0 'foo))")); - - // invalid name string (too long) - assertCastError( - step("(lookup-meta \"cdiubcidciuecgieufgvuifeviufegviufeviuefbviufegviufevguiefvgfiuevgeufigv\")")); - - // bad symbols - assertCastError(step("(lookup-meta count)")); - assertCastError(step("(lookup-meta nil)")); - assertCastError(step("(lookup-meta 10)")); - assertCastError(step("(lookup-meta [])")); - - // Bad addresses - assertCastError(step("(lookup-meta :foo 'bar)")); - assertCastError(step("(lookup-meta 8 'count)")); - - assertArityError(step("(lookup-meta)")); - assertArityError(step("(lookup-meta 1 2 3)")); - } - - @Test - public void testEmpty() { - assertNull(eval("(empty nil)")); - assertSame(Lists.empty(), eval("(empty (list 1 2))")); - assertSame(Maps.empty(), eval("(empty {1 2 3 4})")); - assertSame(Vectors.empty(), eval("(empty [1 2 3])")); - assertSame(Sets.empty(), eval("(empty #{1 2})")); - - assertCastError(step("(empty 1)")); - assertCastError(step("(empty :foo)")); - assertArityError(step("(empty)")); - assertArityError(step("(empty [1] [2])")); - } - - @Test - public void testMapAsFunction() { - assertEquals(1L, evalL("({2 1 1 2} 2)")); - assertNull(eval("({2 1 1 2} 3)")); - assertNull(eval("({} 3)")); - - // fall-through behaviour - assertEquals(10L, evalL("({2 1 1 2} 5 10)")); - assertNull(eval("({} 1 nil)")); - - // bad arity - assertArityError(step("({})")); - assertArityError(step("({} 1 2 3 4)")); - } - - @Test - public void testVectorAsFunction() { - assertEquals(5L, evalL("([1 3 5 7] 2)")); - - assertEquals(5L, evalL("([1 3 5 7] (byte 2))")); // TODO: is this sane? Implicit cast to Long is OK? - - // bounds checks get applied - assertBoundsError(step("([] 0)")); - assertBoundsError(step("([1 2 3] -1)")); - assertBoundsError(step("([1 2 3] 3)")); - - // Bad index types - assertCastError(step("([] nil)")); - assertCastError(step("([] :foo)")); - - // bad arity - assertArityError(step("([])")); - assertArityError(step("([] 1 2 3 4)")); - } - - @Test - public void testListAsFunction() { - assertEquals(5L, evalL("('(1 3 5 7) 2)")); - - // bounds checks get applied - assertBoundsError(step("(() 0)")); - assertBoundsError(step("('(1 2 3) -1)")); - assertBoundsError(step("('(1 2 3) 3)")); - - // cast error - assertCastError(step("(() nil)")); - assertCastError(step("(() :foo)")); - assertCastError(step("(() {})")); - - - // bad arity - assertArityError(step("(())")); - assertArityError(step("(() 1 2 3 4)")); - } - - @Test - public void testApply() { - // Basic data structure application - assertSame(Maps.empty(),eval("(apply assoc [nil])")); - assertSame(Vectors.empty(), eval("(apply vector ())")); - assertSame(BlobMaps.empty(), eval("(apply blob-map ())")); - assertSame(Lists.empty(), eval("(apply list [])")); - - assertEquals("foo", evalS("(apply str [\\f \\o \\o])")); - - assertEquals(10L,evalL("(apply + 1 2 [3 4])")); - assertEquals(3L,evalL("(apply + 1 2 nil)")); - - assertEquals(Vectors.of(1L, 2L, 3L, 4L), eval("(apply vector 1 2 (list 3 4))")); - assertEquals(List.of(1L, 2L, 3L, 4L), eval("(apply list 1 2 [3 4])")); - assertEquals(List.of(1L, 2L), eval("(apply list 1 2 nil)")); - - // Bad function type - assertCastError(step("(apply 666 1 2 [3 4])")); - - // Keyword works as a function lookup, but wrong arity (#79) - assertArityError(step("(apply :n 1 2 [3 4])")); - - // Insufficient args to apply itself - assertArityError(step("(apply)")); - assertArityError(step("(apply vector)")); - - // Arity failure before cast - assertArityError(step("(apply 666)")); - - // Insufficient args to applied function - assertArityError(step("(apply assoc nil)")); - - // Cast error if not applied to collection - assertCastError(step("(apply inc 1)")); - assertCastError(step("(apply inc :foo)")); - - // not a sequential collection - assertCastError(step("(apply + 1 2 {})")); - } - - - @Test - public void testNonExistentAccountBalance() { - // Address that doesn't exist address, shouldn't have any balance initially - long addr=7777777777L; - assertNull(eval("(let [a (address "+addr+")] (balance a))")); - } - - @Test - public void testBalance() { - - // hero balance, should reflect cost of initial juice - Long expectedHeroBalance = HERO_BALANCE; - assertEquals(expectedHeroBalance, evalL("(let [a (address " + HERO + ")] (balance a))")); - - // someone else's balance - Long expectedVillainBalance = VILLAIN_BALANCE; - assertEquals(expectedVillainBalance, evalL("(let [a (address " + VILLAIN + ")] (balance a))")); - - assertCastError(step("(balance nil)")); - assertCastError(step("(balance 0x00)")); - assertCastError(step("(balance :foo)")); - - assertArityError(step("(balance)")); - assertArityError(step("(balance 1 2)")); - } - - @Test - public void testCreateAccount() { - Context
ctx=step("(create-account 0x817934590c058ee5b7f1265053eeb4cf77b869e14c33e7f85b2babc85d672bbc)"); - Address addr=ctx.getResult(); - assertEquals(addr.longValue()+1,ctx.getState().getAccounts().count()); // should be last Address added - - assertCastError(step("(create-account :foo)")); - assertCastError(step("(create-account 1)")); - assertCastError(step("(create-account nil)")); - assertCastError(step("(create-account #666666)")); - - assertArityError(step("(create-account)")); - assertArityError(step("(create-account 1 2)")); - } - - @Test - public void testAccept() { - assertEquals(0L, evalL("(accept 0)")); - assertEquals(0L, evalL("(accept *offer*)")); // offer should be initially zero - assertEquals(0L, evalL("(accept (byte 0))")); // byte should widen to Long - - // accepting non-integer value -> CAST error - assertCastError(step("(accept :foo)")); - assertCastError(step("(accept :foo)")); - assertCastError(step("(accept 0.3)")); - - // accepting negative -> ARGUMENT error - assertArgumentError(step("(accept -1)")); - - // accepting more than is offered -> STATE error - assertStateError(step("(accept 1)")); - - assertArityError(step("(accept)")); - assertArityError(step("(accept 1 2)")); - } - - @Test - public void testAcceptInActor() { - Context ctx=context(); - ctx=step(ctx,"(def act (deploy '(do (defn receive-coin ^{:callable? true} [sender amount data] (accept amount)) (defn echo-offer ^{:callable? true} [] *offer*))))"); - - ctx=step(ctx,"(transfer act 100)"); - assertEquals(100L, (long)RT.jvm(ctx.getResult())); - assertEquals(100L,evalL(ctx,"(balance act)")); - assertEquals(999L,evalL(ctx,"(call act 999 (echo-offer))")); - - // send via contract call - ctx=step(ctx,"(call act 666 (receive-coin *address* 350 nil))"); - assertEquals(350L, (long)RT.jvm(ctx.getResult())); - assertEquals(450L,evalL(ctx,"(balance act)")); - - } - - @Test - public void testCall() { - Context
ctx = step("(def ctr (deploy '(do (defn foo ^{:callable? true} [] :bar))))"); - - assertEquals(Keywords.BAR,eval(ctx,"(call ctr (foo))")); // regular call - assertEquals(Keywords.BAR,eval(ctx,"(call ctr 100 (foo))")); // call with offer - - assertArityError(step(ctx, "(call)")); - assertArityError(step(ctx, "(call 12)")); - - assertCastError(step(ctx, "(call ctr :foo (bad-fn 1 2))")); // cast fail on offered value - assertStateError(step(ctx, "(call ctr 12 (bad-fn 1 2))")); // bad function - - // bad format for call - assertCompileError(step(ctx,"(call ctr foo)")); - assertCompileError(step(ctx,"(call ctr [foo])")); // not a list - assertCompileError(step(ctx,"(call ctr #{some-func 42})")); // See #135 - - assertNobodyError(step(ctx, "(call #666666 12 (bad-fn 1 2))")); // bad actor - assertArgumentError(step(ctx, "(call ctr -12 (bad-fn 1 2))")); // negative offer - - // bad actor takes precedence over bad offer - assertNobodyError(step(ctx, "(call #666666 -12 (bad-fn 1 2))")); - - } - - @Test - public void testCallSelf() { - Context
ctx = step("(def ctr (deploy '(do (defn foo ^{:callable? true} [] (call *address* (bar))) (defn bar ^{:callable? true} [] (= *address* *caller*)))))"); - Address actor=ctx.getResult(); - - assertTrue(evalB(ctx, "(call ctr (foo))")); // nested call to same actor - assertFalse(evalB(ctx, "(call ctr (bar))")); // call from hero only - - assertEquals(Sets.of(Symbols.FOO,Symbols.BAR),ctx.getAccountStatus(actor).getCallableFunctions()); - } - - @Test - public void testCallables() { - assertSame(Sets.empty(),context().getAccountStatus().getCallableFunctions()); - } - - - @Test - public void testCallStar() { - Context
ctx = step("(def ctr (deploy '(do :foo (defn f ^{:callable? true} [x] (inc x)) )))"); - - assertEquals(9L,evalL(ctx, "(call* ctr 0 'f 8)")); - assertCastError(step(ctx, "(call* ctr 0 :f 8)")); // cast fail on keyword function name - - assertArityError(step(ctx, "(call*)")); - assertArityError(step(ctx, "(call* 12)")); - assertArityError(step(ctx, "(call* 1 2)")); // no function - - assertCastError(step(ctx, "(call* ctr :foo 'bad-fn 1 2)")); // cast fail on offered value - assertStateError(step(ctx, "(call* ctr 12 'bad-fn 1 2)")); // bad function - } - - @Test - public void testDeploy() { - Context
ctx = step("(def ctr (deploy '(fn [] :foo :bar)))"); - Address ca = ctx.getResult(); - assertNotNull(ca); - AccountStatus as = ctx.getAccountStatus(ca); - assertNotNull(as); - assertEquals(ca, eval(ctx, "ctr")); // defined address in environment - - // initial deployed state - assertEquals(0L, as.getBalance()); - - // double-deploy should get different addresses - assertFalse(evalB("(let [cfn '(do 1)] (= (deploy cfn) (deploy cfn)))")); - } - - @Test - public void testActorQ() { - Context
ctx = step("(def ctr (deploy '(fn [] :foo :bar)))"); - Address ctr=ctx.getResult(); - - assertTrue(evalB(ctx,"(actor? ctr)")); - - assertTrue(evalB(ctx,"(actor? (address ctr))")); - - // hero address is not an Actor - assertFalse(evalB(ctx,"(actor? *address*)")); - - // Not an Actor Address, even though the given values string refer to one. - assertFalse(evalB(ctx,"(actor? \""+ctr.toHexString()+"\")")); - assertFalse(evalB(ctx,"(actor? 8)")); - - // Above are OK if cast to addresses explicitly - assertTrue(evalB(ctx,"(actor? (address \""+ctr.toHexString()+"\"))")); - assertTrue(evalB(ctx,"(actor? (address 8))")); - - assertFalse(evalB(ctx,"(actor? :foo)")); - assertFalse(evalB(ctx,"(actor? nil)")); - assertFalse(evalB(ctx,"(actor? [ctr])")); - assertFalse(evalB(ctx,"(actor? 'ctr)")); - - // non-existant account is not an actor - assertFalse(evalB(ctx,"(actor? (address 99999999))")); - assertFalse(evalB(ctx,"(actor? #4512)")); - assertFalse(evalB(ctx,"(actor? -1234)")); - - assertArityError(step("(actor?)")); - assertArityError(step("(actor? :foo :bar)")); // ARITY before CAST - - } - - @Test - public void testAccountQ() { - // a new Actor is an account - Context
ctx = step("(def ctr (deploy '(fn [] :foo :bar)))"); - assertTrue(evalB(ctx,"(account? ctr)")); - - // standard actors are accounts - assertTrue(evalB(ctx,"(account? *registry*)")); - - // standard actors are accounts - assertTrue(evalB(ctx,"(account? "+HERO+")")); - - // a fake address - assertFalse(evalB(ctx,"(account? 77777777)")); - - // String with and without hex. See Issue #90 - assertFalse(evalB(ctx,"(account? \"deadbeef\")")); - assertFalse(evalB(ctx,"(account? \"zzz\")")); - - // a blob that is wrong length for an address. See Issue #90 - assertFalse(evalB(ctx,"(account? 0x1234)")); - - // a blob that actually refers to a valid account. But it isn't an Address... - assertFalse(evalB(ctx,"(account? 0x0000000000000008)")); - - // current hero address is an account - assertTrue(evalB(ctx,"(account? *address*)")); - - assertFalse(evalB("(account? :foo)")); - assertFalse(evalB("(account? nil)")); - assertFalse(evalB("(account? [])")); - assertFalse(evalB("(account? 'foo)")); - - assertArityError(step("(account?)")); - assertArityError(step("(account? 1 2)")); // ARITY before CAST - } - - @Test - public void testAccount() { - // a new Actor is an account - Context
ctx = step("(def ctr (deploy '(fn [] :foo :bar)))"); - AccountStatus as=eval(ctx,"(account ctr)"); - assertNotNull(as); - - // standard actors are accounts - assertTrue(eval("(account *registry*)") instanceof AccountStatus); - - // a non-existent address returns null - assertNull(eval(ctx,"(account #77777777)")); - - // current address is an account, and its balance is correct - assertTrue(evalB("(= *balance* (:balance (account *address*)))")); - - // invalid addresses - assertCastError(step("(account nil)")); - assertCastError(step("(account 8)")); - assertCastError(step("(account :foo)")); - assertCastError(step("(account [])")); - assertCastError(step("(account 'foo)")); - - assertArityError(step("(account)")); - assertArityError(step("(account 1 2)")); // ARITY before CAST - } - - @Test - public void testSetKey() { - Context ctx=context(); - - ctx=step(ctx,"(set-key 0x0000000000000000000000000000000000000000000000000000000000000000)"); - assertEquals(AccountKey.ZERO,ctx.getResult()); - assertEquals(AccountKey.ZERO,eval(ctx,"*key*")); - - ctx=step(ctx,"(set-key nil)"); - assertNull(ctx.getResult()); - assertNull(eval(ctx,"*key*")); - - ctx=step(ctx,"(set-key "+InitTest.HERO_KEY+")"); - assertEquals(InitTest.HERO_KEY,ctx.getResult()); - assertEquals(InitTest.HERO_KEY,eval(ctx,"*key*")); - - assertEquals(true, evalB("(do " - + " (def k 0x0000000000000000000000000000000000000000000000000000000000000000)" - + " (def a (deploy `(set-key ~k)))" - + " (= k (:key (account a))))")); - } - - @Test - public void testSetAllowance() { - - // zero price for unchanged allowance - assertEquals(0L, evalL("(set-memory *memory*)")); - - // sell whole allowance, should zero memory - assertEquals(0L, evalL("(do (set-memory 0) *memory*)")); - - // buy allowance reduces balance - assertTrue(evalL("(let [b *balance*] (set-memory (inc *memory*)) (- *balance* b))")<0); - - // sell allowance increases balance - assertTrue(evalL("(let [b *balance*] (set-memory (dec *memory*)) (- *balance* b))")>0); - - // trying to buy too much is a funds error - assertFundsError(step("(set-memory 1000000000000000000)")); - - // trying to set memory negative is an ARGUMENT error - assertArgumentError(step("(set-memory -1)")); - assertArgumentError(step("(set-memory -10000000)")); - assertArgumentError(step("(set-memory "+Long.MIN_VALUE+")")); - - assertCastError(step("(set-memory :foo)")); - assertCastError(step("(set-memory nil)")); - - assertArityError(step("(set-memory)")); - assertArityError(step("(set-memory 1 2)")); - - } - - @Test - public void testTransferMemory() { - long ALL=Constants.INITIAL_ACCOUNT_ALLOWANCE; - assertEquals(ALL, evalL(Symbols.STAR_MEMORY.toString())); - - { - Context ctx=step("(transfer-memory *address* 1337)"); - assertEquals(1337L, ctx.getResult().longValue()); - assertEquals(ALL, ctx.getAccountStatus(HERO).getMemory()); - } - - assertEquals(ALL-1337, step("(transfer-memory "+VILLAIN+" 1337)").getAccountStatus(HERO).getMemory()); - - assertEquals(0L, step("(transfer-memory "+VILLAIN+" "+ALL+")").getAccountStatus(HERO).getMemory()); - - assertArgumentError(step("(transfer-memory *address* -1000)")); - assertNobodyError(step("(transfer-memory #88888888 0)")); - - assertMemoryError(step("(transfer-memory *address* (+ 1 "+ALL+"))")); - - // check bad arg types - assertCastError(step("(transfer-memory -1000 1000)")); - assertCastError(step("(transfer-memory *address* :foo)")); - - // check bad arities - assertArityError(step("(transfer-memory -1000)")); - assertArityError(step("(transfer-memory)")); - assertArityError(step("(transfer-memory *address* 100 100)")); - - } - - @Test - public void testTransferToActor() { - // SECURITY: be careful with these tests - Address CORE=Init.CORE_ADDRESS; - - // should fail transferring to an account with no receive-coins export - assertStateError(step("(transfer "+CORE+" 1337)")); - - { // transfer to an Actor that accepts everything - Context ctx=step("(deploy '(do (defn receive-coin ^{:callable? true} [sender amount data] (accept amount))))"); - Address receiver=(Address) ctx.getResult(); - - ctx=step(ctx,"(transfer "+receiver.toString()+" 100)"); - assertCVMEquals(100L,ctx.getResult()); - assertCVMEquals(100L,ctx.getBalance(receiver)); - } - - { // transfer to an Actor that accepts nothing - Context ctx=step("(deploy '(do (defn receive-coin ^{:callable? true} [sender amount data] (accept 0))))"); - Address receiver=(Address) ctx.getResult(); - - ctx=step(ctx,"(transfer "+receiver.toString()+" 100)"); - assertCVMEquals(0L,ctx.getResult()); - assertCVMEquals(0L,ctx.getBalance(receiver)); - } - - { // transfer to an Actor that accepts half - Context ctx=step("(deploy '(do (defn receive-coin ^{:callable? true} [sender amount data] (accept (long (/ amount 2))))))"); - Address receiver=(Address) ctx.getResult(); - - // should be OK with a Blob Address - ctx=step(ctx,"(transfer "+receiver+" 100)"); - assertCVMEquals(50L,ctx.getResult()); - assertCVMEquals(50L,ctx.getBalance(receiver)); - } - - - } - - @Test - public void testTransfer() { - // SECURITY: don't mess with these tests - - // balance at start of transaction - long BAL = HERO_BALANCE; - - // transfer to self. Note juice already accounted for in context. - assertEquals(1337L, evalL("(transfer *address* 1337)")); // should return transfer amount - assertEquals(BAL, step("(transfer *address* 1337)").getBalance(HERO)); - - // transfers to an address that doesn't exist - { - Context nc1=step("(transfer (address 666666) 1337)"); - assertNobodyError(nc1); - } - - - // String representing a new User Address - Context
ctx=step("(create-account "+InitTest.HERO_KEYPAIR.getAccountKey()+")"); - Address naddr=ctx.getResult(); - - // transfers to a new address - { - Context nc1=step(ctx,"(transfer "+naddr+" 1337)"); - assertCVMEquals(1337L, nc1.getResult()); - assertEquals(BAL - 1337,nc1.getBalance(HERO)); - assertEquals(1337L, evalL(nc1,"(balance "+naddr+")")); - } - - assertTrue(() -> evalB(ctx,"(let [a "+naddr+"]" - + " (not (= *balance* (transfer a 1337))))")); - - // transfer it all! - assertEquals(0L,step(ctx,"(transfer "+naddr+" *balance*)").getBalance(HERO)); - - // Should never be possible to transfer negative amounts - assertArgumentError(step("(transfer *address* -1000)")); - assertArgumentError(step("(transfer "+naddr+" -1)")); - - // Long.MAX_VALUE is too big for an Amount - assertArgumentError(step("(transfer *address* 9223372036854775807)")); // Long.MAX_VALUE - - assertFundsError(step("(transfer *address* 999999999999999999)")); - - assertCastError(step("(transfer :foo 1)")); - assertCastError(step("(transfer *address* :foo)")); - - assertArityError(step("(transfer)")); - assertArityError(step("(transfer 1)")); - assertArityError(step("(transfer 1 2 3)")); - } - - @Test - public void testStake() { - Context ctx=step(context(),"(def my-peer 0x"+InitTest.FIRST_PEER_KEY.toHexString()+")"); - AccountKey MY_PEER=InitTest.FIRST_PEER_KEY; - long PS=ctx.getState().getPeer(InitTest.FIRST_PEER_KEY).getPeerStake(); - - { - // simple case of staking 1000000 on first peer of the realm - Context rc=step(ctx,"(stake my-peer 1000000)"); - assertNotError(rc); - assertEquals(PS+1000000,rc.getState().getPeer(MY_PEER).getTotalStake()); - assertEquals(1000000,rc.getState().getPeer(MY_PEER).getDelegatedStake()); - assertEquals(Constants.MAX_SUPPLY, rc.getState().computeTotalFunds()); - } - - // staking on an account key that isn't a peer - assertStateError(step(ctx,"(stake 0x1234567812345678123456781234567812345678123456781234567812345678 1234)")); - - // staking on an address - assertCastError(step(ctx,"(stake *address* 1234)")); - - // bad arg types - assertCastError(step(ctx,"(stake :foo 1234)")); - assertCastError(step(ctx,"(stake my-peer :foo)")); - assertCastError(step(ctx,"(stake my-peer nil)")); - - assertArityError(step(ctx,"(stake my-peer)")); - assertArityError(step(ctx,"(stake my-peer 1000 :foo)")); - } - - @Test - public void testSetPeerData() { - String newHostname = "new_hostname:1234"; - Context ctx=context(); - ctx=ctx.forkWithAddress(InitTest.FIRST_PEER_ADDRESS); - AccountKey peerKey=InitTest.FIRST_PEER_KEY; - ctx=step(ctx,"(def peer-key "+peerKey+")"); - { - // make sure we are using the FIRST_PEER address - ctx=step(ctx,"(set-peer-data peer-key {:url \"" + newHostname + "\"})"); - assertNotError(ctx); - assertEquals(newHostname,ctx.getState().getPeer(InitTest.FIRST_PEER_KEY).getHostname().toString()); - ctx=step(ctx,"(set-peer-data peer-key {})"); - assertNotError(ctx); - // no change to data - assertEquals(newHostname,ctx.getState().getPeer(InitTest.FIRST_PEER_KEY).getHostname().toString()); - } - - // Try to hijack with an account that isn't the first Peer - ctx=ctx.forkWithAddress(HERO.offset(2)); - { - newHostname = "set-key-hijack"; - ctx=step(ctx,"(do (set-key "+peerKey+") (set-peer-data "+peerKey+" {:url \"" + newHostname + "\"}))"); - assertStateError(ctx); - } - - ctx=ctx.forkWithAddress(InitTest.FIRST_PEER_ADDRESS); - assertCastError(step(ctx,"(set-peer-data peer-key 0x1234567812345678123456781234567812345678123456781234567812345678)")); - assertCastError(step(ctx,"(set-peer-data peer-key :bad-key)")); - assertCastError(step(ctx,"(set-peer-data 12 {})")); - assertCastError(step(ctx,"(set-peer-data nil {})")); - - assertArityError(step(ctx,"(set-peer-data)")); - assertArityError(step(ctx,"(set-peer-data peer-key)")); - assertArityError(step(ctx,"(set-peer-data peer-key {:url \"test\" :bad-key 1234} 2)")); - } - - @Test - public void testCreatePeer() { - // Kep Pair for new Peer - AKeyPair kp=AKeyPair.createSeeded(4583763); - - Context ctx=step(context(),"(def hero-peer 0x"+kp.getAccountKey().toHexString()+")"); - ctx=ctx.forkWithAddress(InitTest.HERO); - - Context peerCTX = step(ctx,"(create-peer hero-peer 1000)"); - // create a peer based on the HERO address and public key - assertNotError(peerCTX); - - // create a peer again on the same peer key and address - assertError(step(peerCTX,"(create-peer hero-peer 1000)")); - - // create a new peer with zero stake - assertError(step(ctx,"(create-peer hero-peer 0)")); - - // creating a peer on an account key that isn't the hero account key - // TODO: what should happen here? - //assertArgumentError(step(ctx,"(create-peer 0x1234567812345678123456781234567812345678123456781234567812345678 1234)")); - - // creating a peer with invalid account key - assertCastError(step(ctx,"(create-peer *address* 1234)")); - - // bad arg types - assertCastError(step(ctx,"(create-peer :foo 1234)")); - assertCastError(step(ctx,"(create-peer hero-peer :foo)")); - assertCastError(step(ctx,"(create-peer hero-peer nil)")); - - assertArityError(step(ctx,"(create-peer hero-peer)")); - assertArityError(step(ctx,"(create-peer hero-peer 1000 :foo)")); - } - - @Test - public void testCreatePeerRegression() { - assertNotError(step("(create-peer 0x42ae93b185bd2ba64fd9b0304fec81a4d4809221a5b68de4da041b48c85bcc2e (dec *balance*))")); - assertNotError(step("(create-peer 0x42ae93b185bd2ba64fd9b0304fec81a4d4809221a5b68de4da041b48c85bcc2e *balance*)")); - assertFundsError(step("(create-peer 0x42ae93b185bd2ba64fd9b0304fec81a4d4809221a5b68de4da041b48c85bcc2e (inc *balance*))")); - - } - - @Test - public void testNumericComparisons() { - assertFalse(evalB("(== 1 2)")); - assertFalse(evalB("(== 1.0 2.0)")); - - assertTrue(evalB("(== 3 3)")); - assertTrue(evalB("(== 0.0 -0.0)")); // IEE754 defines as equals - assertFalse(evalB("(= 0.0 -0.0)")); // Not identical values - assertTrue(evalB("(== 7.0000 7.0)")); - assertTrue(evalB("(== -1.00E0 -1.0)")); - assertTrue(evalB("(== 7 7.0)")); - assertTrue(evalB("(== 7.0 7)")); - - assertTrue(evalB("(<)")); - assertTrue(evalB("(>)")); - assertTrue(evalB("(< 1 2)")); - assertTrue(evalB("(<= 1 2)")); - assertTrue(evalB("(<= 1 2 6)")); - assertFalse(evalB("(< 2 2)")); - assertFalse(evalB("(< 3 2)")); - assertTrue(evalB("(<= 2.0 2.0)")); - assertTrue(evalB("(>= 2.0 2.0)")); - assertFalse(evalB("(> 1 2)")); - assertFalse(evalB("(> 3.0 3.0)")); - assertFalse(evalB("(> 3.0 1.0 7.0)")); - assertTrue(evalB("(>= 3.0 3.0)")); - - // assertTrue(evalB("(>= \\b \\a)")); // TODO: do we want this to work? - - // juice should go down in order of evaluation - assertTrue(evalB("(> *juice* *juice* *juice*)")); - - assertCastError(step("(> :foo)")); - assertCastError(step("(> :foo :bar)")); - assertCastError(step("(> [] [1])")); - } - - @Test - public void testMin() { - assertEquals(1L, evalL("(min 1 2 3 4)")); - assertEquals(7L, evalL("(min 7)")); - assertEquals(2L, evalL("(min 4 3 2)")); - assertEquals(1L, evalL("(min 1 2)")); - - assertEquals(1L, evalL("("+Init.CORE_ADDRESS+"/min 1 2)")); - - assertEquals(1.0, evalD("(min 2.0 1.0 3.0)")); - assertEquals(CVMDouble.NaN, eval("(min 2.0 ##NaN -0.0 ##Inf)")); - assertEquals(CVMDouble.NaN, eval("(min ##NaN)")); - - // TODO: Figure out how this should behave. See issue https://github.com/Convex-Dev/convex/issues/99 - // assertEquals(CVMLong.ONE, eval("(min ##NaN 1 ##NaN)")); - - assertCastError(step("(min true)")); - assertCastError(step("(min \\c)")); - assertCastError(step("(min ##NaN true)")); - assertCastError(step("(min true ##NaN)")); - - - // #NaNs should get ignored - assertEquals(CVMDouble.NaN,eval("(min ##NaN 42)")); - assertEquals(CVMDouble.NaN,eval("(min 42 ##NaN)")); - - assertArityError(step("(min)")); - } - - @Test - public void testMax() { - assertEquals(4L, evalL("(max 1 2 3 4)")); - assertEquals(CVMDouble.NaN, eval("(max 1 ##-Inf 3 ##NaN 4)")); - assertEquals(7L, evalL("(max 7)")); - assertEquals(4.0, evalD("(max 4.0 3 2)")); - assertEquals(CVMDouble.NaN, eval("(max 1 2.5 ##NaN)")); - - assertArityError(step("(max)")); - } - - @Test - public void testPow() { - assertEquals(4.0, evalD("(pow 2 2)")); - - assertCastError(step("(pow :a 7)")); - assertCastError(step("(pow 7 :a)")); - - assertArityError(step("(pow)")); - assertArityError(step("(pow 1)")); - assertArityError(step("(pow 1 2 3)")); - } - - @Test - public void testQuot() { - assertEquals(0L, evalL("(quot 4 10)")); - assertEquals(2L, evalL("(quot 10 4)")); - assertEquals(-2L, evalL("(quot -10 4)")); - - assertCastError(step("(quot :a 7)")); - assertCastError(step("(quot 7 nil)")); - - assertArityError(step("(quot)")); - assertArityError(step("(quot 1)")); - assertArityError(step("(quot 1 2 3)")); - } - - @Test - public void testMod() { - assertEquals(4L, evalL("(mod 4 10)")); - assertEquals(4L, evalL("(mod 14 10)")); - assertEquals(6L, evalL("(mod -1 7)")); - assertEquals(0L, evalL("(mod 7 7)")); - assertEquals(0L, evalL("(mod 0 -1)")); - - assertEquals(6L, evalL("(mod -1 -7)")); - - assertArgumentError(step("(mod 10 0)")); - - assertCastError(step("(mod :a 7)")); - assertCastError(step("(mod 7 nil)")); - - assertArityError(step("(mod)")); - assertArityError(step("(mod 1)")); - assertArityError(step("(mod 1 2 3)")); - } - - @Test - public void testRem() { - assertEquals(4L, evalL("(rem 4 10)")); - assertEquals(4L, evalL("(rem 14 10)")); - assertEquals(-1L, evalL("(rem -1 7)")); - assertEquals(0L, evalL("(rem 7 7)")); - assertEquals(0L, evalL("(rem 0 -1)")); - - assertEquals(-1L, evalL("(rem -1 -7)")); - - assertArgumentError(step("(rem 10 0)")); - - assertCastError(step("(rem :a 7)")); - assertCastError(step("(rem 7 nil)")); - - assertArityError(step("(rem)")); - assertArityError(step("(rem 1)")); - assertArityError(step("(rem 1 2 3)")); - } - - @Test - public void testExp() { - assertEquals(1.0, evalD("(exp 0)")); - assertEquals(1.0, evalD("(exp -0)")); - assertEquals(StrictMath.exp(1.0), evalD("(exp 1)")); - assertEquals(0.0, evalD("(exp (/ -1 0))")); - assertEquals(Double.POSITIVE_INFINITY, evalD("(exp (/ 1 0))")); - - assertCastError(step("(exp :a)")); - assertCastError(step("(exp #3)")); - assertCastError(step("(exp nil)")); - - assertArityError(step("(exp)")); - assertArityError(step("(exp 1 2)")); - } - - @Test - public void testHash() { - assertEquals(Hash.fromHex("a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"),eval("(hash 0x)")); - - assertEquals(Hash.NULL_HASH, eval("(hash (encoding nil))")); - assertEquals(Hash.TRUE_HASH, eval("(hash (encoding true))")); - assertEquals(Maps.empty().getHash(), eval("(hash (encoding {}))")); - - assertTrue(evalB("(= (hash 0x12) (hash 0x12))")); - assertTrue(evalB("(blob? (hash (encoding 42)))")); // Should be a Blob - - assertArityError(step("(hash)")); - assertArityError(step("(hash nil nil)")); - } - - @Test - public void testCount() { - assertEquals(0L, evalL("(count nil)")); - assertEquals(0L, evalL("(count [])")); - assertEquals(0L, evalL("(count ())")); - assertEquals(0L, evalL("(count 0x)")); - assertEquals(0L, evalL("(count \"\")")); - assertEquals(2L, evalL("(count (list :foo :bar))")); - assertEquals(2L, evalL("(count #{1 2 2})")); - assertEquals(3L, evalL("(count [1 2 3])")); - assertEquals(4L, evalL("(count 0xcafebabe)")); - - // Count of a map is the number of entries - assertEquals(2L, evalL("(count {1 2 2 3})")); - - // non-countable things fail with CAST - assertCastError(step("(count 1)")); - assertCastError(step("(count :foo)")); - - assertArityError(step("(count)")); - assertArityError(step("(count 1 2)")); - } - - @Test - public void testCompile() { - assertEquals(Constant.of(1L), eval("(compile 1)")); - - assertEquals(Constant.of(1L), eval("(compile 1)")); - assertEquals(Constant.of(null), eval("(compile nil)")); - assertEquals(Invoke.class, eval("(compile '(+ 1 2))").getClass()); - assertEquals(Do.class, eval("(compile '(do a b))").getClass()); - - assertArityError(step("(compile)")); - assertArityError(step("(compile 1 2)")); - assertArityError(step("(if 1)")); - } - - private AVector ALL_PREDICATES = Vectors - .create(Core.ENVIRONMENT.filterValues(e -> e instanceof CorePred).values()); - private AVector ALL_CORE_DEFS = Vectors - .create(Core.ENVIRONMENT.filterValues(e -> e instanceof ICoreDef).values()); - - @Test - public void testPredArity() { - AVector pvals = ALL_PREDICATES; - assertFalse(pvals.isEmpty()); - Context C = context(); - ACell[] a0 = new ACell[0]; - ACell[] a1 = new ACell[1]; - ACell[] a2 = new ACell[2]; - for (ACell p : pvals) { - CorePred pred = (CorePred) p; - assertTrue(RT.isBoolean(pred.invoke(C, a1).getResult()), "Predicate: " + pred); - assertArityError(pred.invoke(C, a0)); - assertArityError(pred.invoke(C, a2)); - } - } - - @Test - public void testCoreDefSymbols() throws BadFormatException { - AVector vals = ALL_CORE_DEFS; - assertFalse(vals.isEmpty()); - for (ACell def : vals) { - Symbol sym = ((ICoreDef)def).getSymbol(); - ACell v=Core.ENVIRONMENT.get(sym); - assertSame(def, v); - - Blob b = Format.encodedBlob(def); - assertSame(def, Format.read(b)); - - AHashMap meta= Core.METADATA.get(sym); - assertNotNull(meta,"Missing metadata for core symbol: "+sym); - ACell dobj=meta.get(Keywords.DOC); - assertNotNull(dobj,"No documentation found for core definition: "+sym); - } - } - - @Test - public void testNilPred() { - assertTrue(evalB("(nil? nil)")); - assertFalse(evalB("(nil? 1)")); - assertFalse(evalB("(nil? [])")); - } - - @Test - public void testListPred() { - assertFalse(evalB("(list? nil)")); - assertFalse(evalB("(list? 1)")); - assertTrue(evalB("(list? '())")); - assertTrue(evalB("(list? '(3 4 5))")); - assertFalse(evalB("(list? [1 2 3])")); - assertFalse(evalB("(list? {1 2})")); - } - - @Test - public void testVectorPred() { - assertFalse(evalB("(vector? nil)")); - assertFalse(evalB("(vector? 1)")); - assertTrue(evalB("(vector? [])")); - assertFalse(evalB("(vector? '(3 4 5))")); - assertTrue(evalB("(vector? [1 2 3])")); - assertTrue(evalB("(vector? (first {1 2 3 4}))")); - assertFalse(evalB("(vector? {1 2})")); - } - - @Test - public void testSetPred() { - assertFalse(evalB("(set? nil)")); - assertFalse(evalB("(set? 1)")); - assertTrue(evalB("(set? #{})")); - assertFalse(evalB("(set? '(3 4 5))")); - assertTrue(evalB("(set? #{1 2 3})")); - assertFalse(evalB("(set? {1 2})")); - } - - @Test - public void testMapPred() { - assertFalse(evalB("(map? nil)")); - assertFalse(evalB("(map? 1)")); - assertTrue(evalB("(map? {})")); - assertFalse(evalB("(map? '(3 4 5))")); - assertTrue(evalB("(map? {1 2 3 4})")); - assertFalse(evalB("(map? #{1 2})")); - } - - @Test - public void testCollPred() { - assertFalse(evalB("(coll? nil)")); - assertFalse(evalB("(coll? 1)")); - assertFalse(evalB("(coll? :foo)")); - assertTrue(evalB("(coll? {})")); - assertTrue(evalB("(coll? [])")); - assertTrue(evalB("(coll? ())")); - assertTrue(evalB("(coll? '())")); - assertTrue(evalB("(coll? #{})")); - assertTrue(evalB("(coll? '(3 4 5))")); - assertTrue(evalB("(coll? [:foo :bar])")); - assertTrue(evalB("(coll? {1 2 3 4})")); - assertTrue(evalB("(coll? #{1 2})")); - } - - @Test - public void testEmptyPred() { - assertTrue(evalB("(empty? nil)")); - assertTrue(evalB("(empty? {})")); - assertTrue(evalB("(empty? [])")); - assertTrue(evalB("(empty? ())")); - assertTrue(evalB("(empty? #{})")); - assertFalse(evalB("(empty? {1 2})")); - assertFalse(evalB("(empty? [ 3])")); - assertFalse(evalB("(empty? '(foo))")); - assertFalse(evalB("(empty? #{[]})")); - } - - @Test - public void testSymbolPred() { - assertTrue(evalB("(symbol? 'foo)")); - assertTrue(evalB("(symbol? (symbol :bar))")); - assertFalse(evalB("(symbol? nil)")); - assertFalse(evalB("(symbol? 1)")); - assertFalse(evalB("(symbol? ['foo])")); - } - - @Test - public void testKeywordPred() { - assertTrue(evalB("(keyword? :foo)")); - assertTrue(evalB("(keyword? (keyword 'bar))")); - assertFalse(evalB("(keyword? nil)")); - assertFalse(evalB("(keyword? 1)")); - assertFalse(evalB("(keyword? [:foo])")); - } - - @Test - public void testAddressPred() { - assertTrue(evalB("(address? *origin*)")); - assertFalse(evalB("(address? nil)")); - assertFalse(evalB("(address? 1)")); - assertFalse(evalB("(address? \"0a1b2c3d\")")); - assertFalse(evalB("(address? (blob *origin*))")); - } - - @Test - public void testBlobPred() { - assertTrue(evalB("(blob? (blob *origin*))")); - assertTrue(evalB("(blob? 0xFF)")); - assertTrue(evalB("(blob? (blob 0x17))")); - assertTrue(evalB("(blob? (hash (encoding *state*)))")); // HAsh - assertTrue(evalB("(blob? *key*)")); // AccountKey - - assertFalse(evalB("(blob? 17)")); - assertFalse(evalB("(blob? nil)")); - assertFalse(evalB("(blob? *address*)")); - } - - @Test - public void testLongPred() { - assertTrue(evalB("(long? 1)")); - assertTrue(evalB("(long? (long *balance*))")); // TODO: is this sane? - assertFalse(evalB("(long? (byte 1))")); - assertFalse(evalB("(long? nil)")); - assertFalse(evalB("(long? 0xFF)")); - assertFalse(evalB("(long? [1 2])")); - } - - @Test - public void testStrPred() { - assertTrue(evalB("(str? (name :foo))")); - assertTrue(evalB("(str? (str :foo))")); - assertTrue(evalB("(str? (str nil))")); - assertFalse(evalB("(str? 1)")); - assertFalse(evalB("(str? nil)")); - } - - @Test - public void testNumberPred() { - assertTrue(evalB("(number? 0)")); - assertTrue(evalB("(number? (byte 0))")); - assertTrue(evalB("(number? 0.5)")); - assertTrue(evalB("(number? ##NaN)")); // Sane? Is numeric double type.... - - assertFalse(evalB("(number? nil)")); - assertFalse(evalB("(number? :foo)")); - assertFalse(evalB("(number? 0xFF)")); - assertFalse(evalB("(number? [1 2])")); - - assertFalse(evalB("(number? true)")); - - } - - @Test - public void testZeroPred() { - assertTrue(evalB("(zero? 0)")); - assertTrue(evalB("(zero? (byte 0))")); - assertTrue(evalB("(zero? 0.0)")); - assertFalse(evalB("(zero? 0.00005)")); - assertFalse(evalB("(zero? 0x00)")); // not numeric! - - assertFalse(0.0 > -0.0); // check we are living in a sane universe - assertTrue(evalB("(zero? -0.0)")); - - assertFalse(evalB("(zero? \\c)")); - - assertFalse(evalB("(zero? nil)")); - assertFalse(evalB("(zero? :foo)")); - assertFalse(evalB("(zero? [1 2])")); - } - - @Test - public void testFn() { - assertEquals(1L,evalL("((fn [] 1))")); - assertEquals(2L,evalL("((fn [x] 2) 1)")); - assertEquals(3L,evalL("((fn [x] 2 3) 1)")); - // TODO: more cases! - - // test closing over lexical scope - assertEquals(3L,evalL("(let [a 3 f (fn [x] a)] (f 0))")); - - // Bad arity fn execution - assertArityError(step("((fn [x] 0))")); - assertArityError(step("((fn [] 0) 1)")); - - // Bad fn forms - assertArityError(step("(fn)")); - assertCompileError(step("(fn 1)")); - assertCompileError(step("(fn {})")); - assertCompileError(step("(fn '())")); - - // fn printing - assertCVMEquals("(fn [x y] 0)",eval("(str (fn [x y] 0))")); - assertCVMEquals("(fn [x y] (do 0 1))",eval("(str (fn [x y] 0 1))")); - - } - - @Test - public void testFnMulti() { - assertEquals(1L,evalL("((fn ([] 1)))")); - assertEquals(2L,evalL("((fn ([x] 2)) 1)")); - - // dispatch by arity - assertEquals(1L,evalL("((fn ([x] 1) ([x y] 2)) 3)")); - assertEquals(2L,evalL("((fn ([x] 1) ([x y] 2)) 3 4)")); - - // first matching impl chosen - assertEquals(1L,evalL("((fn ([x] 1) ([x] 2)) 3)")); - - // variadic match - assertEquals(2L,evalL("((fn ([x] 1) ([x & more] 2)) 3 4 5 6)")); - assertEquals(2L,evalL("((fn ([x] 1) ([x y & more] 2)) 3 4)")); - - // MultiFn printing - assertCVMEquals("(fn ([] 0) ([x] 1))",eval("(str (fn ([]0) ([x] 1) ))")); - - // Issue #193 Error test - assertEquals(Vectors.of(1,2,3),eval("(do (defn f ([[a b] c] [a b c])) (f [1 2] 3))")); - - // arity errors - assertArityError(step("((fn ([x] 1) ([x & more] 2)))")); - assertArityError(step("((fn ([x] 1) ([x y] 2)))")); - assertArityError(step("((fn ([x] 1) ([x y z] 2)) 2 3)")); - assertArityError(step("((fn ([x] 1) ([x y z & more] 2)) 2 3)")); - } - - @Test - public void testFnMultiRecur() { - assertEquals(7L,evalL("((fn ([x] x) ([x y] (recur 7))) 1 2)")); - - assertArityError(step("((fn ([x] x) ([x y] (recur))) 1 2)")); - - assertJuiceError(step("((fn ([x] (recur 3 4)) ([x y] (recur 5))) 1 2)")); - } - - @Test - public void testFnPred() { - assertFalse(evalB("(fn? 0)")); - assertTrue(evalB("(fn? (fn[x] 0))")); - assertFalse(evalB("(fn? {})")); - assertTrue(evalB("(fn? count)")); - assertTrue(evalB("(fn? fn?)")); - assertTrue(evalB("(fn? if)")); - } - - @Test - public void testDef() { - // Def returns defined value - assertEquals(Keywords.FOO, eval("(def v :foo)")); - - // Def establishes mapping in environment - assertEquals(CVMLong.ONE, step("(def foo 1)").getEnvironment().get(Symbols.FOO)); - - // Def creates valid dynamic variables - assertEquals(Vectors.of(2L, 3L), eval("(do (def v [2 3]) v)")); - assertNull(eval("(do (def v nil) v)")); - - // def overwrites existing bindings - assertEquals(Vectors.of(2L, 3L), eval("(do (def v nil) (def v [2 3]) v)")); - assertEquals(Vectors.of(2L, 3L), eval("(do (def count [2 3]) count)")); // overwriting core - - // TODO: are these error types logical? - assertCompileError(step("(def)")); - assertCompileError(step("(def a b c)")); - - assertUndeclaredError(step("(def a b)")); - - assertUndeclaredError(step("(def a a)")); - } - - @Test - public void testDefMeta() { - AHashMap FOOMAP = Maps.of(Keywords.FOO, CVMBool.TRUE); - AHashMap BARMAP = Maps.of(Keywords.BAR, CVMBool.TRUE); - AHashMap FOOBARMAP=FOOMAP.merge(BARMAP); - - // def of simple symbol has empty meta - assertEquals(Maps.empty(), eval("(do (def v 1) (lookup-meta 'v))")); - - // def with a keyword tag - assertEquals(FOOMAP, eval("(do (def ^:foo v 1) (lookup-meta 'v))")); - - // def with a keyword tag on value - assertEquals(FOOMAP, eval("(do (def v ^:foo 1) (lookup-meta 'v))")); - - // def with constructed syntax object shouldn't set metadata - assertEquals(Maps.empty(), eval("(do (def v (syntax 1 {:foo true})) (lookup-meta 'v))")); - - // def with syntax object constructed for symbol inline - // assertEquals(FOOMAP, eval("(do (def ~(syntax 'v {:foo true})) (lookup-meta 'v))")); - - // def without metadata on symbol shouldn't change metadata - assertEquals(FOOMAP, eval("(do (def ^:foo v 1) (def v 2) (lookup-meta 'v))")); - - // def with new metadata should overwrite - assertEquals(BARMAP, eval("(do (def ^:foo v 1) (def ^:bar v 2) (lookup-meta 'v))")); - - // def with metadata on both symbol and value should merge - assertEquals(FOOBARMAP, eval("(do (def ^:foo v ^{:bar true} 1) (lookup-meta 'v))")); - - } - - @Test - public void testDefinedQ() { - assertFalse(evalB("(defined? foobar)")); - - assertTrue(evalB("(do (def foobar [2 3]) (defined? foobar))")); - assertTrue(evalB("(defined? count)")); - - // invalid names - assertCastError(step("(defined? :count)")); // not a Symbol - assertCastError(step("(defined? \"count\")")); // not a Symbol - assertCastError(step("(defined? nil)")); - assertCastError(step("(defined? 1)")); - assertCastError(step("(defined? 0x)")); - - assertArityError(step("(defined?)")); - assertArityError(step("(defined? foo bar)")); - } - - @Test - public void testUndef() { - assertNull(eval("(undef count)")); - assertNull(eval("(undef foo)")); - assertNull(eval("(undef *balance*)")); - assertNull(eval("(undef bar)")); - - assertEquals(Vectors.of(1L, 2L), eval("(do (def a 1) (def v [a 2]) (undef a) v)")); - - assertFalse(evalB("(do (def a 1) (undef a) (defined? a))")); - - assertUndeclaredError(step("(do (def a 1) (undef a) a)")); - - assertArityError(step("(undef a b)")); - assertArityError(step("(undef)")); - } - - @Test - public void testUnquote() { - assertEquals(Vectors.of(1L, 2L), eval("`[1 (unquote (+ 1 1))]")); - assertEquals(Constant.create(CVMLong.create(3L)), comp("(unquote (+ 1 2))")); - - assertCompileError(step("(unquote)")); - assertCompileError(step("(unquote 1 2)")); - } - - @Test - public void testUnquoteError() { - assertUndeclaredError(step("~~foo")); - } - - @Test - public void testDefn() { - assertTrue(evalB("(do (defn f [a] a) (fn? f))")); - assertEquals(Vectors.of(2L, 3L), eval("(do (defn f [a & more] more) (f 1 2 3))")); - - // multiple expressions in body - assertEquals(2L,evalL("(do (defn f [a] 1 2) (f 3))")); - - // arity problems - assertArityError(step("(defn)")); - assertArityError(step("(defn f)")); - - // bad function construction - assertCompileError(step("(defn f b)")); - - } - - @Test - public void testDefnMulti() { - assertEquals(2L,evalL("(do (defn f ([a] 1 2)) (f 3))")); - assertEquals(2L,evalL("(do (defn f ([] 4) ([a] 1 2)) (f 3))")); - - assertArityError(step("(do (defn f ([] nil)) (f 3))")); - } - - @Test - public void testDefExpander() { - Context ctx=step("(defexpander expand-once [x e] (expand x (fn [x e] (syntax x))))"); - - assertEquals(Syntax.of(42L),eval(ctx,"(expand 42 expand-once)")); - } - - @Test - public void testSetBang() { - // set! fails on undeclared values - assertCompileError(step("(set! a 13)")); - assertCompileError(step("(do (set! a 13) a)")); - - // set! works in a function body - assertEquals(35L,evalL("(let [a 13 f (fn [x] (set! a 25) (+ x a))] (f 10))")); - - // set! only works in the scope of the immediate surrounding binding expression - assertEquals(10L,evalL("(let [a 10] (let [] (set! a 13)) a)")); - - // set! binding does not escape current form, still undeclared in enclosing local context - assertUndeclaredError(step("(do (let [a 10] (set! a 20)) a)")); - - // set! cannot alter value across closure boundary - { - assertEquals(5L,evalL("(let [a 5] ((fn [] (set! a 666))) a)")); - } - - // set! cannot alter value within query - assertEquals(5L,evalL("(let [a 5] (query (set! a 6)) a)")); - - // TODO: reconsider this - // set! doesn't work outside eval boundary? - assertCompileError(step ("(let [a 5] (eval `(set! a 7)) a)")); - } - - @Test - public void testEval() { - assertEquals("foo", evalS("(eval (list 'str \\f \\o \\o))")); - assertNull(eval("(eval 'nil)")); - assertEquals(10L, evalL("(eval '(+ 3 7))")); - assertEquals(40L, evalL("(eval `(* 2 4 5))")); - - assertArityError(step("(eval)")); - assertArityError(step("(eval 1 2)")); - } - - @Test - public void testEvalAs() { - assertEquals("foo", evalS("(eval-as *address* (list 'str \\f \\o \\o))")); - - assertTrustError(step("(eval-as *registry* '1)")); - - assertCastError(step("(eval-as :foo 2)")); - assertArityError(step("(eval-as 1)")); // arity > cast - assertArityError(step("(eval-as 1 2 3)")); - } - - @Test - public void testEvalAsTrustedUser() { - Context ctx=step("(set-controller "+VILLAIN+")"); - ctx=ctx.forkWithAddress(VILLAIN); - ctx=step(ctx,"(def hero "+HERO+")"); - - assertEquals(3L, evalL(ctx,"(eval-as hero '(+ 1 2))")); - assertEquals(HERO, eval(ctx,"(eval-as hero '*address*)")); - assertEquals(VILLAIN, eval(ctx,"(eval-as hero '*caller*)")); - assertEquals(Keywords.FOO, eval(ctx,"(eval-as hero '(return :foo))")); - assertEquals(Keywords.FOO, eval(ctx,"(eval-as hero '(halt :foo))")); - assertEquals(Keywords.FOO, eval(ctx,"(eval-as hero '(rollback :foo))")); - - assertAssertError(step(ctx,"(eval-as hero '(assert false))")); - } - - @Test - public void testEvalAsUntrustedUser() { - Context ctx=step("(set-controller nil)"); - ctx=ctx.forkWithAddress(VILLAIN); - ctx=step(ctx,"(def hero "+HERO+")"); - - assertTrustError(step(ctx,"(eval-as hero '(+ 1 2))")); - assertTrustError(step(ctx,"(eval-as (address hero) '(+ 1 2))")); - } - - @Test - public void testEvalAsWhitelistedUser() { - // create trust monitor that allows VILLAIN - Context ctx=step("(deploy '(do (defn check-trusted? ^{:callable? true} [s a o] (= s (address "+VILLAIN+")))))"); - Address monitor = (Address) ctx.getResult(); - ctx=step(ctx,"(set-controller "+monitor+")"); - - ctx=ctx.forkWithAddress(VILLAIN); - ctx=step(ctx,"(def hero "+HERO+")"); - - assertEquals(3L, evalL(ctx,"(eval-as hero '(+ 1 2))")); - } - - @Test - public void testQuery() { - Context> ctx=step("(query (def a 10) [*address* *origin* *caller* 10])"); - assertEquals(Vectors.of(HERO,HERO,null,10L), ctx.getResult()); - - // shouldn't be possible to mutate surrounding environment in query - assertEquals(10L,evalL("(let [a 3] (+ (query (set! a 5) (+ a 2)) a) )")); - - // shouldn't be any def in the environment - assertSame(INITIAL,ctx.getState()); - - // some juice should be consumed - assertTrue(context().getJuice()>ctx.getJuice()); - } - - @Test - public void testQueryError() { - Context ctx=step("(query (fail :FOO))"); - assertAssertError(ctx); - - // some juice should be consumed - assertTrue(context().getJuice()>ctx.getJuice()); - } - -// TODO: probably needs Op level support? -// @Test -// public void testQueryAs() { -// Context> ctx=step("(query-as "+Init.VILLAIN+" '(do (def a 10) [*address* *origin* *caller* 10]))"); -// assertEquals(Vectors.of(Init.VILLAIN,Init.VILLAIN,null,10L), ctx.getResult()); -// -// // shouldn't be any def in the environment -// assertSame(INITIAL,ctx.getState()); -// assertSame(INITIAL_CONTEXT.getLocalBindings(),ctx.getLocalBindings()); -// -// // some juice should be consumed -// assertTrue(INITIAL_CONTEXT.getJuice()>ctx.getJuice()); -// } - - @Test - public void testEvalAsNotWhitelistedUser() { - // create trust monitor that allows HERO only - Context ctx=step("(deploy '(do (defn check-trusted? ^{:callable? true} [s a o] (= s (address "+HERO+")))))"); - Address monitor = (Address) ctx.getResult(); - ctx=step(ctx,"(set-controller "+monitor+")"); - - ctx=ctx.forkWithAddress(VILLAIN); - ctx=step(ctx,"(def hero "+HERO+")"); - - assertTrustError(step(ctx,"(eval-as hero '(+ 1 2))")); - } - - @Test - public void testSetController() { - // set-controller returns new controller - assertEquals(VILLAIN, eval("(set-controller "+VILLAIN+")")); - assertEquals(VILLAIN, eval("(set-controller (address "+VILLAIN+"))")); - assertEquals(null, (Address)eval("(set-controller nil)")); - - assertNobodyError(step("(set-controller #666666)")); // non-existent account - - assertCastError(step("(set-controller :foo)")); - assertCastError(step("(set-controller (address nil))")); // Address cast fails - - assertArityError(step("(set-controller)")); - assertArityError(step("(set-controller 1 2)")); // arity > cast - } - - @Test - public void testScheduleFailures() { - assertArityError(step("(schedule)")); - assertArityError(step("(schedule 1)")); - assertArityError(step("(schedule 1 2 3)")); - assertArityError(step("(schedule :foo 2 3)")); // ARITY error before CAST - - assertCastError(step("(schedule :foo (def a 2))")); - assertCastError(step("(schedule nil (def a 2))")); - } - - @Test - public void testScheduleExecution() throws BadSignatureException { - long expectedTS = INITIAL.getTimeStamp().longValue() + 1000; - Context ctx = step("(schedule (+ *timestamp* 1000) (def a 2))"); - assertCVMEquals(expectedTS, ctx.getResult()); - State s = ctx.getState(); - BlobMap> sched = s.getSchedule(); - assertEquals(1L, sched.count()); - assertEquals(expectedTS, sched.entryAt(0).getKey().longValue()); - - assertTrue(step(ctx, "(do a)").isExceptional()); - - Block b = Block.of(expectedTS,InitTest.FIRST_PEER_KEY); - BlockResult br = s.applyBlock(b); - State s2 = br.getState(); - - Context ctx2 = Context.createInitial(s2, HERO, INITIAL_JUICE); - assertEquals(2L, evalL(ctx2, "a")); - } - - @Test - public void testExpand() { - assertEquals(Strings.create("foo"), eval("(expand (name :foo) (fn [x e] x))")); - assertEquals(CVMLong.create(3), eval("(expand '[1 2 3] (fn [x e] (nth x 2)))")); - - assertNull(Syntax.unwrap(eval("(expand nil)"))); - - assertCastError(step("(expand 1 :foo)")); - assertCastError(step("(expand { 888 227 723 560} [75 561 258 833])")); - - - assertArityError(step("(expand)")); - assertArityError(step("(expand 1 (fn [x e] x) :blah :blah)")); - - // arity error calling expander function - assertArityError(step("(expand 1 (fn [x] x))")); - - // arity error in expansion execution - assertArityError(step("(expand 1 (fn [x e] (count)))")); - } - - @Test - public void testExpandEdgeCases() { - // BAd functions - assertCastError(step("(expand 123 #0 :foo)")); - assertCastError(step("(expand 123 #0)")); - - // psuedo-function application, not valid for expand - assertCastError(step("(expand 'foo 'bar 'baz)")); - assertCastError(step("(expand {} :foo)")); - assertCastError(step("(expand {:bar 1 :bax 2} :bar :baz)")); - assertCastError(step("(expand {:foo 1 :bax 2} :bar :baz)")); - } - - @Test - public void testExpandOnce() { - // an expander that does nothing except wrap as syntax. - Context c=step("(def identity-expand (fn [x e] x))"); - assertEquals(Keywords.FOO,eval(c,"(identity-expand :foo nil)")); - - // function that expands once with initial-expander, then with identity - c=step(c,"(defn expand-once [x] (*initial-expander* x identity-expand))"); - // Should expand the outermost macro only - assertEquals(read("(cond (if 1 2) 3 4)"),Syntax.unwrapAll(eval(c,"(expand-once '(if (if 1 2) 3 4))"))); - - // Should be idempotent - assertEquals(eval(c,"(expand '(if (if 1 2) 3 4))"),eval(c,"(expand (expand-once '(if (if 1 2) 3 4)))")); - } - - - @Test - public void testMacro() { - Context c=step("(defmacro foo [] :foo)"); - assertEquals(Keywords.FOO,eval(c,"(foo)")); - } - - @Test - public void testQuote() { - assertEquals(Vectors.of(1,2,3),eval("(quote [1 2 3])")); - assertEquals(Sets.of(42),eval("(quote #{42})")); // See Issue #109 - assertFalse(evalB("(= (quote #{42}) (quote #{(syntax 42)}))")); // See Issue #109 - - assertEquals(Vectors.of(1,Lists.of(Symbols.IF,4,7),3),eval("(quote [1 (if 4 7) 3])")); - } - - @Test - public void testSyntax() { - assertEquals(Syntax.of(null), eval("(syntax nil)")); - assertEquals(Syntax.of(10L), eval("(syntax 10)")); - - // TODO: check if this is sensible - // Syntax should be idempotent and wrap one level only - assertCVMEquals(eval("(syntax 10)"), eval("(syntax (syntax 10))")); - - // Syntax with null / empty metadata should equal basic syntax - assertCVMEquals(eval("(syntax 10)"), eval("(syntax 10 nil)")); - assertCVMEquals(eval("(syntax 10)"), eval("(syntax 10 {})")); - - assertCastError(step("(syntax 2 3)")); - - assertArityError(step("(syntax)")); - assertArityError(step("(syntax 2 3 4)")); - } - - @Test - public void testUnsyntax() { - assertNull(eval("(unsyntax (syntax nil))")); - assertNull(eval("(unsyntax nil)")); - assertEquals(10L, evalL("(unsyntax (syntax 10))")); - assertEquals(Keywords.FOO, eval("(unsyntax (expand :foo))")); - - assertArityError(step("(unsyntax)")); - assertArityError(step("(unsyntax 2 3)")); - } - - @Test - public void testMeta() { - assertEquals(Maps.empty(),eval("(meta (syntax nil))")); - assertNull(eval("(meta nil)")); - assertNull(eval("(meta 10)")); - assertEquals(Maps.of(1L,2L), eval("(meta (syntax 10 {1 2}))")); - - assertArityError(step("(meta)")); - assertArityError(step("(meta 2 3)")); - } - - @Test - public void testSyntaxQ() { - assertFalse(evalB("(syntax? nil)")); - assertTrue(evalB("(syntax? (syntax 10))")); - - assertArityError(step("(syntax?)")); - assertArityError(step("(syntax? 2 3)")); - } - - @Test - public void testInitialExpander() { - // bad continuation expanders - assertCastError(step("(*initial-expander* (list #0) #0)")); - - assertArityError(step("(*initial-expander* 1 2 3)")); - assertArityError(step("(*initial-expander* 1)")); - } - - @Test - public void testExportsQ() { - Context ctx = step("(def caddr (deploy '(do " + "(defn private [] :priv) " + "(defn public ^{:callable? true} [] :pub))))"); - - Address caddr = (Address) ctx.getResult(); - assertNotNull(caddr); - - assertTrue(evalB(ctx, "(callable? caddr 'public)")); // OK - assertFalse(evalB(ctx, "(callable? caddr 'private)")); // Defined, but not exported - assertFalse(evalB(ctx, "(callable? caddr 'random-symbol)")); // Doesn't exist - - assertCastError(step(ctx, "(callable? caddr :public)")); // not a Symbol - assertCastError(step(ctx, "(callable? caddr :random-name)")); - assertCastError(step(ctx, "(callable? caddr :private)")); - - assertArityError(step(ctx, "(callable? 1)")); - assertArityError(step(ctx, "(callable? 1 2 3)")); - - assertCastError(step(ctx, "(callable? :foo :foo)")); - assertCastError(step(ctx, "(callable? nil :foo)")); - assertCastError(step(ctx, "(callable? caddr nil)")); - assertCastError(step(ctx, "(callable? caddr 1)")); - } - - @Test - public void testDec() { - assertEquals(0L, evalL("(dec 1)")); - assertEquals(0L, evalL("(dec (byte 1))")); - assertEquals(-10L, evalL("(dec -9)")); - // assertEquals(96L,(long)eval("(dec \\a)")); // TODO: think about this - - assertCastError(step("(dec nil)")); - assertCastError(step("(dec :foo)")); - assertCastError(step("(dec [1])")); - assertCastError(step("(dec #666)")); - assertCastError(step("(dec 3.0)")); - - assertArityError(step("(dec)")); - assertArityError(step("(dec 1 2)")); - } - - @Test - public void testInc() { - assertEquals(2L, evalL("(inc 1)")); - assertEquals(2L, evalL("(inc (byte 1))")); - // assertEquals(98L,(long)eval("(inc \\a)")); // TODO: think about this - - assertCastError(step("(inc #42)")); // Issue #89 - assertCastError(step("(inc nil)")); - assertCastError(step("(inc \\c)")); // Issue #89 - assertCastError(step("(inc true)")); // Issue #89 - - assertArityError(step("(inc)")); - assertArityError(step("(inc 1 2)")); - } - - @Test - public void testOr() { - assertNull(eval("(or)")); - assertNull(eval("(or nil)")); - assertEquals(Keywords.FOO, eval("(or :foo)")); - assertEquals(Keywords.FOO, eval("(or nil :foo :bar)")); - assertEquals(Keywords.FOO, eval("(or :foo nil :bar)")); - - // ensure later branches never get executed - assertEquals(Keywords.FOO, eval("(or :foo (+ nil :bar))")); - - assertFalse(evalB("(or nil nil false)")); - assertTrue(evalB("(or nil nil true)")); - - // arity error if fails before first truth value - assertArityError(step("(or nil (count) true)")); - } - - @Test - public void testAnd() { - assertTrue(evalB("(and)")); - assertNull(eval("(and nil)")); - assertEquals(Keywords.FOO, eval("(and :foo)")); - assertEquals(Keywords.FOO, eval("(and :bar :foo)")); - assertNull(eval("(and :foo nil :bar)")); - - // ensure later branches never get executed - assertNull(eval("(and nil (+ nil :bar))")); - - assertFalse(evalB("(and 1 false 2)")); - assertTrue(evalB("(and 1 :foo true true)")); - - // arity error if fails before first falsey value - assertArityError(step("(and true (count) nil)")); - } - - @Test - public void testSpecialAddress() { - // Hero should be *address* initial context - assertEquals(InitTest.HERO, eval("*address*")); - - // *address* MUST return Actor address within actor call - Context ctx=step("(def act (deploy `(do (defn addr ^{:callable? true} [] *address*))))"); - Address act=(Address) ctx.getResult(); - assertEquals(act, eval(ctx,"(call act (addr))")); - - // *address* MUST be current address in library call - assertEquals(InitTest.HERO, eval(ctx,"(act/addr)")); - } - - @Test - public void testSpecialOrigin() { - // Hero should be *origin* in initial context - assertEquals(InitTest.HERO, eval("*origin*")); - - // *origin* MUST return original address within actor call - Context ctx=step("(def act (deploy `(do (defn origin ^{:callable? true} [] *origin*))))"); - assertEquals(InitTest.HERO, eval(ctx,"(call act (origin))")); - - // *origin* MUST be original address in library call - assertEquals(InitTest.HERO, eval(ctx,"(act/origin)")); - } - - @Test - public void testSpecialAllowance() { - // Should have initial allowance at start - assertEquals(Constants.INITIAL_ACCOUNT_ALLOWANCE, evalL("*memory*")); - - // Buy some memory - assertEquals(Constants.INITIAL_ACCOUNT_ALLOWANCE, evalL("*memory*")); - - } - - - @Test - public void testSpecialBalance() { - // balance should return exact balance of account after execution - Context ctx = step("(long *balance*)"); - Long bal=ctx.getAccountStatus(HERO).getBalance(); - assertCVMEquals(bal, ctx.getResult()); - - // throwing it all away.... - assertEquals(0L, evalL("(do (transfer "+VILLAIN+" *balance*) *balance*)")); - - // check balance as single expression - assertEquals(bal, evalL("*balance*")); - - // Local values override specials - assertNull(eval("(let [*balance* nil] *balance*)")); - - // TODO: reconsider this, special take priority over enviornment? - assertCVMEquals(ctx.getOffer(),eval("(do (def *offer* :foo) *offer*)")); - - // Alternative behaviour - //assertNull(eval("(let [*balance* nil] *balance*)")); - //assertEquals(Keywords.FOO,eval("(do (def *balance* :foo) *balance*)")); - } - - @Test - public void testSpecialCaller() { - assertNull(eval("*caller*")); - assertEquals(HERO, eval("(do (def c (deploy '(do (defn f ^{:callable? true} [] *caller*)))) (call c (f)))")); - } - - @Test - public void testSpecialResult() { - // initial context result should be null - assertNull(eval("*result*")); - - // Result should get value of last completed expression - assertEquals(Keywords.FOO, eval("(do :foo *result*)")); - assertNull(eval("(do 1 (do) *result*)")); - - // TODO: how should this behave? - // assertEquals(Keywords.FOO, eval("(let [a :foo] *result*)")); - - assertEquals(Keywords.FOO, eval("(do ((fn [] :foo)) *result*)")); - - // *result* should be cleared to nil in an Actor call. - assertNull(eval("(do (def c (deploy '(do (defn f ^{:callable? true} [] *result*)))) (call c (f)))")); - - } - - @Test - public void testSpecialState() { - assertSame(INITIAL, eval("*state*")); - assertSame(INITIAL.getAccounts(), eval("(:accounts *state*)")); - } - - @Test - public void testSpecialKey() { - assertEquals(InitTest.HERO_KEYPAIR.getAccountKey(), eval("*key*")); - } - - @Test - public void testSpecialJuice() { - // TODO: semantics of returning juice before lookup complete is OK? - // seems sensible, represents "juice left at this position". - assertCVMEquals(INITIAL_JUICE, eval(Special.forSymbol(Symbols.STAR_JUICE))); - - // juice gets consumed before returning a value - assertCVMEquals(INITIAL_JUICE-Juice.DO - Juice.CONSTANT, eval(comp("(do 1 *juice*)"))); - } - - - @Test - public void testSpecialEdgeCases() { - - // TODO: consider this - //assertEquals(Init.HERO,eval(Init.CORE_ADDRESS+"/*balance*")); - - // TODO: consider this - // Lookup in core environment of special returns the Symbol - assertEquals(Symbols.STAR_JUICE,eval("(lookup *juice*)")); - - assertEquals(Symbols.STAR_JUICE,eval(Lookup.create(Symbols.STAR_JUICE))); - } - - @Test public void testSpecialHoldings() { - assertSame(BlobMaps.empty(),eval("*holdings*")); - - // Test set-holding modifies *holdings* as expected - assertNull(eval("(get-holding *address*)")); - assertEquals(BlobMaps.of(HERO,1L),eval("(do (set-holding *address* 1) *holdings*)")); - - assertNull(eval("(*holdings* { :PuSg 650989 })")); - assertEquals(Keywords.FOO,eval("(*holdings* { :PuSg 650989 } :foo )")); - } - - @Test public void testHoldings() { - Context ctx = step("(def VILLAIN (address \""+VILLAIN.toHexString()+"\"))"); - assertTrue(eval(ctx,"VILLAIN") instanceof Address); - ctx=step(ctx,"(def NOONE (address 7777777))"); - - // Basic empty holding should match empty blobmap in account record. See #131 - assertTrue(evalB("(= *holdings* (:holdings (account *address*)) (blob-map))")); - - // initial holding behaviour - assertNull(eval(ctx,"(get-holding VILLAIN)")); - assertCastError(step(ctx,"(get-holding :foo)")); - assertCastError(step(ctx,"(get-holding nil)")); - assertNobodyError(step(ctx,"(get-holding NOONE)")); - - // OK to set holding for a real owner account - assertEquals(100L,evalL(ctx,"(set-holding VILLAIN 100)")); - - // error to set holding for a non-existent owner account - assertNobodyError(step(ctx,"(set-holding NOONE 200)")); - - // trying to set holding for the wrong type - assertCastError(step(ctx,"(set-holding :foo 300)")); - - { // test simple assign - Context c2 = step(ctx,"(set-holding VILLAIN 123)"); - assertEquals(123L,evalL(c2,"(get-holding VILLAIN)")); - - assertTrue(c2.getAccountStatus(VILLAIN).getHoldings().containsKey(HERO)); - assertCVMEquals(123L,c2.getAccountStatus(VILLAIN).getHolding(HERO)); - } - - { // test null assign - Context c2 = step(ctx,"(set-holding VILLAIN nil)"); - assertFalse(c2.getAccountStatus(VILLAIN).getHoldings().containsKey(HERO)); - } - } - - @Test - public void testSymbolFor() { - assertEquals(Symbols.COUNT, Core.symbolFor(Core.COUNT)); - assertThrows(Throwable.class, () -> Core.symbolFor(null)); - } - - @Test - public void testCoreFormatRoundTrip() throws BadFormatException { - { // a core function - ACell c = eval("count"); - Blob b = Format.encodedBlob(c); - assertSame(c, Format.read(b)); - } - - { // a core macro - ACell c = eval("*initial-expander*"); - Blob b = Format.encodedBlob(c); - assertSame(c, Format.read(b)); - } - - { // a basic lambda expression - ACell c = eval("(fn [x] x)"); - Blob b = Format.encodedBlob(c); - assertEquals(c, Format.read(b)); - } - } - -} diff --git a/convex-core/src/test/java/convex/core/lang/DataStructuresTest.java b/convex-core/src/test/java/convex/core/lang/DataStructuresTest.java deleted file mode 100644 index 3df2130a2..000000000 --- a/convex-core/src/test/java/convex/core/lang/DataStructuresTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package convex.core.lang; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -import convex.core.State; -import convex.core.data.ACell; -import convex.core.data.ASequence; -import convex.core.data.ASet; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMLong; -import convex.core.init.InitTest; -import convex.core.util.Utils; - -/** - * Tests for medium sized data structure operations. - * - */ -public class DataStructuresTest { - - private static final State INITIAL = TestState.STATE; - private static final long INITIAL_JUICE = 100000; - private static final Context INITIAL_CONTEXT; - - static { - try { - INITIAL_CONTEXT = Context.createInitial(INITIAL, InitTest.HERO, INITIAL_JUICE); - } catch (Throwable e) { - throw new Error(e); - } - } - - public T eval(String source) { - try { - Context c = INITIAL_CONTEXT; - AOp op = TestState.compile(c, source); - Context rc = c.execute(op); - return rc.getResult(); - } catch (Exception e) { - throw Utils.sneakyThrow(e); - } - } - - @Test - public void testSetRoundTripRegression() { - ASet a = eval("#{1,8,0,4,9,5,2,3,7,6}"); - assertEquals(10, a.count()); - ASequence b = RT.sequence(a); - assertEquals(10, b.count()); - ASet c = RT.castSet(b); - assertEquals(a, c); - } - - @Test - public void testRefCounts() { - assertEquals(0, Utils.totalRefCount(Vectors.empty())); - assertEquals(2, Utils.totalRefCount(Vectors.of(1, 2))); - assertEquals(1, Utils.totalRefCount(eval("(fn [a] a)"))); // 1 Ref in params [symbol] - assertEquals(6, Utils.totalRefCount(eval("[[1 2] [3 4]]"))); // 6 vector element Refs - } - -} diff --git a/convex-core/src/test/java/convex/core/lang/DocsTest.java b/convex-core/src/test/java/convex/core/lang/DocsTest.java deleted file mode 100644 index cbfcf1e3b..000000000 --- a/convex-core/src/test/java/convex/core/lang/DocsTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package convex.core.lang; - -import static convex.core.lang.TestState.step; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.util.Map.Entry; - -import org.junit.jupiter.api.Test; - -import convex.core.data.ACell; -import convex.core.data.AHashMap; -import convex.core.data.AVector; -import convex.core.data.Keywords; -import convex.core.data.Symbol; - -public class DocsTest { - public static final boolean PRINT_MISSING=true; - - @Test public void testDocs() { - for (Entry> me: Core.METADATA.entrySet()) { - Symbol sym=me.getKey(); - AHashMap meta = me.getValue(); - if (meta.isEmpty()) { - if (PRINT_MISSING) System.err.println("Empty metadata in Core: "+sym); - } else { - @SuppressWarnings("unchecked") - AHashMap doc=(AHashMap) meta.get(Keywords.DOC); - if (doc==null) { - if (PRINT_MISSING) System.err.println("No documentation in Core: "+sym); - } else { - doDocTest(sym,doc); - } - } - } - } - - public void doDocTest(Symbol sym,AHashMap doc) { - ACell desc = doc.get(Keywords.DESCRIPTION); - if (desc == null) { - if (PRINT_MISSING) System.err.println("No description on Core def: " + sym); - } - - @SuppressWarnings("unchecked") - AVector> examples=(AVector>) doc.get(Keywords.EXAMPLES); - if (examples!=null) { - for (AHashMap ex:examples) { - doExampleTest(sym,ex); - } - } - } - - private void doExampleTest(Symbol sym, AHashMap ex) { - String code=RT.jvm( ex.get(Keywords.CODE)); - - Context ctx=step(code); - assertNotNull(ctx); - } -} diff --git a/convex-core/src/test/java/convex/core/lang/GenTestCode.java b/convex-core/src/test/java/convex/core/lang/GenTestCode.java deleted file mode 100644 index 554684d4e..000000000 --- a/convex-core/src/test/java/convex/core/lang/GenTestCode.java +++ /dev/null @@ -1,63 +0,0 @@ -package convex.core.lang; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.Random; - -import com.pholser.junit.quickcheck.From; -import com.pholser.junit.quickcheck.Property; - -import convex.core.data.ACell; -import convex.core.data.Syntax; -import convex.core.exceptions.ParseException; -import convex.core.init.InitTest; -import convex.test.generators.FormGen; - -public class GenTestCode { - - @Property - public void testExpand(@From(FormGen.class) ACell form) { - Context ctx = Context.createFake(TestState.STATE, InitTest.HERO); - ctx = ctx.expand(form); - - if (!ctx.isExceptional()) { - ACell expObject=ctx.getResult(); - assertTrue(expObject instanceof Syntax); - - ctx=ctx.compile((Syntax) expObject); - - if (!ctx.isExceptional()) { - Object compObject=ctx.getResult(); - assertTrue(compObject instanceof AOp); - - ctx=ctx.execute((AOp) compObject); - } - } - - String s=RT.str(form); - doMutateTest(s); - } - - - @SuppressWarnings("unused") - public void doMutateTest(String original) { - StringBuffer sb=new StringBuffer(original); - Random r=new Random(original.hashCode()); - - int n=r.nextInt(3); - switch (n) { - case 0: sb.deleteCharAt(r.nextInt(sb.length())); break; - case 1: sb.insert(r.nextInt(sb.length()+1),sb.charAt(r.nextInt(sb.length()))); break; - case 2: sb.setCharAt(r.nextInt(sb.length()),sb.charAt(r.nextInt(sb.length()))); break; - default: - } - - try { - String source=sb.toString(); - ACell newForm=Reader.read(source); - Syntax newSyntax=Reader.readSyntax(source); - } catch (ParseException p) { - // OK, we broken the string - } - } -} diff --git a/convex-core/src/test/java/convex/core/lang/GenTestCore.java b/convex-core/src/test/java/convex/core/lang/GenTestCore.java deleted file mode 100644 index 5f7819e69..000000000 --- a/convex-core/src/test/java/convex/core/lang/GenTestCore.java +++ /dev/null @@ -1,220 +0,0 @@ -package convex.core.lang; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import static convex.test.Assertions.*; - -import org.junit.runner.RunWith; - -import com.pholser.junit.quickcheck.From; -import com.pholser.junit.quickcheck.Property; -import com.pholser.junit.quickcheck.generator.java.lang.LongGenerator; -import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; - -import convex.core.data.ACell; -import convex.core.data.ADataStructure; -import convex.core.data.AList; -import convex.core.data.ASequence; -import convex.core.data.ASet; -import convex.core.data.AString; -import convex.core.data.AVector; -import convex.core.data.Address; -import convex.core.data.Blob; -import convex.core.data.BlobsTest; -import convex.core.data.Lists; -import convex.core.data.MapEntry; -import convex.core.data.Sets; -import convex.core.data.Strings; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMBool; -import convex.core.data.prim.CVMByte; -import convex.core.data.prim.CVMLong; -import convex.core.util.Utils; -import convex.test.generators.AddressGen; -import convex.test.generators.ListGen; -import convex.test.generators.SetGen; -import convex.test.generators.VectorGen; - -/** - * Set of generative tests for Runtime functions - * - * Generally grouped according to generated input types. The idea is to generate random sets of paramter - * values and check that RT / core functions behave as expected. - */ -@RunWith(JUnitQuickcheck.class) -public class GenTestCore { - - private void doDataStructureTests(ADataStructure a) { - long n=RT.count(a); - - assertTrue(RT.bool(a)); - - ASet uniqueVals=RT.castSet(a); - long ucount=RT.count(uniqueVals); - assertTrue(ucount<=n); - - if (n>0) { - assertTrue(ucount>0); - } else { - assertSame(a.empty(),a); - } - } - - private void doSequenceTests(ASequence a) { - doDataStructureTests(a); - - long n=RT.count(a); - - assertSame(a,RT.sequence(a)); - - // bounds exceptions - assertThrows(IndexOutOfBoundsException.class,()->RT.nth(a,n)); - assertThrows(IndexOutOfBoundsException.class,()->RT.nth(a,-1)); - } - - /** - * Tests for objects that can be coerced into sequences - * @param a - */ - private void doSequenceableTests(ACell a) { - ASequence seq=RT.sequence(a); - doSequenceTests(seq); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Property - public void testListFunctions(@From(ListGen.class) AList a) { - doSequenceTests(a); - - assertSame(Lists.empty(),a.empty()); - - AString foos=Strings.create("foo"); - ASequence ca=RT.cons(foos, a); - assertEquals(foos,ca.get(0)); - // assertEquals(a,RT.next(ca)); // TODO BUG: broken for big lists / vectors - assertEquals(ca,a.conj(foos)); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Property - public void testVectorFunctions(@From(VectorGen.class) AVector a) { - doSequenceTests(a); - - if (!(a instanceof MapEntry)) { - // only true for regular vectors - assertSame(a,RT.vec(a)); - } - assertSame(Vectors.empty(),a.empty()); - - AString foos=Strings.create("foo"); - long n=RT.count(a); - AVector ca=a.conj(foos); - assertEquals(foos,RT.nth(ca,n)); - } - - private void doAddressTests(Address a) { - assertSame(a,RT.castAddress(a)); - long n=RT.count(a); - - Blob b=a.toBlob(); - assertEquals(b,RT.castBlob(a)); - assertEquals(a.toHexString(),b.toHexString()); - - // Check a byte in the Address - assertSame(CVMByte.create(a.byteAt(6)),RT.nth(a, 6)); - - assertThrows(IndexOutOfBoundsException.class,()->RT.nth(a,-1)); - assertThrows(IndexOutOfBoundsException.class,()->RT.nth(a,n)); - - BlobsTest.doBlobTests(a); - } - - @Property - public void testAddressFunctions(@From(AddressGen.class) Address a) { - doAddressTests(a); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Property - public void testSetFunctions(@From(SetGen.class) ASet a) { - doSequenceableTests(a); - - assertSame(a,RT.castSet(a)); - assertSame(Sets.empty(),a.empty()); - - assertEquals(a,RT.castSet(RT.sequence(a))); - - long n=RT.count(a); - AString key=Strings.create("newkey1"); - // loop until we have a key not in set - while(a.contains(key)) { - key=Strings.create("newkey"+(Integer.parseInt(key.toString().substring(6))+1)); - } - - ASet ca=a.conj(key); - assertSame(CVMBool.TRUE,RT.get(ca, key)); - assertEquals(n+1, RT.count(ca)); - assertFalse(a.containsKey(key)); - assertTrue(ca.containsKey(key)); - } - - @Property - public void testLongFunctions(@From(LongGenerator.class) Long a) { - CVMLong ca=CVMLong.create(a); - - long v=a; - assertEquals(Long.toString(v),RT.str(ca).toString()); - assertSame(CVMByte.create(v),RT.castByte(ca)); - assertCVMEquals((char)v,RT.toCharacter(ca)); - assertCVMEquals(v+1,RT.inc(ca)); - assertCVMEquals(v-1,RT.dec(ca)); - assertCVMEquals(0,RT.compare(a,(Long)v)); - assertCVMEquals(-1,RT.compare((long)a,v+10)); - assertCVMEquals(1,RT.compare((long)a,v-10)); - - CVMLong[] args=new CVMLong[] {ca}; - assertEquals(-v,RT.minus(args).longValue()); - assertEquals(v,RT.plus(args).longValue()); - assertEquals(v,RT.times(args).longValue()); - assertEquals(1.0/v,RT.divide(args).doubleValue()); - - assertTrue(RT.lt(args)); - assertTrue(RT.gt(args)); - assertTrue(RT.le(args)); - assertTrue(RT.ge(args)); - assertTrue(RT.eq(args)); - - assertTrue(Utils.bool(a)); // longs are always truthy - - assertNull(RT.count(a)); - assertNull(RT.vec(a)); - } - - @Property - public void testLongMaths(@From(LongGenerator.class) Long a, @From(LongGenerator.class) Long b) { - assertEquals(RT.compare(a, b),-RT.compare(b,a)); - - CVMLong ca=CVMLong.create(a); - CVMLong cb=CVMLong.create(b); - - - CVMLong[] args=new CVMLong[] {ca,cb}; - assertEquals(a+b,RT.plus(args).longValue()); - assertEquals(a*b,RT.times(args).longValue()); - assertEquals(a-b,RT.minus(args).longValue()); - assertEquals(((double)a)/((double)b),RT.divide(args).doubleValue()); - - assertEquals(ab,RT.gt(args)); - assertEquals(a<=b,RT.le(args)); - assertEquals(a>=b,RT.ge(args)); - assertEquals(a==b,RT.eq(args)); - // assertEquals(a!=b,RT.ne(args)); // TODO: do we need this? - } - -} diff --git a/convex-core/src/test/java/convex/core/lang/GenTestRT.java b/convex-core/src/test/java/convex/core/lang/GenTestRT.java deleted file mode 100644 index 3dd636c62..000000000 --- a/convex-core/src/test/java/convex/core/lang/GenTestRT.java +++ /dev/null @@ -1,44 +0,0 @@ -package convex.core.lang; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import org.junit.runner.RunWith; - -import com.pholser.junit.quickcheck.From; -import com.pholser.junit.quickcheck.Property; -import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; - -import convex.core.data.ACell; -import convex.core.data.ACollection; -import convex.core.data.ASet; -import convex.test.generators.CollectionGen; -import convex.test.generators.ValueGen; - -@RunWith(JUnitQuickcheck.class) -public class GenTestRT { - - @SuppressWarnings("rawtypes") - @Property - public void setConversion(@From(CollectionGen.class) ACollection a) { - long ac = a.count(); - ASet set = RT.castSet(a); - assertTrue(set.count() <= ac); - for (Object o : a) { - assertTrue(set.contains(o)); - } - } - - @Property - public void strTest(@From(ValueGen.class) ACell b) { - String s = RT.str(b); - assertNotNull(s); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Property - public void conjTest(@From(CollectionGen.class) ACollection a, @From(ValueGen.class) ACell b) { - ACollection ac = a.conj(b); - assertTrue(ac.contains(b)); - } -} diff --git a/convex-core/src/test/java/convex/core/lang/JuiceTest.java b/convex-core/src/test/java/convex/core/lang/JuiceTest.java deleted file mode 100644 index 421eb4579..000000000 --- a/convex-core/src/test/java/convex/core/lang/JuiceTest.java +++ /dev/null @@ -1,173 +0,0 @@ -package convex.core.lang; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -import convex.core.data.ACell; - -/** - * Tests for expected juice costs - * - * These are not your regular example based tests. These are handcrafted, - * artisinal juice tests. - */ -public class JuiceTest extends ACVMTest { - - public JuiceTest() { - super(TestState.STATE); - } - - private long JUICE = context().getJuice(); - - /** - * Compute the precise juice consumed by executing the compiled source code - * (i.e. this excludes the code of expansion+compilation). - * - * @param source - * @return Juice consumed - */ - public long juice(String source) { - ACell form = Reader.read(source); - AOp op = context().expandCompile(form).getResult(); - Context jctx = context().execute(op); - return JUICE - jctx.getJuice(); - } - - /** - * Compute the precise juice consumed by compiling the source code (i.e. the - * cost of expand+compilation). - * - * @param source - * @return Juice consumed - */ - public long compileJuice(String source) { - ACell form = Reader.read(source); - Context jctx = context().expandCompile(form); - return JUICE - jctx.getJuice(); - } - - /** - * Compute the precise juice consumed by expanding the source code (i.e. the - * cost of initial expander execution). - * - * @param source - * @return Juice consumed - */ - public long expandJuice(String source) { - ACell form = Reader.read(source); - Context jctx = context().invoke(Core.INITIAL_EXPANDER,form, Core.INITIAL_EXPANDER); - return JUICE - jctx.getJuice(); - } - - /** - * Returns the difference in juice consumed between two sources - * - * @param a - * @param b - * @return Difference in juice consumed - */ - public long juiceDiff(String a, String b) { - return juice(b) - juice(a); - } - - @Test - public void testSimpleValues() { - assertEquals(Juice.CONSTANT, juice("1")); - assertEquals(Juice.LOOKUP_SYM, juice("count")); - assertEquals(Juice.DO, juice("(do)")); - } - - @Test - public void testFunctionCalls() { - assertEquals(Juice.LOOKUP_SYM + Juice.EQUALS, juice("(=)")); - } - - @Test - public void testCompileJuice() { - assertEquals(Juice.EXPAND_CONSTANT + Juice.COMPILE_CONSTANT, compileJuice("1")); - assertEquals(Juice.EXPAND_CONSTANT + Juice.COMPILE_CONSTANT, compileJuice("[]")); - - assertEquals(Juice.EXPAND_CONSTANT + Juice.COMPILE_LOOKUP, compileJuice("foobar")); - } - - @Test - public void testExpandJuice() { - assertEquals(Juice.EXPAND_CONSTANT, expandJuice("1")); - assertEquals(Juice.EXPAND_CONSTANT, expandJuice("[]")); - assertEquals(Juice.EXPAND_SEQUENCE + Juice.EXPAND_CONSTANT * 4, expandJuice("(= 1 2 3)")); - assertEquals(Juice.EXPAND_SEQUENCE + Juice.EXPAND_CONSTANT * 3, expandJuice("[1 2 3]")); // [1 2 3] -> (vector 1 - // 2 3) - } - - @Test - public void testEval() { - {// eval for a single constant - long j = juice("(eval 1)"); - assertEquals((Juice.EVAL + Juice.LOOKUP_SYM + Juice.CONSTANT) + Juice.EXPAND_CONSTANT + Juice.COMPILE_CONSTANT - + Juice.CONSTANT, j); - - // expand list with symbol and number literal - long je = expandJuice("(eval 1)"); - assertEquals((Juice.EXPAND_SEQUENCE + Juice.EXPAND_CONSTANT * 2), je); - - // compile node with constant and symbol lookup - long jc = compileJuice("(eval 1)"); - assertEquals(je + (Juice.COMPILE_NODE + Juice.COMPILE_CONSTANT + Juice.COMPILE_LOOKUP), jc); - } - - // Calculate cost of executing op to build a single element vector, need this - // later - long oneElemVectorJuice = juice("[1]"); - // (vector 1), where vector is a constant core function. - assertEquals((Juice.CONSTANT + Juice.BUILD_DATA + Juice.BUILD_PER_ELEMENT + Juice.CONSTANT), - oneElemVectorJuice); - - {// eval for a small vector - long j = juice("(eval [1])"); - long exParams = (Juice.LOOKUP_SYM + oneElemVectorJuice); // prepare call (lookup 'eval', build 1-vector arg) - long exCompile = compileJuice("[1]"); // cost of compiling [1] - long exInvoke = (Juice.EVAL + oneElemVectorJuice); // cost of eval plus cost of running [1] - assertEquals(exParams + exCompile + exInvoke, j); - } - - { - long jdiffSimple = juiceDiff("[1]", "[1 2]"); - assertEquals(Juice.BUILD_PER_ELEMENT + Juice.CONSTANT, jdiffSimple); // extra cost per element in execution - - long jdiff = juiceDiff("(eval [1])", "(eval [1 2])"); - - // we pay +1 simple cost preparing args eval call, and +1 in ecexution phase. - // One extra constant in expand and compile phase. - assertEquals(Juice.EXPAND_CONSTANT + Juice.COMPILE_CONSTANT + jdiffSimple * 2, jdiff); - } - } - - @Test - public void testDef() { - assertEquals(Juice.DEF + Juice.CONSTANT, juice("(def a 1)")); - } - - @Test - public void testReturn() { - assertEquals(Juice.RETURN + Juice.CONSTANT + Juice.LOOKUP_SYM, juice("(return :foo)")); - } - - @Test - public void testHalt() { - assertEquals(Juice.RETURN + Juice.CONSTANT + Juice.LOOKUP_SYM, juice("(halt 123)")); - } - - @Test - public void testRollback() { - assertEquals(Juice.RETURN + Juice.CONSTANT + Juice.LOOKUP_SYM, juice("(rollback 123)")); - } - - @Test - public void testLoopIteration() { - long j1 = juice("(loop [i 2] (cond (> i 0) (recur (dec i)) :end))"); - long j2 = juice("(loop [i 3] (cond (> i 0) (recur (dec i)) :end))"); - assertEquals(Juice.COND_OP + (Juice.LOOKUP_SYM * 3) + ((Juice.LOOKUP)*2) + Juice.CONSTANT * 1 + Juice.ARITHMETIC + Juice.NUMERIC_COMPARE - + Juice.RECUR, j2 - j1); - } -} diff --git a/convex-core/src/test/java/convex/core/lang/NumericsTest.java b/convex-core/src/test/java/convex/core/lang/NumericsTest.java deleted file mode 100644 index 249e6b7ac..000000000 --- a/convex-core/src/test/java/convex/core/lang/NumericsTest.java +++ /dev/null @@ -1,307 +0,0 @@ -package convex.core.lang; - -import static convex.core.lang.TestState.eval; -import static convex.core.lang.TestState.evalB; -import static convex.core.lang.TestState.evalD; -import static convex.core.lang.TestState.evalL; -import static convex.core.lang.TestState.step; -import static convex.test.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -import convex.core.data.prim.CVMByte; -import convex.core.data.prim.CVMDouble; - -/** - * - * "You know, people think mathematics is complicated. Mathematics is the simple - * bit. Its the stuff we can understand. Its cats that are complicated. I mean, - * what is it in those little molecules and stuff that make one cat behave - * differently than another, or that make a cat? And how do you define a cat? I - * have no idea." - * - * - John Conway - */ -public class NumericsTest { - - @Test - public void testIncDec() { - assertEquals(1L, evalL("(inc (byte 256))")); - assertEquals(2L, evalL("(inc 1)")); - assertEquals(3L, evalL("(dec 4)")); - assertEquals(4L, evalL("(inc (dec 4))")); - - assertCastError(step("(inc nil)")); - assertCastError(step("(dec nil)")); - assertCastError(step("(inc :foo)")); - - } - - @Test - public void testPlus() { - assertEquals(0L, evalL("(+)")); - assertEquals(1L, evalL("(+ 1)")); - assertEquals(2L, evalL("(+ 3 -1)")); - assertEquals(3L, evalL("(+ 1 2)")); - assertEquals(4L, evalL("(+ 0 1 2 1)")); - - // long wrap round 64-bit signed - assertEquals(-2, evalL("(+ 9223372036854775807 9223372036854775807)")); - assertEquals(0, evalL("(+ -9223372036854775808 -9223372036854775808)")); - - } - - @Test - public void testPlusDouble() { - assertEquals(1.0, evalD("(+ 1.0)")); - assertEquals(2.0, evalD("(+ 3 -1.0)")); - assertEquals(3.0, evalD("(+ 1.0 2)")); - assertEquals(4.0, evalD("(+ 0 1 2.0 1)")); - - assertCastError(step("(+ nil)")); - assertCastError(step("(+ 1.0 :foo)")); - assertCastError(step("(+ 1.0 nil 2)")); - } - - @Test - public void testMinus() { - assertEquals(1L, evalL("(- -1)")); - assertEquals(2L, evalL("(- 3 1)")); - assertEquals(3L, evalL("(- 6 1 2)")); - assertEquals(4L, evalL("(- 10 1 -2 7)")); - - assertCastError(step("(- nil)")); - assertCastError(step("(- 1 [])")); - } - - @Test - public void testDivisionConsistency() { - // Check consistency of quot and rem - assertNotError(step("(map (fn [[a b]] (or (== a (+ (* b (quot a b)) (rem a b) ) ) (fail [a b])) ) [[10 3] [-10 3] [10 -3] [-10 -3] [10000000 1] [1 10000000]])")); - - // Check modular behaviour (mod a b) == (mod (mod a b) b) - assertNotError(step("(map (fn [[a b]] (or (== (mod a b) (mod (mod a b) b)) (fail [a b])) ) [[10 3] [-10 3] [10 -3] [-10 -3] [10000000 1] [1 10000000]])")); - } - - @Test - public void testMinusDouble() { - assertEquals(1.0, evalD("(- -1.0)")); - assertEquals(2.0, evalD("(- 3 1.0)")); - assertEquals(3.0, evalD("(- 6.0 1 2)")); - assertEquals(4.0, evalD("(- 10 1.0 -2 7)")); - assertEquals(5.0, evalD("(- 7.5 2.5)")); - } - - @Test - public void testMinusNoArgs() { - assertArityError(step("(-)")); - } - - @Test - public void testTimes() { - assertEquals(0L, evalL("(* 0 10)")); - assertEquals(1L, evalL("(*)")); - assertEquals(20L, evalL("(* 2 10)")); - assertEquals(120L, evalL("(* 1 2 3 4 5)")); - - // long wrap round 64 bits - assertEquals(0L, evalL("(* 65536 65536 65536 65536)")); - assertEquals(Long.MIN_VALUE, evalL("(* 32768 65536 65536 65536)")); - - assertCastError(step("(* nil)")); - assertCastError(step("(* :foo)")); - } - - @Test - public void testTimesDouble() { - assertEquals(0.0, evalD("(* 0 10.0)")); - assertEquals(5.0, evalD("(* 0.5 10)")); - - assertEquals(50.0, evalD("(* 10 2.5 2)")); - - assertEquals(2.25, evalD("(* -1.5 -1.5)")); - } - - @Test - public void testDouble() { - assertEquals(1.0, evalD("1.0")); - assertEquals(1.0, evalD("(double 1)")); - assertEquals(-1.0, evalD("-1.0")); - assertEquals(Double.NaN, evalD("(double ##NaN)")); - } - - @Test - public void testDivide() { - assertEquals(0.5, evalD("(/ 2.0)"), 0); - assertEquals(0.5, evalD("(/ 1 2.0)"), 0); - assertEquals(0.25, evalD("(/ 1 2 2)"), 0); - assertEquals(-4.0, evalD("(/ 2 -0.5)"), 0); - assertEquals(0.5, evalD("(/ 2)"), 0); - - assertEquals(Double.NaN, evalD("(/ 0.0 0.0)"), 0); - assertEquals(Double.POSITIVE_INFINITY, evalD("(/ 2.0 0.0)"), 0); - assertEquals(Double.NEGATIVE_INFINITY, evalD("(/ -2.0 0.0)"), 0); - - assertCastError(step("(/ nil)")); - assertCastError(step("(/ 1 :foo)")); - assertCastError(step("(/ #7 #0)")); - assertCastError(step("(/ 'foo 1)")); - - assertArityError(step("(/)")); - } - - @Test - public void testNaNPropagation() { - assertEquals(Double.NaN, evalD("##NaN"), 0); - assertEquals(Double.NaN, evalD("(+ 1 ##NaN)"), 0); - assertEquals(Double.NaN, evalD("(/ ##NaN 2)"), 0); - assertEquals(Double.NaN, evalD("(* 1 ##NaN 3.0)"), 0); - } - - @Test - public void testNaNBehaviour() { - assertEquals(Double.NaN, evalD("##NaN"), 0); - assertTrue(evalB("(= ##NaN ##NaN)")); - - // match Java primitives for equality as IEE754. All NaN comparisons should be false - assertFalse(Double.NaN==Double.NaN); - assertFalse(evalB("(== ##NaN ##NaN)")); - assertFalse(evalB("(< ##NaN ##NaN)")); - assertFalse(evalB("(>= ##NaN ##NaN)")); - assertFalse(evalB("(>= ##NaN 1.0)")); - assertFalse(evalB("(< 1 ##NaN)")); - - // TODO: should this be in core? NaN is not equal to itself - // assertTrue(evalB("(!= ##NaN ##NaN)")); - } - - @Test - public void testInfinity() { - assertEquals(CVMDouble.POSITIVE_INFINITY, eval("(/ 1 0)")); - assertEquals(CVMDouble.NEGATIVE_INFINITY, eval("(/ -1 0)")); - - assertEquals(CVMDouble.NEGATIVE_INFINITY, eval("(- ##Inf)")); - assertEquals(CVMDouble.POSITIVE_INFINITY, eval("(- ##-Inf)")); - - assertTrue(evalB("(== ##Inf ##Inf)")); - assertFalse(evalB("(== ##Inf ##-Inf)")); - } - - @Test - public void testZero() { - assertTrue(evalB("(== 0 -0)")); - assertTrue(evalB("(== 0.0 -0.0)")); - assertTrue(evalB("(== 0 -0.0)")); - assertTrue(evalB("(<= 0 -0)")); - assertTrue(evalB("(>= 0 0)")); - - } - - @Test - public void testSignum() { - assertEquals(CVMDouble.NEGATIVE_ZERO,eval("(signum -0.0)")); - assertEquals(CVMDouble.ZERO,eval("(signum 0.0)")); - assertEquals(CVMDouble.ONE,eval("(signum 13.3)")); - assertEquals(CVMDouble.MINUS_ONE,eval("(signum (/ -1 0))")); - assertEquals(CVMDouble.ONE,eval("(signum ##Inf)")); - assertEquals(CVMDouble.NaN,eval("(signum (sqrt -1))")); - } - - - @Test - public void testSqrt() { - assertEquals(2.0, evalD("(sqrt 4.0)"), 0); - assertEquals(0.0, evalD("(sqrt 0.0)"), 0); - assertEquals(Double.NaN, evalD("(sqrt -3)"), 0); - assertEquals(Double.NaN, evalD("(sqrt ##NaN)"), 0); - - assertArityError(step("(sqrt)")); - assertArityError(step("(sqrt :foo :bar)")); // arity before cast error - - assertCastError(step("(sqrt :foo)")); - assertCastError(step("(sqrt nil)")); - - } - - @Test - public void testExp() { - assertEquals(1.0, evalD("(exp 0.0)"), 0); - - assertEquals(StrictMath.exp(1.0), evalD("(exp 1.0)")); - assertEquals(Math.E, evalD("(exp 1.0)"), 0.000001); - assertEquals(0.0, evalD("(exp -100000000.0)"), 0); - assertEquals(Double.POSITIVE_INFINITY, evalD("(exp 100000000)"), 0); - - assertArityError(step("(exp)")); - assertArityError(step("(exp 1 2)")); - } - - @Test - public void testPow() { - assertEquals(1.0, evalD("(pow 1.0 1.0)"), 0); - assertEquals(1.0, evalD("(pow 3.0 0.0)"), 0); - assertEquals(2.0, evalD("(pow 4 0.5)"), 0); - - assertEquals(StrictMath.pow(1.2, 3.5), evalD("(pow 1.2,3.5)")); - assertEquals(0.0, evalD("(pow 2 -100000000.0)"), 0); - assertEquals(Double.POSITIVE_INFINITY,evalD("(pow 3 100000000)"), 0); - - assertEquals(Double.NaN, evalD("(pow -1.0 1.5)"), 0); - assertEquals(-1.0, evalD("(pow -1.0 7)"), 0); - - assertArityError(step("(pow)")); - assertArityError(step("(pow 1)")); - assertArityError(step("(pow 1 2 3)")); - } - - @Test - public void testApply() { - assertEquals(6L, evalL("(apply + [1 2 3])"), 0); - assertEquals(2L, evalL("(apply inc [1])"), 0); - assertEquals(1L, evalL("(apply * [])"), 0); - assertEquals(0L, evalL("(apply + nil)"), 0); - } - - @Test - public void testHexCasts() { - assertEquals(3L, evalL("(+ (long 0x01) (byte 0x02))")); - - // byte cast wraps over - assertSame(CVMByte.create(-1), eval("(byte 0xFF)")); - - // check we are treating blobs as unsigned values - assertEquals(510L, evalL("(+ (long 0xFF) (long 0xFF))")); - assertEquals(-2L, evalL("(+ (long 0xFFFFFFFFFFFFFFFF) (long 0xFFFFFFFFFFFFFFFF))")); - - // take low order bytes of big long - assertEquals(-1L, evalL("(long 0x0000000000000000FFFFFFFFFFFFFFFF)")); - } - - @Test - public void testBadArgs() { - // Regression check for issue #89 - assertCastError(step("(+ 1 #42)")); - assertCastError(step("(+ #42 1.0)")); - } - - @Test - public void testCasts() { - assertEquals(0L, evalL("(long (byte 256))")); - assertEquals(13L, evalL("(long #13)")); - assertEquals(255L, evalL("(long 0xff)")); - assertEquals(1L, evalL("(long 1)")); - assertCVMEquals('a', eval("(char 97)")); - assertEquals(97L, evalL("(long \\a)")); - assertSame(CVMByte.create(1), eval("(byte 1)")); - } - - @Test - public void testBadStringCast() { - assertCastError(step("(inc (str 1))")); - } - -} diff --git a/convex-core/src/test/java/convex/core/lang/OpsTest.java b/convex-core/src/test/java/convex/core/lang/OpsTest.java deleted file mode 100644 index 772819e59..000000000 --- a/convex-core/src/test/java/convex/core/lang/OpsTest.java +++ /dev/null @@ -1,302 +0,0 @@ -package convex.core.lang; - -import static convex.test.Assertions.assertJuiceError; -import static convex.test.Assertions.assertUndeclaredError; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -import convex.core.State; -import convex.core.data.ACell; -import convex.core.data.AMap; -import convex.core.data.AString; -import convex.core.data.Address; -import convex.core.data.ObjectsTest; -import convex.core.data.Symbol; -import convex.core.data.Syntax; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.InvalidDataException; -import convex.core.init.Init; -import convex.core.init.InitTest; -import convex.core.lang.impl.AClosure; -import convex.core.lang.impl.Fn; -import convex.core.lang.ops.Cond; -import convex.core.lang.ops.Constant; -import convex.core.lang.ops.Def; -import convex.core.lang.ops.Do; -import convex.core.lang.ops.Invoke; -import convex.core.lang.ops.Lambda; -import convex.core.lang.ops.Let; -import convex.core.lang.ops.Local; -import convex.core.lang.ops.Lookup; -import convex.core.lang.ops.Special; -import convex.core.util.Utils; - -/** - * Tests for ops functionality. - * - * In general, focused on unit testing special op capabilities. General on-chain - * behaviour should be covered elsewhere. - */ -public class OpsTest extends ACVMTest { - - protected OpsTest() { - super(InitTest.BASE); - } - - private final long INITIAL_JUICE = context().getJuice(); - - @Test - public void testConstant() { - Context c = context(); - - {// simple long constant - AOp op = Constant.of(10L); - Context c2 = c.fork().execute(op); - - assertEquals(INITIAL_JUICE - Juice.CONSTANT, c2.getJuice()); - assertEquals(CVMLong.create(10L), c2.getResult()); - doOpTest(op); - } - - {// null constant - AOp op = Constant.nil(); - Context c2 = c.fork().execute(op); - - assertEquals(INITIAL_JUICE - Juice.CONSTANT, c2.getJuice()); - assertNull(c2.getResult()); - doOpTest(op); - } - } - - @Test - public void testOutOfJuice() { - long JUICE = Juice.CONSTANT - 1; // insufficient juice to run operation - Context c = Context.createInitial(INITIAL, InitTest.HERO, JUICE); - - AOp op = Constant.of(10L); - assertJuiceError(c.execute(op)); - - doOpTest(op); - } - - @Test - public void testDef() { - Context c1 = context(); - - Symbol fooSym = Symbol.create("foo"); - AOp op = Def.create(Syntax.create(fooSym), Constant.createString("bar")); - - AMap env1 = c1.getEnvironment(); - Context c2 = c1.execute(op); - AMap env2 = c2.getEnvironment(); - - assertNotEquals(env1, env2); - - assertNull(env1.get(fooSym)); // initially no entry - assertEquals("bar", env2.get(fooSym).toString()); - - long expectedJuice = INITIAL_JUICE - Juice.CONSTANT - Juice.DEF; - assertEquals(expectedJuice, c2.getJuice()); - assertEquals("bar", c2.getResult().toString()); - - AOp lookupOp = Lookup.create(Symbol.create("foo")); - Context c3 = c2.execute(lookupOp); - expectedJuice -= Juice.LOOKUP_DYNAMIC; - assertEquals(expectedJuice, c3.getJuice()); - assertEquals("bar", c3.getResult().toString()); - - doOpTest(op); - doOpTest(lookupOp); - } - - @Test - public void testUndeclaredLookup() { - Context c = context(); - AOp op = Lookup.create("missing-symbol"); - assertUndeclaredError(c.execute(op)); - - doOpTest(op); - } - - @Test - public void testDo() { - Context c = context(); - - AOp op = Do.create(Def.create("foo", Constant.createString("bar")), Lookup.create("foo")); - - Context c2 = c.execute(op); - long expectedJuice = INITIAL_JUICE - (Juice.CONSTANT + Juice.DEF + Juice.LOOKUP_DYNAMIC + Juice.DO); - assertEquals(expectedJuice, c2.getJuice()); - assertEquals("bar", c2.getResult().toString()); - - doOpTest(op); - } - - @Test - public void testSpecial() { - Context c = context(); - - AOp
op = Special.forSymbol(Symbols.STAR_ADDRESS); - assertEquals(op,Special.forSymbol(Symbol.create("*address*"))); // double check lookup in hash map - - Context
c2 = c.execute(op); - assertEquals(c2.getAddress(), c2.getResult()); - - doOpTest(op); - } - - @Test - public void testLet() { - Context c = context(); - AOp op = Let.create(Vectors.of(Symbols.FOO), - Vectors.of(Constant.createString("bar"), Local.create(0)), false); - Context c2 = c.execute(op); - assertEquals("bar", c2.getResult().toString()); - - doOpTest(op); - } - - @Test - public void testCondTrue() { - Context c = context(); - - AOp op = Cond.create(Constant.of(true), Constant.createString("trueResult"), - Constant.createString("falseResult")); - - Context c2 = c.execute(op); - - assertEquals("trueResult", c2.getResult().toString()); - long expectedJuice = INITIAL_JUICE - (Juice.COND_OP + Juice.CONSTANT + Juice.CONSTANT); - assertEquals(expectedJuice, c2.getJuice()); - - doOpTest(op); - } - - @Test - public void testCondFalse() { - Context c = context(); - - AOp op = Cond.create(Constant.of(false), Constant.createString("trueResult"), - Constant.createString("falseResult")); - - Context c2 = c.execute(op); - - assertEquals("falseResult", c2.getResult().toString()); - long expectedJuice = INITIAL_JUICE - (Juice.COND_OP + Juice.CONSTANT + Juice.CONSTANT); - assertEquals(expectedJuice, c2.getJuice()); - - doOpTest(op); - } - - @Test - public void testCondNoResult() { - Context c = context(); - - AOp op = Cond.create(Constant.of(false), Constant.createString("trueResult")); - - Context c2 = c.execute(op); - - assertNull(c2.getResult()); - long expectedJuice = INITIAL_JUICE - (Juice.COND_OP + Juice.CONSTANT); - assertEquals(expectedJuice, c2.getJuice()); - - doOpTest(op); - } - - @Test - public void testCondEnvironmentChange() { - Context c = context(); - - Symbol sym = Symbol.create("val"); - - AOp op = Cond.create(Do.create(Def.create(sym, Constant.of(false)), Constant.of(false)), - Constant.createString("1"), Lookup.create(sym), Constant.of("2"), - Do.create(Def.create(sym, Constant.of(true)), Constant.of(false)), Constant.of("3"), - Lookup.create(sym), Constant.of("4"), Constant.of("5")); - - Context c2 = c.execute(op); - assertEquals("4", c2.getResult().toString()); - - doOpTest(op); - } - - @Test - public void testInvoke() { - Context c = context(); - - Symbol sym = Symbol.create("arg0"); - - Invoke op = Invoke.create(Lambda.create(Vectors.of(sym), Local.create(0)), - Constant.createString("bar")); - - Context c2 = c.execute(op); - assertEquals("bar", c2.getResult().toString()); - - doOpTest(op); - } - - @Test - public void testLookup() throws InvalidDataException { - Lookup l1=Lookup.create("foo"); - assertNull(l1.getAddress()); - doOpTest(l1); - - Lookup l2=Lookup.create(Constant.of(Init.CORE_ADDRESS),"count"); - assertEquals(Constant.of(Init.CORE_ADDRESS),l2.getAddress()); - doOpTest(l2); - } - - @Test - public void testLocal() throws InvalidDataException { - Context c=Context.createFake(State.EMPTY); - c=c.withLocalBindings(Vectors.of(1337L)); - - Local op=Local.create(0); - c=c.execute(op); - assertEquals(RT.cvm(1337),c.getResult()); - - doOpTest(op); - } - - @Test - public void testLambda() { - Context c = context(); - - Symbol sym = Symbol.create("arg0"); - - Lambda lam = Lambda.create(Vectors.of(Syntax.create(sym)), Lookup.create(sym)); - - Context> c2 = c.execute(lam); - AClosure fn = c2.getResult(); - assertTrue(fn.hasArity(1)); - assertFalse(fn.hasArity(2)); - - doOpTest(lam); - } - - @Test - public void testLambdaString() { - Fn fn = Fn.create(Vectors.empty(), Constant.nil()); - assertEquals("(fn [] nil)",fn.toString()); - } - - public void doOpTest(AOp op) { - // Executing any Op should not throw - context().execute(op); - - try { - op.validate(); - } catch (InvalidDataException e) { - throw Utils.sneakyThrow(e); - } - - ObjectsTest.doAnyValueTests(op); - } - -} diff --git a/convex-core/src/test/java/convex/core/lang/ParamTestCasts.java b/convex-core/src/test/java/convex/core/lang/ParamTestCasts.java deleted file mode 100644 index c47f3f308..000000000 --- a/convex-core/src/test/java/convex/core/lang/ParamTestCasts.java +++ /dev/null @@ -1,64 +0,0 @@ -package convex.core.lang; - -import static convex.core.lang.TestState.eval; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import convex.core.ErrorCodes; -import convex.core.data.ACell; -import convex.core.data.Sets; -import convex.core.data.Strings; -import convex.core.data.prim.CVMDouble; -import convex.core.data.prim.CVMLong; -import convex.test.Samples; - -public class ParamTestCasts { - - public final ACell[] values = new ACell[] { - CVMLong.ZERO, - CVMLong.ONE, - CVMLong.MAX_VALUE, - CVMLong.MIN_VALUE, - CVMDouble.NaN, - CVMDouble.POSITIVE_INFINITY, - CVMDouble.NEGATIVE_INFINITY, - CVMDouble.ZERO, - Strings.EMPTY, - Strings.create("foobar"), - Samples.ACCOUNT_KEY, - Samples.BAD_HASH, - Samples.INT_SET_300, - Samples.INT_VECTOR_23, - Samples.LONG_MAP_100, - null - }; - - - @SuppressWarnings("unchecked") - @ParameterizedTest - @ValueSource(strings = { "long","boolean","double","byte","char","blob","str","vec","set","blob","address"}) - public void testIdempotent(String name) { - ACell namedFn=eval(name); - - assertTrue(namedFn instanceof AFn); - AFn fn=(AFn)namedFn; - - Context ctx=TestState.CONTEXT.fork(); - - for (ACell x: values) { - ACell[] args= new ACell[] {x}; - Context r = ctx.fork().invoke(fn, args); - if (r.isExceptional()) { - ACell code=r.getExceptional().getCode(); - assertTrue(Sets.of(ErrorCodes.CAST,ErrorCodes.ARGUMENT).contains(code)); - } else { - ACell result=r.getResult(); - ACell result2=r.invoke(fn, args).getResult(); - assertEquals(result,result2); - } - } - } -} diff --git a/convex-core/src/test/java/convex/core/lang/ParamTestEvals.java b/convex-core/src/test/java/convex/core/lang/ParamTestEvals.java deleted file mode 100644 index 9ed7f95f1..000000000 --- a/convex-core/src/test/java/convex/core/lang/ParamTestEvals.java +++ /dev/null @@ -1,122 +0,0 @@ -package convex.core.lang; - -import static convex.test.Assertions.assertCVMEquals; -import static org.junit.Assert.assertEquals; - -import java.util.Arrays; -import java.util.Collection; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.data.Blob; -import convex.core.data.Format; -import convex.core.data.Keyword; -import convex.core.exceptions.BadFormatException; -import convex.core.init.InitTest; -import convex.core.util.Utils; - -@RunWith(Parameterized.class) -public class ParamTestEvals { - - private static long INITIAL_JUICE = TestState.INITIAL_JUICE; - private static final Context INITIAL_CONTEXT = TestState.CONTEXT.fork(); - - private static final Address TEST_CONTRACT = TestState.CONTRACTS[0]; - - @Parameterized.Parameters(name = "{index}: {0}") - public static Collection dataExamples() { - return Arrays.asList(new Object[][] { - { "(do)", null }, - { "(do (do :foo))", Keyword.create("foo") }, - { "(do 1 2)", 2L }, - { "(do 1 *result*)", 1L }, - { "(do (do :foo) (do))", null }, - { "*result*", null }, - { "*origin*", InitTest.HERO }, - { "*caller*", null }, - { "*address*", InitTest.HERO }, - { "(do 1 *result*)", 1L }, - - { "(call " + TEST_CONTRACT + " (my-address))", TEST_CONTRACT }, - { "(call " + TEST_CONTRACT + " (foo))", Keyword.create("bar") }, - - { "(let [a (address " + TEST_CONTRACT + ")]" + "(call a (write :bar))" + "(call a (read)))", - Keyword.create("bar") }, - - { "*depth*", 0L }, // *depth* - { "(do *depth*)", 1L }, // do, *depth* - { "(let [a *depth*] a)", 1L }, // let, *depth* - { "(let [f (fn [] *depth*)] (f))", 2L }, // let, invoke, *depth* - - { "(let [])", null }, { "(let [a 1])", null }, { "(let [a 1] a)", 1L }, - { "(do (def a 2) (let [a 13] a))", 13L }, { "*juice*", INITIAL_JUICE }, - { "(- *juice* *juice*)", Juice.SPECIAL }, - { "((fn [a] a) 4)", 4L }, { "(do (def a 3) a)", 3L }, - { "(do (let [a 1] (def f (fn [] a))) (f))", 1L }, { "1", 1L }, { "(not true)", false }, - { "(= true true)", true } }); - } - - private String source; - private Object expectedResult; - - public ParamTestEvals(String source, Object expectedResult) { - this.source = source; - this.expectedResult = expectedResult; - } - - public AOp compile(String source) { - try { - Context c = INITIAL_CONTEXT.fork(); - AOp op = TestState.compile(c, source); - return op; - } catch (Exception e) { - throw Utils.sneakyThrow(e); - } - } - - public Context eval(AOp op) { - try { - Context c = INITIAL_CONTEXT.fork(); - Context rc = c.execute(op); - return rc; - } catch (Exception e) { - throw Utils.sneakyThrow(e); - } - } - - public Context eval(String source) { - try { - Context c = INITIAL_CONTEXT.fork(); - AOp op = TestState.compile(c, source); - return eval(op); - } catch (Exception e) { - throw Utils.sneakyThrow(e); - } - } - - @Test - public void testOpRoundTrip() throws BadFormatException { - AOp op = compile(source); - Blob b = Format.encodedBlob(op); - ACell.createPersisted(op); // persist to allow re-creation - - AOp op2 = Format.read(b); - Blob b2 = Format.encodedBlob(op2); - assertEquals(b, b2); - - ACell result = eval(op2).getResult(); - assertCVMEquals(expectedResult, result); - } - - @Test - public void testResultAndJuice() { - Context c = eval(source); - - Object result = c.getResult(); - assertCVMEquals(expectedResult, result); - } -} diff --git a/convex-core/src/test/java/convex/core/lang/ParamTestJuice.java b/convex-core/src/test/java/convex/core/lang/ParamTestJuice.java deleted file mode 100644 index 53cbe5247..000000000 --- a/convex-core/src/test/java/convex/core/lang/ParamTestJuice.java +++ /dev/null @@ -1,126 +0,0 @@ -package convex.core.lang; - -import static convex.test.Assertions.assertCVMEquals; -import static org.junit.Assert.assertEquals; - -import java.util.Arrays; -import java.util.Collection; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import convex.core.State; -import convex.core.data.ACell; -import convex.core.data.Blob; -import convex.core.data.Format; -import convex.core.data.Keyword; -import convex.core.data.Lists; -import convex.core.data.Maps; -import convex.core.exceptions.BadFormatException; -import convex.core.init.InitTest; -import convex.core.util.Utils; - -@RunWith(Parameterized.class) -public class ParamTestJuice { - - private static final long JUICE_SYM_LOOKUP = Juice.LOOKUP_SYM; - private static final long JUICE_EMPTY_MAP = (Juice.BUILD_DATA + JUICE_SYM_LOOKUP); // consider: (hash-map) - private static final long JUICE_IDENTITY_FN = (Juice.LAMBDA); - - @Parameterized.Parameters(name = "{index}: {0}") - public static Collection dataExamples() { - return Arrays.asList(new Object[][] { - { "3", 3L, Juice.CONSTANT }, - { "'()", Lists.empty(), Juice.CONSTANT }, - { "{}", Maps.empty(), JUICE_EMPTY_MAP }, // (hash-map) - { "(hash-map)", Maps.empty(), JUICE_EMPTY_MAP }, // (hash-map) - { "(eval 1)", 1L, - (Juice.EVAL + JUICE_SYM_LOOKUP + Juice.CONSTANT) + Juice.EXPAND_CONSTANT + Juice.COMPILE_CONSTANT - + Juice.CONSTANT }, - { "(do)", null, Juice.DO }, { "({} 0 1)", 1L, JUICE_EMPTY_MAP + Juice.CONSTANT * 2 }, - { "(do (do :foo))", Keyword.create("foo"), Juice.DO * 2 + Juice.CONSTANT }, - { "(let [])", null, Juice.LET }, { "(cond)", null, Juice.COND_OP }, - { "(if 1 2 3)", 2L, Juice.COND_OP + 2 * Juice.CONSTANT }, - { "(fn [x] x)", eval("(fn [x] x)").getResult(), JUICE_IDENTITY_FN }, - { "(do (def a 3) a)", 3L, Juice.DO + Juice.CONSTANT + JUICE_SYM_LOOKUP + Juice.DEF }, - { "(do (let [a 1] (def f (fn [] a))) (f))", 1L, - Juice.DO + Juice.LET + Juice.CONSTANT * 1 + JUICE_SYM_LOOKUP + Juice.LOOKUP + JUICE_IDENTITY_FN - + Juice.DEF }, - { "(let [a 1] a)", 1L, Juice.LET + Juice.LOOKUP + Juice.CONSTANT }, { "~(+ 1 2)", 3L, Juice.CONSTANT }, // compiler - // executes - // + - // in - // advance, - // so - // this - // is - // constant - { "*depth*", 0L, Juice.SPECIAL }, - { "(= true true)", true, (1 * JUICE_SYM_LOOKUP) + (2 * Juice.CONSTANT) + Juice.EQUALS } }); - } - - private String source; - private long expectedJuice; - private Object expectedResult; - - public ParamTestJuice(String source, Object expectedResult, Long expectedJuice) { - this.source = source; - this.expectedJuice = expectedJuice; - this.expectedResult = expectedResult; - } - - private static final State INITIAL = TestState.STATE; - private static final long INITIAL_JUICE = 10000; - private static final Context INITIAL_CONTEXT; - - static { - try { - INITIAL_CONTEXT = Context.createInitial(INITIAL, InitTest.HERO, INITIAL_JUICE); - } catch (Throwable e) { - throw new Error(e); - } - } - - public static AOp compile(String source) { - try { - Context c = INITIAL_CONTEXT.fork(); - AOp op = TestState.compile(c, source); - return op; - } catch (Exception e) { - throw Utils.sneakyThrow(e); - } - } - - public static Context eval(String source) { - try { - Context c = INITIAL_CONTEXT.fork(); - AOp op = TestState.compile(c, source); - Context rc = c.fork().execute(op); - return rc; - } catch (Exception e) { - throw Utils.sneakyThrow(e); - } - } - - @Test - public void testOpRoundTrip() throws BadFormatException { - AOp op = compile(source); - Blob b = Format.encodedBlob(op); - AOp op2 = Format.read(b); - Blob b2 = Format.encodedBlob(op2); - assertEquals(b, b2); - } - - @Test - public void testResultAndJuice() { - Context c = eval(source); - - Object result = c.getResult(); - assertCVMEquals(expectedResult, result); - - long juiceUsed = INITIAL_JUICE - c.getJuice(); - assertEquals(expectedJuice, juiceUsed); - - } -} diff --git a/convex-core/src/test/java/convex/core/lang/ParamTestRTSequences.java b/convex-core/src/test/java/convex/core/lang/ParamTestRTSequences.java deleted file mode 100644 index a16eaa63c..000000000 --- a/convex-core/src/test/java/convex/core/lang/ParamTestRTSequences.java +++ /dev/null @@ -1,82 +0,0 @@ -package convex.core.lang; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static convex.test.Assertions.*; - -import java.util.Arrays; -import java.util.Collection; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import convex.core.data.ACell; -import convex.core.data.ACollection; -import convex.core.data.ASequence; -import convex.core.data.AVector; -import convex.core.data.List; -import convex.core.data.Lists; -import convex.core.data.MapEntry; -import convex.core.data.Maps; -import convex.core.data.Sets; -import convex.core.data.Vectors; - -/** - * Set of test for objects that can be treated as sequences - * - */ -@RunWith(Parameterized.class) -public class ParamTestRTSequences { - @Parameterized.Parameters(name = "{index}: {1}") - public static Collection dataExamples() { - return Arrays.asList(new Object[][] { { 0, null }, { 0, Vectors.empty() }, { 0, Lists.empty() }, - { 2, MapEntry.of(1L, 2L) }, { 2, MapEntry.of(Maps.of(1L, 2L), 2L) }, - { 2, MapEntry.of(null, 2L) }, { 3, Vectors.of(1L, 2L, 3L) }, { 2, List.of("foo", "bar") }, - { 3, Sets.of(null, 1L, 1.0) } }); - } - - private ACollection data; - private long expectedCount; - - public ParamTestRTSequences(int expectedCount, Object data) { - this.expectedCount = expectedCount; - this.data = (ACollection)data; - } - - @Test - public void testCount() { - assertEquals(expectedCount, RT.count(data)); - } - - @Test - public void testSeq() { - assertEquals(expectedCount, RT.count(RT.sequence(data))); - } - - @Test - public void testVec() { - AVector v = RT.vec(data); - assertEquals(expectedCount, v.count()); - if (expectedCount > 0) { - assertEquals(data.get(0), v.get(0)); - } - } - - @Test - public void testCons() { - ASequence a = RT.cons(RT.cvm("foo"), RT.sequence(data)); - assertCVMEquals("foo", a.get(0)); - assertCVMEquals("foo", RT.nth(a, 0)); - } - - @Test - public void testFirst() { - if (expectedCount > 0) { - ACell fst = data.get(0); - assertEquals(RT.nth(data, 0), fst); - } else { - if (data!=null) assertThrows(IndexOutOfBoundsException.class, () -> data.get(0)); - } - } -} diff --git a/convex-core/src/test/java/convex/core/lang/RTTest.java b/convex-core/src/test/java/convex/core/lang/RTTest.java deleted file mode 100644 index 66599b13b..000000000 --- a/convex-core/src/test/java/convex/core/lang/RTTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package convex.core.lang; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; - -import org.junit.jupiter.api.Test; - -import convex.core.data.AList; -import convex.core.data.AVector; -import convex.core.data.Address; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.Lists; -import convex.core.data.Strings; -import convex.core.data.Symbol; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMDouble; -import convex.core.data.prim.CVMLong; - -/** - * Tests for RT functions. - * - * Normally should prefer testing via executing core environment functions, but - * these are useful for testing utility functions, edge cases and internal - * behaviour of the RT class itself. - */ -public class RTTest { - - @Test - public void testName() { - assertEquals("foo", RT.name(Symbol.create("foo")).toString()); - assertEquals("foo", RT.name(Keyword.create("foo")).toString()); - assertEquals("foo", RT.name(Strings.create("foo")).toString()); - - assertNull(RT.name(null)); - } - - @Test - public void testAddress() { - Address za = Address.create(0x7777777); - assertEquals(za, RT.castAddress(za.toBlob())); - assertSame(za, RT.castAddress(za)); - - // reading a hex address - assertEquals(Address.create(18),RT.castAddress(Strings.create("0000000000000012"))); // OK, hex string - assertNull(RT.castAddress(Strings.create("0012"))); // too short - - // Check null return values for invalid addresses - assertNull(RT.castAddress(null)); // null not allowed - assertNull(RT.castAddress(CVMLong.create(-1))); // negative ints not allowed - assertNull(RT.castAddress(Strings.create("xyz2030405060708090a0b0c0d0e0f1011121314"))); // bad format - } - - @Test - public void testSequence() { - AVector v = Vectors.of(1L, 2L, 3L); - AList l = Lists.of(1L, 2L, 3L); - assertSame(v, RT.sequence(v)); - assertSame(l, RT.sequence(l)); - assertSame(Vectors.empty(), RT.sequence(null)); - - // null return values if cast fails - assertNull(RT.sequence(Keywords.FOO)); // keywords not allowed - } - - @Test - public void testVec() { - AVector v = Vectors.of(1L, 2L, 3L); - AList l = Lists.of(1L, 2L, 3L); - assertEquals(Vectors.of(1L, 2L,3L), RT.vec(l.toCellArray())); - assertEquals(v, RT.vec(new java.util.ArrayList<>(v))); - - assertNull(RT.vec(1)); // ints not allowed - } - - @Test - public void testCVMCasts() { - assertEquals(CVMLong.create(1L),RT.cvm(1L)); - assertEquals(CVMDouble.create(0.17),RT.cvm(0.17)); - assertEquals(Strings.create("foo"),RT.cvm("foo")); - - // CVM objects shouldn't change - Keyword k=Keyword.create("test-key"); - assertSame(k,RT.cvm(k)); - - } -} diff --git a/convex-core/src/test/java/convex/core/lang/ReaderTest.java b/convex-core/src/test/java/convex/core/lang/ReaderTest.java deleted file mode 100644 index 759eb08e2..000000000 --- a/convex-core/src/test/java/convex/core/lang/ReaderTest.java +++ /dev/null @@ -1,273 +0,0 @@ -package convex.core.lang; - -import static convex.test.Assertions.assertCVMEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; - -import convex.core.data.ACell; -import convex.core.data.AList; -import convex.core.data.Address; -import convex.core.data.Blob; -import convex.core.data.Blobs; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.Lists; -import convex.core.data.Maps; -import convex.core.data.Strings; -import convex.core.data.Symbol; -import convex.core.data.Syntax; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMBool; -import convex.core.data.prim.CVMDouble; -import convex.core.exceptions.ParseException; -import convex.test.Samples; - -public class ReaderTest { - - @Test - public void testVectors() { - assertSame(Vectors.empty(), Reader.read("[]")); - assertSame(Vectors.empty(), Reader.read(" [ ] ")); - - assertEquals(Vectors.of(1L,-2L), Reader.read("[1 -2]")); - - assertEquals(Vectors.of(Samples.FOO), Reader.read(" [ :foo ] ")); - assertEquals(Vectors.of(Vectors.empty()), Reader.read(" [ [] ] ")); - } - - @Test - public void testKeywords() { - assertEquals(Samples.FOO, Reader.read(":foo")); - assertEquals(Keyword.create("foo.bar"), Reader.read(":foo.bar")); - - // : is currently a valid symbol character - assertEquals(Keyword.create("foo:bar"), Reader.read(":foo:bar")); - - } - - @Test - public void testBadKeywords() { - assertThrows(Error.class, () -> Reader.read(":")); - } - - @Test - public void testComment() { - assertCVMEquals(1L, Reader.read(";this is a comment\n 1 \n")); - assertCVMEquals(Vectors.of(2L), Reader.read("[#_foo 2]")); - assertCVMEquals(Vectors.of(3L), Reader.read("[3 #_foo]")); - } - - @Test - public void testSymbols() { - assertEquals(Symbols.FOO, Reader.read("foo")); - assertEquals(Lists.of(Symbols.LOOKUP,Address.create(666),Symbols.FOO), Reader.read("#666/foo")); - - assertEquals(Lists.of(Symbol.create("+"), 1L), Reader.read("(+ 1)")); - assertEquals(Lists.of(Symbol.create("+a")), Reader.read("( +a )")); - assertEquals(Lists.of(Symbol.create("/")), Reader.read("(/)")); - assertEquals(Lists.of(Symbols.LOOKUP,Symbols.FOO,Symbols.BAR), Reader.read("foo/bar")); - assertEquals(Symbol.create("a*+!-_?<>=!"), Reader.read("a*+!-_?<>=!")); - assertEquals(Symbol.create("foo.bar"), Reader.read("foo.bar")); - assertEquals(Symbol.create(".bar"), Reader.read(".bar")); - - // Interpret leading dot as symbols always. Addresses Issue #65 - assertEquals(Symbol.create(".56"), Reader.read(".56")); - - // TODO: maybe this should be possible? - // namespaces cannot themselves be qualified - //assertThrows(ParseException.class,()->Reader.read("a/b/c")); - - // Bad address parsing - assertThrows(ParseException.class,()->Reader.read("#-1/foo")); - - // too long symbol names - assertThrows(ParseException.class,()->Reader.read("abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmnop")); - assertThrows(ParseException.class,()->Reader.read("abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmnop/a")); - assertThrows(ParseException.class,()->Reader.read("a/abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmnop")); - - } - - @Test - public void testSymbolPath() { - ACell form=Reader.read("foo/bar/baz"); - assertEquals(Lists.of(Symbols.LOOKUP,Lists.of(Symbols.LOOKUP,Symbols.FOO,Symbols.BAR),Symbols.BAZ),form) ; - } - - @Test - public void testSymbolsRegressionCases() { - assertEquals(Symbol.create("nils"), Reader.read("nils")); - - // symbol starting with a boolean value - assertEquals(Symbol.create("falsey"), Reader.read("falsey")); - assertEquals(Symbol.create("true-exp"), Reader.read("true-exp")); - } - - @Test - public void testChar() { - assertCVMEquals('A', Reader.read("\\A")); - assertCVMEquals('a', Reader.read("\\u0061")); - assertCVMEquals(' ', Reader.read("\\space")); - assertCVMEquals('\t', Reader.read("\\tab")); - assertCVMEquals('\n', Reader.read("\\newline")); - assertCVMEquals('\f', Reader.read("\\formfeed")); - assertCVMEquals('\b', Reader.read("\\backspace")); - assertCVMEquals('\r', Reader.read("\\return")); - } - - @Test - public void testNumbers() { - assertCVMEquals(1L, Reader.read("1")); - assertCVMEquals(2.0, Reader.read("2.0")); - - // scientific notation - assertCVMEquals(2.0, Reader.read("2.0e0")); - assertCVMEquals(20.0, Reader.read("2.0e1")); - assertCVMEquals(0.2, Reader.read("2.0e-1")); - assertCVMEquals(12.0, Reader.read("12e0")); - - assertThrows(Error.class, () -> { - Reader.read("2.0e0.1234"); - }); - // assertNull( Reader.read("[2.0e0.1234]")); - // TODO: do we want this? - //assertThrows(Error.class, () -> Reader.read("[2.0e0.1234]")); // Issue #70 - - // metadata ignored - assertEquals(Syntax.create(RT.cvm(3.23),Maps.of(Keywords.FOO, CVMBool.TRUE)), Reader.read("^:foo 3.23")); - } - - @Test - public void testSpecialNumbers() { - assertEquals(CVMDouble.NaN, Reader.read("##NaN")); - assertEquals(CVMDouble.POSITIVE_INFINITY, Reader.read("##Inf ")); - assertEquals(CVMDouble.NEGATIVE_INFINITY, Reader.read(" ##-Inf")); - } - - @Test - public void testHexBlobs() { - assertEquals(Blobs.fromHex("cafebabe"), Reader.read("0xcafebabe")); - assertEquals(Blobs.fromHex("0aA1"), Reader.read("0x0Aa1")); - assertEquals(Blob.EMPTY, Reader.read("0x")); - - // TODO: figure out the edge case - assertThrows(Error.class, () -> Reader.read("0x1")); - //assertThrows(Error.class, () -> Reader.read("[0x1]")); // odd number of hex digits - - assertThrows(Error.class, () -> Reader.read("0x123")); // odd number of hex digits - } - - @Test - public void testNil() { - assertNull(Reader.read("nil")); - - // metadata on null - assertEquals(Syntax.create(null),Reader.read("^{} nil")); - } - - @Test - public void testStrings() { - assertSame(Strings.empty(), Reader.read("\"\"")); - assertEquals(Strings.create("bar"), Reader.read("\"bar\"")); - assertEquals(Vectors.of(Strings.create("bar")), Reader.read("[\"bar\"]")); - assertEquals(Strings.create("\"bar\""), Reader.read("\"\\\"bar\\\"\"")); - - } - - @Test - public void testList() { - assertSame(Lists.empty(), Reader.read(" ()")); - assertEquals(Lists.of(1L, 2L), Reader.read("(1 2)")); - assertEquals(Lists.of(Vectors.empty()), Reader.read(" ([] )")); - } - - @Test - public void testNoWhiteSpace() { - assertEquals(Lists.of(Vectors.empty(), Vectors.empty()), Reader.read("([][])")); - assertEquals(Lists.of(Vectors.empty(), 13L), Reader.read("([]13)")); - assertEquals(Lists.of(Symbols.SET, Vectors.empty()), Reader.read("(set[])")); - } - - @Test - public void testMaps() { - assertSame(Maps.empty(), Reader.read("{}")); - assertEquals(Maps.of(1L, 2L), Reader.read("{1,2}")); - assertEquals(Maps.of(Samples.FOO, Samples.BAR), Reader.read("{:foo :bar}")); - } - - @Test - public void testMapError() { - assertThrows(ParseException.class,()->Reader.read("{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}")); - } - - @Test - public void testQuote() { - assertEquals(Lists.of(Symbols.QUOTE, 1L), Reader.read("'1")); - assertEquals(Lists.of(Symbols.QUOTE, Lists.of(Symbols.QUOTE, Vectors.empty())), Reader.read("''[]")); - - assertEquals(Lists.of(Symbols.QUOTE,Lists.of(Symbols.UNQUOTE,Symbols.FOO)),Reader.read("'~foo")); - - } - - - @Test - public void testTooManyClosingParens() { - // See #244 - assertThrows(ParseException.class, () -> Reader.read("(42))))")); - } - - - @Test - public void testWrongSizeMaps() { - assertThrows(ParseException.class, () -> Reader.read("{:foobar}")); - } - - @Test - public void testParsingNothing() { - assertThrows(ParseException.class, () -> Reader.read(" ")); - } - - @Test - public void testSyntaxReader() { - assertEquals(Syntax.class, Reader.readSyntax("nil").getClass()); - assertEquals(Syntax.create(RT.cvm(1L)), Reader.readSyntax("1").withoutMeta()); - assertEquals(Syntax.create(Symbols.FOO), Reader.readSyntax("foo").withoutMeta()); - assertEquals(Syntax.create(Keywords.FOO), Reader.readSyntax(":foo").withoutMeta()); - } - - @Test - public void testReadMetadata() { - assertEquals(Syntax.create(Keywords.FOO),Reader.read("^{} :foo")); - } - - @SuppressWarnings("unchecked") - @Test - public void testMetadata() { - assertCVMEquals(Boolean.TRUE, Reader.readSyntax("^:foo a").getMeta().get(Keywords.FOO)); - - { - AList def=(AList) Reader.readAll("(def ^{:foo 2} a 1)").get(0); - Syntax form=(Syntax) def.get(1); - - assertCVMEquals(2L, form.getMeta().get(Keywords.FOO)); - } - - } - - @Test public void testIdempotentPrint() { - doIdempotencyTest(Address.create(12345)); - doIdempotencyTest(Samples.LONG_MAP_10); - doIdempotencyTest(Samples.BAD_HASH); - doIdempotencyTest(Samples.MAX_EMBEDDED_STRING); - doIdempotencyTest(Reader.readAll("(def ^{:foo 2} a 1)")); - doIdempotencyTest(Reader.readAll("(fn ^{:foo 2} [] bar/baz)")); - } - - public void doIdempotencyTest(ACell cell) { - String s=cell.print(); - assertEquals(s,Reader.read(s).print()); - } -} diff --git a/convex-core/src/test/java/convex/core/lang/SyntaxTest.java b/convex-core/src/test/java/convex/core/lang/SyntaxTest.java deleted file mode 100644 index 5f8e1dc45..000000000 --- a/convex-core/src/test/java/convex/core/lang/SyntaxTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package convex.core.lang; - -import static convex.test.Assertions.assertCVMEquals; - -import org.junit.jupiter.api.Test; - -import convex.core.data.ObjectsTest; -import convex.core.data.Syntax; - -public class SyntaxTest { - - @Test - public void testSyntaxConstructor() { - Syntax s = Syntax.create(RT.cvm(1L)); - assertCVMEquals(1L, s.getValue()); - - ObjectsTest.doAnyValueTests(s); - } -} diff --git a/convex-core/src/test/java/convex/core/lang/TestState.java b/convex-core/src/test/java/convex/core/lang/TestState.java deleted file mode 100644 index d85281193..000000000 --- a/convex-core/src/test/java/convex/core/lang/TestState.java +++ /dev/null @@ -1,252 +0,0 @@ -package convex.core.lang; - -import static convex.test.Assertions.assertCVMEquals; -import static convex.test.Assertions.assertStateError; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; - -import java.io.IOException; - -import org.junit.jupiter.api.Test; - -import convex.core.Constants; -import convex.core.State; -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.data.Keyword; -import convex.core.data.prim.CVMBool; -import convex.core.data.prim.CVMDouble; -import convex.core.data.prim.CVMLong; -import convex.core.init.InitTest; -import convex.core.util.Utils; - -/** - * Class for building and testing a State for the unit test suite. - * - * Includes example smart contracts. - */ -public class TestState { - public static final int NUM_CONTRACTS = 5; - - public static final Address[] CONTRACTS = new Address[NUM_CONTRACTS]; - - /** - * A test state set up with a few accounts - */ - public static final State STATE; - - - static { - - try { - - State s = InitTest.STATE; - Context ctx = Context.createFake(s, InitTest.HERO); - for (int i = 0; i < NUM_CONTRACTS; i++) { - // Construct code for each contract - ACell contractCode = Reader.read( - "(do " + "(def my-data nil)" + "(defn write ^{:callable? true} [x] (def my-data x)) " - + "(defn read ^{:callable? true} [] my-data)" + "(defn who-called-me ^{:callable? true} [] *caller*)" - + "(defn my-address ^{:callable? true} [] *address*)" + "(defn my-number ^{:callable? true} [] "+i+")" + "(defn foo ^{:callable? true} [] :bar))"); - - ctx = ctx.deployActor(contractCode); - CONTRACTS[i] = (Address) ctx.getResult(); - } - - s= ctx.getState(); - STATE = s; - CONTEXT = Context.createFake(STATE, InitTest.HERO); - } catch (Throwable e) { - e.printStackTrace(); - throw new Error(e); - } - } - - /** - * Initial juice for TestState.INITIAL_CONTEXT - */ - public static final long INITIAL_JUICE = Constants.MAX_TRANSACTION_JUICE; - - /** - * Initial juice price - */ - public static final CVMLong JUICE_PRICE = STATE.getJuicePrice(); - - /** - * A test context set up with a few accounts - */ - public static final Context CONTEXT; - - - - /** - * Total funds in the test state, minus those subtracted for juice in the - * initial context - */ - public static final Long TOTAL_FUNDS = Constants.MAX_SUPPLY; - - - - - @SuppressWarnings("unchecked") - static AOp compile(Context c, String source) { - c=c.fork(); - try { - ACell form = Reader.read(source); - AOp op = (AOp) c.expandCompile(form).getResult(); - return op; - } catch (Exception e) { - throw Utils.sneakyThrow(e); - } - } - - public static T eval(Context c, String source) { - c=c.fork(); - try { - AOp op = compile(c, source); - Context rc = c.run(op); - return rc.getResult(); - } catch (Exception e) { - throw Utils.sneakyThrow(e); - } - } - - // Deploy actor code directly into a Context - public static Context deploy(Context ctx,String actorResource) { - String source; - try { - source = Utils.readResourceAsString(actorResource); - ACell contractCode=Reader.read(source); - ctx=ctx.deployActor(contractCode); - } catch (IOException e) { - e.printStackTrace(); - } - return ctx; - } - - @Test - public void testInitial() { - Context ctx = Context.createFake(STATE,InitTest.HERO); - State s = ctx.getState(); - assertEquals(STATE, s); - assertSame(Core.COUNT, ctx.lookup(Symbols.COUNT).getResult()); - - assertCVMEquals(Symbols.STAR_TIMESTAMP, ctx.lookup(Symbols.STAR_TIMESTAMP).getResult()); - - assertCVMEquals(Constants.INITIAL_TIMESTAMP, s.getTimeStamp()); - } - - @Test - public void testContractCall() { - Context ctx0 = Context.createFake(STATE, InitTest.HERO); - Address TARGET = CONTRACTS[0]; - ctx0 = ctx0.execute(compile(ctx0, "(def target (address \"" + TARGET.toHexString() + "\"))")); - ctx0 = ctx0.execute(compile(ctx0, "(def hero *address*)")); - final Context ctx = ctx0; - - assertEquals(InitTest.HERO, ctx.lookup(Symbols.HERO).getResult()); - assertEquals(Keyword.create("bar"), eval(ctx, "(call target (foo))")); - assertEquals(InitTest.HERO, eval(ctx, "(call target (who-called-me))")); - assertEquals(TARGET, eval(ctx, "(call target (my-address))")); - - assertEquals(0L, evalL(ctx, "(call target (my-number))")); - - assertStateError(TestState.step(ctx, "(call target (missing-function))")); - } - - public static boolean evalB(String source) { - return ((CVMBool)eval(source)).booleanValue(); - } - - public static boolean evalB(Context ctx, String source) { - return ((CVMBool)eval(ctx, source)).booleanValue(); - } - - public static double evalD(Context ctx, String source) { - ACell result=eval(ctx,source); - CVMDouble d=RT.castDouble(result); - if (d==null) throw new ClassCastException("Expected Double, but got: "+RT.getType(result)); - return d.doubleValue(); - } - - public static double evalD(String source) { - return evalD(CONTEXT,source); - } - - public static long evalL(Context ctx, String source) { - ACell result=eval(ctx,source); - CVMLong d=RT.castLong(result); - if (d==null) throw new ClassCastException("Expected Long, but got: "+RT.getType(result)); - return d.longValue(); - } - - public static long evalL(String source) { - return evalL(CONTEXT,source); - } - - public static String evalS(String source) { - return eval(source).toString(); - } - - @SuppressWarnings("unchecked") - public static T eval(String source) { - return (T) step(source).getResult(); - } - - public static Context step(String source) { - return step(CONTEXT, source); - } - - /** - * Steps execution in a new forked Context - * @param - * @param ctx Initial context to fork - * @param source - * @return New forked context containing step result - */ - @SuppressWarnings("unchecked") - public static Context step(Context ctx, String source) { - // Compile form in forked context - Context> cctx=ctx.fork(); - ACell form = Reader.read(source); - cctx = cctx.expandCompile(form); - if (cctx.isExceptional()) return (Context) cctx; - AOp op = cctx.getResult(); - - // Run form in separate forked context to get result context - Context rctx = ctx.fork(); - rctx=(Context) rctx.run(op); - assert(rctx.getDepth()==0):"Invalid depth after step: "+rctx.getDepth(); - return rctx; - } - - /** - * Runs an execution step as a different address. Returns value after restoring - * the original address. - * @param address Address to run as - * @param c Initial Context. Will not be modified. - * @param source Source form to execute - * @return Updated context - */ - @SuppressWarnings("unchecked") - public static Context stepAs(Address address, Context c, String source) { - Context rc = Context.createFake(c.getState(), address); - rc = step(rc, source); - return (Context) Context.createFake(rc.getState(), c.getAddress()).withValue(rc.getValue()); - } - - @Test public void testStateSetup() { - assertEquals(0,CONTEXT.getDepth()); - assertFalse(CONTEXT.isExceptional()); - assertNull(CONTEXT.getResult()); - assertEquals(TestState.TOTAL_FUNDS, STATE.computeTotalFunds()); - - } - - public static void main(String[] args) { - System.out.println(Utils.print(STATE)); - } - -} diff --git a/convex-core/src/test/java/convex/core/lang/reader/ANTLRTest.java b/convex-core/src/test/java/convex/core/lang/reader/ANTLRTest.java deleted file mode 100644 index e00e5a495..000000000 --- a/convex-core/src/test/java/convex/core/lang/reader/ANTLRTest.java +++ /dev/null @@ -1,144 +0,0 @@ -package convex.core.lang.reader; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; - -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.data.Blob; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.Lists; -import convex.core.data.Maps; -import convex.core.data.Sets; -import convex.core.data.Strings; -import convex.core.data.Symbol; -import convex.core.data.Syntax; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMBool; -import convex.core.data.prim.CVMChar; -import convex.core.data.prim.CVMDouble; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.ParseException; -import convex.core.lang.Symbols; - -public class ANTLRTest { - - @SuppressWarnings("unchecked") - private R read(String s) { - return (R) AntlrReader.read(s); - } - - @SuppressWarnings("unchecked") - private R readAll(String s) { - return (R) AntlrReader.readAll(s); - } - - @Test public void testNil() { - assertNull(read("nil")); - } - - @Test public void testPrimitives () { - assertSame(CVMBool.TRUE,read("true")); - assertSame(CVMBool.FALSE,read("false")); - assertEquals(CVMLong.create(17),read("17")); - assertEquals(CVMLong.create(-2),read("-2")); - assertEquals(CVMLong.ZERO,read("0")); - } - - @Test public void testDataStructures() { - // basic data structures - assertEquals(Vectors.of(1,2),read("[1 2]")); - assertEquals(Lists.of(1,2),read("(1 2)")); - assertEquals(Sets.of(1,2),read("#{1 2}")); - assertEquals(Maps.of(1,2),read("{1 2}")); - - // empty structures - assertSame(Sets.empty(),read("#{}")); - assertSame(Lists.empty(),read("()")); - assertSame(Vectors.empty(),read("[]")); - assertSame(Maps.empty(),read("{}")); - } - - @Test public void testSymbols() { - assertEquals(Symbols.FOO,read("foo")); - assertEquals(Symbol.create("/"),read("/")); - } - - @Test public void testKeywords() { - assertEquals(Keywords.FOO,read(":foo")); - assertEquals(Keyword.create("/"),read(":/")); - } - - - @Test public void testBlobs() { - // Blobs - assertEquals(Blob.EMPTY,read("0x")); - assertEquals(Blob.fromHex("cafebabe"),read("0xcaFEBAbe")); - } - - @Test public void testAddress() { - // Address - assertEquals(Address.create(17),read("#17")); - } - - @Test public void testSyntax() { - assertEquals(Syntax.create(CVMLong.ONE,Maps.empty()), read("^{} 1")); - } - - @Test public void testQuoting() { - assertEquals(Lists.of(Symbols.QUOTE,CVMLong.ZERO), read("'0")); - assertEquals(Lists.of(Symbols.QUOTE,Symbols.FOO), read("'foo")); - assertEquals(Lists.of(Symbols.QUOTE,Vectors.empty()), read("'[]")); - } - - - @Test public void testChars() { - assertEquals(CVMChar.create('a'), read("\\a")); - assertEquals(CVMChar.create('\t'), read("\\tab")); - } - - @Test public void testSpecial() { - assertEquals(CVMDouble.NaN, read("##NaN")); - } - - - @Test public void testDouble() { - assertEquals(CVMDouble.ONE, read("1.0")); - assertEquals(CVMDouble.ONE, read("1.0e0")); - assertEquals(CVMDouble.create(-17.0), read("-17.0")); - assertEquals(CVMDouble.create(-17.0e2), read("-17.0E2")); - assertEquals(CVMDouble.create(1000), read("1e3")); - assertEquals(CVMDouble.create(0.001), read("1e-3")); - } - - @Test public void testStrings() { - assertEquals(Strings.create(""), read("\"\"")); - assertEquals(Strings.create("a"), read("\"a\"")); - assertEquals(Strings.create("bar"), read("\"bar\"")); - assertEquals(Strings.create("ba\nr"), read("\"ba\\\nr\"")); - } - - @Test public void testReadAll() { - assertSame(Lists.empty(),readAll("")); - assertEquals(Lists.of(1,2),readAll(" 1 2 ")); - - assertThrows(ParseException.class,()->readAll("1 2 (")); - - } - - @Test public void testPath() { - assertEquals(Lists.of(Symbols.LOOKUP,Address.ZERO,Symbols.FOO),AntlrReader.read("#0/foo")); - assertEquals(Lists.of(Symbols.LOOKUP,Address.ZERO,Symbols.DIVIDE),AntlrReader.read("#0//")); - } - - @Test public void testError() { - assertThrows(ParseException.class,()->read("1 2")); - assertThrows(ParseException.class,()->read("1.0e0.1234")); - } - -} diff --git a/convex-core/src/test/java/convex/core/util/EconomicsTest.java b/convex-core/src/test/java/convex/core/util/EconomicsTest.java deleted file mode 100644 index fda2be3bb..000000000 --- a/convex-core/src/test/java/convex/core/util/EconomicsTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package convex.core.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; - -public class EconomicsTest { - - @Test - public void testPoolRate() { - - assertEquals(1.0, Economics.swapRate(100, 100)); - assertEquals(1.0, Economics.swapRate(Long.MAX_VALUE, Long.MAX_VALUE)); - assertEquals(2.0, Economics.swapRate(1, 2)); - - assertThrows(IllegalArgumentException.class, () -> Economics.swapRate(0, 0)); - assertThrows(IllegalArgumentException.class, () -> Economics.swapRate(1000, 0)); - assertThrows(IllegalArgumentException.class, () -> Economics.swapRate(0, 1000)); - assertThrows(IllegalArgumentException.class, () -> Economics.swapRate(-10, 10)); - assertThrows(IllegalArgumentException.class, () -> Economics.swapRate(10, -10)); - assertThrows(IllegalArgumentException.class, () -> Economics.swapRate(Long.MIN_VALUE, Long.MIN_VALUE)); - } - - @Test - public void testPoolPrice() { - - assertEquals(101, Economics.swapPrice(50, 100, 100)); - assertEquals(1, Economics.swapPrice(0, 100, 100)); - assertEquals(-33, Economics.swapPrice(-50, 100, 100)); - assertEquals(1, Economics.swapPrice(0, 1675, 117)); - assertEquals(1, Economics.swapPrice(0, 12, 1454517)); - assertEquals(1000000, Economics.swapPrice(999999, 1000000, 1)); - - // TODO: seem to be some instability issues doing things like this? - // assertEquals(Long.MAX_VALUE-1000000, Economics.swapPrice(Long.MAX_VALUE-1000000, Long.MAX_VALUE, 1000000)); - - // Fails because (double)(Long.MAX_VALUE-1) == (double)(Long.MAX_VALUE) - assertThrows(IllegalArgumentException.class, ()->Economics.swapPrice(Long.MAX_VALUE-1, Long.MAX_VALUE, 10)); - - assertThrows(IllegalArgumentException.class, () -> Economics.swapPrice(100, 100, 100)); - assertThrows(IllegalArgumentException.class, () -> Economics.swapPrice(100, 0, 100)); - assertThrows(IllegalArgumentException.class, () -> Economics.swapPrice(0, 0, 0)); - assertThrows(IllegalArgumentException.class, () -> Economics.swapPrice(100, 100, 0)); - assertThrows(IllegalArgumentException.class, () -> Economics.swapPrice(100, 50, 200)); - } -} diff --git a/convex-core/src/test/java/convex/core/util/GenTestEconomics.java b/convex-core/src/test/java/convex/core/util/GenTestEconomics.java deleted file mode 100644 index 7b6f64391..000000000 --- a/convex-core/src/test/java/convex/core/util/GenTestEconomics.java +++ /dev/null @@ -1,31 +0,0 @@ -package convex.core.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.runner.RunWith; - -import com.pholser.junit.quickcheck.Property; -import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; - -@RunWith(JUnitQuickcheck.class) -public class GenTestEconomics { - - @Property - public void alwaysARate(Long a, Long b) { - if ((a>0)&&(b>0)) { - double rate=Economics.swapRate(a, b); - assertTrue(rate>0); - } - } - - @Property - public void zeroCostsZero(Long a, Long b) { - if ((a>0)&&(b>0)) { - long price = Economics.swapPrice(0, a, b); - - // price should always be 1, since must spend minimum 1 unit to increase pool - assertEquals(1L,price); - } - } -} diff --git a/convex-core/src/test/java/convex/lib/AssetTest.java b/convex-core/src/test/java/convex/lib/AssetTest.java deleted file mode 100644 index 508816ea3..000000000 --- a/convex-core/src/test/java/convex/lib/AssetTest.java +++ /dev/null @@ -1,106 +0,0 @@ -package convex.lib; - -import static convex.core.lang.TestState.*; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import convex.core.crypto.AKeyPair; -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.lang.Context; -import convex.core.lang.TestState; - -/** - * - * Generic tests for ANY digital asset compatible with the convex.asset API - */ -public class AssetTest { - - static final AKeyPair TEST_KP=AKeyPair.generate(); - - static { - - } - /** - * Generic tests for an Asset - * - * Both users must have a non-empty balance. - * - * @param ctx Context to execute in - * @param asset Address of asset - * @param user1 First user - * @param user2 Second user - */ - public static void doAssetTests (Context ctx, Address asset, Address user1, Address user2) { - // Set up test user - ctx=ctx.createAccount(TEST_KP.getAccountKey()); - Address tester=(Address) ctx.getResult(); - ctx=ctx.forkWithAddress(tester); - ctx=step(ctx,"(import convex.asset :as asset)"); - ctx=step(ctx,"(def token "+asset+")"); - ctx=step(ctx,"(def user1 "+user1+")"); - ctx=step(ctx,"(def user2 "+user2+")"); - ctx = TestState.step(ctx,"(def actor (deploy '(set-controller "+tester+")))"); - Address actor = (Address) ctx.getResult(); - assertNotNull(actor); - - // Set up user imports - ctx=stepAs(user1,ctx,"(import convex.asset :as asset)"); - ctx=stepAs(user2,ctx,"(import convex.asset :as asset)"); - - - // Tester balance should be the empty value - ACell empty=eval(ctx,"(asset/balance token)"); - assertEquals(empty,eval(ctx,"(asset/quantity-zero token)")); - - // Get user balances and total balance, ensure they are not empty - ctx=step(ctx,"(def bal1 (asset/balance token user1))"); - ACell balance1=ctx.getResult(); - assertNotNull(balance1); - assertNotEquals(empty,balance1); - ctx=step(ctx,"(def bal2 (asset/balance token user2))"); - ACell balance2=ctx.getResult(); - assertNotNull(balance2); - assertNotEquals(empty,balance2); - ACell total=eval(ctx,"(asset/quantity-add token bal1 bal2)"); - assertNotNull(total); - assertNotEquals(empty,total); - - // Tests for each user - doUserAssetTests(ctx,asset,user1,balance1); - doUserAssetTests(ctx,asset,user2,balance2); - - // Test transferring everything to tester - { - Context c=ctx.fork(); - c=stepAs(user1,c,"(asset/transfer "+tester+" ["+asset+" (asset/balance "+asset+")])"); - c=stepAs(user2,c,"(asset/transfer "+tester+" ["+asset+" (asset/balance "+asset+")])"); - - // user balances should now be empty - assertEquals(empty,eval(c,"(asset/balance token user1)")); - assertEquals(empty,eval(c,"(asset/balance token user2)")); - - // tester should own everything - assertEquals(total,eval(c,"(asset/balance token)")); - } - - } - - public static void doUserAssetTests (Context ctx, Address asset, Address user, ACell balance) { - ctx=ctx.forkWithAddress(user); - ctx=step(ctx,"(def ast (address "+asset+"))"); - assertEquals(asset,ctx.getResult()); - - ctx=step(ctx,"(def bal (asset/balance "+asset+"))"); - assertEquals(balance,eval(ctx,"bal")); - assertEquals(balance,eval(ctx,"(asset/quantity-add ast bal nil)")); - assertEquals(balance,eval(ctx,"(asset/quantity-add ast nil bal)")); - assertEquals(eval(ctx,"(asset/quantity-zero ast)"),eval(ctx,"(asset/quantity-sub ast bal bal)")); - - assertTrue(evalB(ctx,"(asset/owns? *address* [ast bal])")); - assertTrue(evalB(ctx,"(asset/owns? *address* [ast nil])")); - } - -} diff --git a/convex-core/src/test/java/convex/lib/FungibleTest.java b/convex-core/src/test/java/convex/lib/FungibleTest.java deleted file mode 100644 index 819362ac2..000000000 --- a/convex-core/src/test/java/convex/lib/FungibleTest.java +++ /dev/null @@ -1,244 +0,0 @@ -package convex.lib; - -import static convex.core.lang.TestState.eval; -import static convex.core.lang.TestState.evalB; -import static convex.core.lang.TestState.evalL; -import static convex.core.lang.TestState.step; -import static convex.test.Assertions.assertAssertError; -import static convex.test.Assertions.assertError; -import static convex.test.Assertions.assertNotError; -import static convex.test.Assertions.assertTrustError; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -import convex.core.crypto.AKeyPair; -import convex.core.data.AMap; -import convex.core.data.Address; -import convex.core.init.InitTest; -import convex.core.lang.Context; -import convex.core.lang.RT; -import convex.core.lang.TestState; - -public class FungibleTest { - - static final AKeyPair TEST_KEYPAIR=AKeyPair.generate(); - - private static final Address VILLAIN=InitTest.VILLAIN; - - private static final Context CTX; - private static final Address fungible; - - static { - Context ctx=TestState.CONTEXT.fork(); - String importS="(import convex.fungible :as fungible)"; - ctx=step(ctx,importS); - assertNotError(ctx); - fungible = (Address)ctx.getResult(); - ctx=step(ctx,"(import convex.asset :as asset)"); - assertFalse(ctx.isExceptional()); - CTX = ctx; - } - - @Test public void testAssetAPI() { - Context ctx = CTX.fork(); - ctx=step(ctx,"(def token (deploy (fungible/build-token {:supply 1000000})))"); - Address token = (Address) ctx.getResult(); - assertNotNull(token); - - // generic tests - doFungibleTests(ctx,token,ctx.getAddress()); - - assertEquals(1000000L,evalL(ctx,"(asset/balance token *address*)")); - assertEquals(0L,evalL(ctx,"(asset/balance token *registry*)")); - - ctx=step(ctx,"(asset/offer "+VILLAIN+" [token 1000])"); - assertNotError(ctx); - - ctx=step(ctx,"(asset/transfer "+VILLAIN+" [token 2000])"); - assertNotError(ctx); - - assertEquals(998000L,evalL(ctx,"(asset/balance token *address*)")); - assertEquals(2000L,evalL(ctx,"(asset/balance token "+VILLAIN+")")); - - assertEquals(0L,evalL(ctx,"(asset/quantity-zero token)")); - assertEquals(110L,evalL(ctx,"(asset/quantity-add token 100 10)")); - assertEquals(110L,evalL(ctx,"(asset/quantity-sub token 120 10)")); - assertEquals(110L,evalL(ctx,"(asset/quantity-sub token 110 nil)")); - assertEquals(0L,evalL(ctx,"(asset/quantity-sub token 100 1000)")); - - assertTrue(evalB(ctx,"(asset/quantity-contains? [token 110] [token 100])")); - assertTrue(evalB(ctx,"(asset/quantity-contains? [token 110] nil)")); - assertTrue(evalB(ctx,"(asset/quantity-contains? token 1000 999)")); - assertFalse(evalB(ctx,"(asset/quantity-contains? [token 110] [token 300])")); - - - - assertTrue(evalB(ctx,"(asset/owns? "+VILLAIN+" [token 1000])")); - assertTrue(evalB(ctx,"(asset/owns? "+VILLAIN+" [token 2000])")); - assertFalse(evalB(ctx,"(asset/owns? "+VILLAIN+" [token 2001])")); - - // transfer using map argument - ctx=step(ctx,"(asset/transfer "+VILLAIN+" {token 100})"); - assertTrue(ctx.getResult() instanceof AMap); - assertTrue(evalB(ctx,"(asset/owns? "+VILLAIN+" [token 2100])")); - - // test offer - ctx=step(ctx,"(asset/offer "+VILLAIN+" [token 1337])"); - assertEquals(1337L,evalL(ctx,"(asset/get-offer token *address* "+VILLAIN+")")); - } - - @Test public void testBuildToken() { - // check our alias is right - Context ctx = CTX.fork(); - assertEquals(fungible,eval(ctx,"fungible")); - - // deploy a token with default config - ctx=step(ctx,"(def token (deploy (fungible/build-token {})))"); - Address token = (Address) ctx.getResult(); - assertTrue(ctx.getAccountStatus(token)!=null); - ctx=step(ctx,"(def token (address "+token+"))"); - - // GEnric tests - doFungibleTests(ctx,token,ctx.getAddress()); - - // check our balance is positive as initial holder - long bal=evalL(ctx,"(fungible/balance token *address*)"); - assertTrue(bal>0); - - // transfer to the Villain scenario - { - Context tctx=step(ctx,"(fungible/transfer token "+VILLAIN+" 100)"); - assertEquals(bal-100,evalL(tctx,"(fungible/balance token *address*)")); - assertEquals(100,evalL(tctx,"(fungible/balance token "+VILLAIN+")")); - } - - // acceptable transfers - assertNotError(step(ctx,"(fungible/transfer token *address* 0)")); - assertNotError(step(ctx,"(fungible/transfer token *address* "+bal+")")); - - // bad transfers - assertAssertError(step(ctx,"(fungible/transfer token *address* -1)")); - assertAssertError(step(ctx,"(fungible/transfer token *address* "+(bal+1)+")")); - } - - @Test public void testMint() { - // check our alias is right - Context ctx = CTX.fork(); - - // deploy a token with default config - ctx=step(ctx,"(def token (deploy [(fungible/build-token {:supply 100}) (fungible/add-mint {:max-supply 1000})]))"); - Address token = (Address) ctx.getResult(); - assertTrue(ctx.getAccountStatus(token)!=null); - - // do Generic Tests - doFungibleTests(ctx,token,ctx.getAddress()); - - // check our balance is positive as initial holder - Long bal=evalL(ctx,"(fungible/balance token *address*)"); - assertEquals(100L,bal); - - // Mint up to max and back down to zero - { - Context c=step(ctx,"(fungible/mint token 900)"); - assertEquals(1000L,evalL(c,"(fungible/balance token *address*)")); - - c=step(c,"(fungible/mint token -900)"); - assertEquals(bal,evalL(c,"(fungible/balance token *address*)")); - - c=step(c,"(fungible/mint token -100)"); - assertEquals(0L,evalL(c,"(fungible/balance token *address*)")); - } - - // Mint up to max and burn down to zero - { - Context c=step(ctx,"(fungible/mint token 900)"); - assertEquals(1000L,evalL(c,"(fungible/balance token *address*)")); - - c=step(c,"(fungible/burn token 900)"); - assertEquals(100L,evalL(c,"(fungible/balance token *address*)")); - - assertAssertError(step(c,"(fungible/burn token 101)")); // Fails, not held - - c=step(c,"(fungible/burn token 100)"); - assertEquals(0L,evalL(c,"(fungible/balance token *address*)")); - - assertAssertError(step(c,"(fungible/burn token 1)")); // Fails, not held - } - - - // Shouldn't be possible to burn tokens in supply but not held - { - Context c=step(ctx,"(fungible/mint token 900)"); - assertEquals(1000L,evalL(c,"(fungible/balance token *address*)")); - - c=step(c,"(fungible/transfer token "+VILLAIN+" 800)"); - assertEquals(200L,evalL(c,"(fungible/balance token *address*)")); - - assertAssertError(step(c,"(fungible/burn token 201)")); // Fails, not held - assertNotError(step(c,"(fungible/burn token 200)")); // OK since held - } - - // Illegal Minting amounts - { - assertError(step(ctx,"(fungible/mint token 901)")); // too much (exceeds max supply) - assertError(step(ctx,"(fungible/mint token -101)")); // too little - } - - // Villain shouldn't be able to mint or burn - { - Context c=ctx.forkWithAddress(VILLAIN); - c=step(c,"(def token "+token+")"); - c=step(c,"(import convex.fungible :as fungible)"); - - assertTrustError(step(c,"(fungible/mint token 100)")); - assertTrustError(step(c,"(fungible/mint token 10000)")); // trust before amount checks - - assertTrustError(step(c,"(fungible/burn token 100)")); - } - } - - /** - * Generic tests for a fungible token. User account should have some of fungible token and sufficient coins. - * @param ctx Initial Context. Will be forked. - * @param token Fungible token Address - * @param user User Address - */ - public static void doFungibleTests (Context ctx, Address token, Address user) { - ctx=ctx.forkWithAddress(user); - ctx=step(ctx,"(import convex.asset :as asset)"); - ctx=step(ctx,"(import convex.fungible :as fungible)"); - ctx=step(ctx,"(def token "+token+")"); - ctx = TestState.step(ctx,"(def actor (deploy '(set-controller "+user+")))"); - Address actor = (Address) ctx.getResult(); - assertNotNull(actor); - - Long BAL=evalL(ctx,"(asset/balance token *address*)"); - assertEquals(0L, evalL(ctx,"(asset/balance token actor)")); - assertTrue(BAL>0,"Should provide a user account with positive balance!"); - - // transfer all to self, should not affect balance - ctx=step(ctx,"(asset/transfer *address* [token "+BAL+"])"); - assertEquals(BAL,RT.jvm(ctx.getResult())); - assertEquals(BAL, evalL(ctx,"(asset/balance token *address*)")); - - // transfer nothing to self, should not affect balance - ctx=step(ctx,"(asset/transfer *address* [token nil])"); - assertEquals(0L,(long)RT.jvm(ctx.getResult())); - assertEquals(BAL, evalL(ctx,"(asset/balance token *address*)")); - - // Run generic asset tests, giving 1/3 the balance to a new user account - { - Context c=ctx.fork(); - c=c.createAccount(TEST_KEYPAIR.getAccountKey()); - Address user2=(Address) c.getResult(); - Long smallBal=BAL/3; - c=step(c,"(asset/transfer "+user2+" [token "+smallBal+"])"); - - AssetTest.doAssetTests(c, token, user, user2); - } - } -} diff --git a/convex-core/src/test/java/convex/lib/SimpleNFTTest.java b/convex-core/src/test/java/convex/lib/SimpleNFTTest.java deleted file mode 100644 index 8051767e4..000000000 --- a/convex-core/src/test/java/convex/lib/SimpleNFTTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package convex.lib; - -import static convex.core.lang.TestState.eval; -import static convex.core.lang.TestState.step; -import static convex.test.Assertions.assertNotError; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import org.junit.jupiter.api.Test; - -import convex.core.crypto.AKeyPair; -import convex.core.data.AVector; -import convex.core.data.Address; -import convex.core.data.Sets; -import convex.core.data.prim.CVMLong; -import convex.core.lang.Context; -import convex.core.lang.TestState; -import convex.test.Testing; - -public class SimpleNFTTest { - - static final AKeyPair KP1=AKeyPair.generate(); - static final AKeyPair KP2=AKeyPair.generate(); - - static final Address NFT; - - private static final Context CTX; - - static { - Context ctx=TestState.CONTEXT.fork(); - String importS = "(import asset.nft.simple :as nft)"; - ctx=step(ctx,importS); - NFT=(Address)ctx.getResult(); - assertNotNull(NFT); - ctx=step(ctx,"(import convex.asset :as asset)"); - CTX=ctx; - } - - @Test public void testScript1() { - Context c=Testing.runTests(CTX,"contracts/nft/simple-nft-test.con"); - assertNotError(c); - } - - - @SuppressWarnings("unchecked") - @Test public void testAssetAPI() { - Context ctx=CTX.fork(); - ctx=step(ctx,"(def total (map (fn [v] (call nft (create))) [1 2 3 4]))"); - AVector v=(AVector) ctx.getResult(); - assertEquals(4,v.count()); - CVMLong b1=v.get(0); - - // Test balance - assertEquals(Sets.of(v.toCellArray()),eval(ctx,"(asset/balance nft)")); - - // Create test Users - ctx=ctx.createAccount(KP1.getAccountKey()); - Address user1=(Address) ctx.getResult(); - ctx=ctx.createAccount(KP2.getAccountKey()); - Address user2=(Address) ctx.getResult(); - - ctx=step(ctx,"(asset/transfer "+user1+" [nft (set (next total))])"); - ctx=step(ctx,"(asset/transfer "+user2+" [nft #{"+b1+"}])"); - assertEquals(Sets.of(b1),ctx.getResult()); - - AssetTest.doAssetTests(ctx, NFT, user1, user2); - - } -} diff --git a/convex-core/src/test/java/convex/lib/TrustTest.java b/convex-core/src/test/java/convex/lib/TrustTest.java deleted file mode 100644 index 3861e3afb..000000000 --- a/convex-core/src/test/java/convex/lib/TrustTest.java +++ /dev/null @@ -1,237 +0,0 @@ -package convex.lib; - -import static convex.test.Assertions.assertCastError; -import static convex.test.Assertions.assertNotError; -import static convex.test.Assertions.assertStateError; -import static convex.test.Assertions.assertTrustError; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; - -import org.junit.jupiter.api.Test; - -import convex.core.data.Address; -import convex.core.data.Keywords; -import convex.core.init.InitTest; -import convex.core.lang.ACVMTest; -import convex.core.lang.Context; -import convex.test.Samples; - -public class TrustTest extends ACVMTest { - private Address trusted=null; - - private Context CONTEXT; - protected TrustTest() throws IOException { - super(InitTest.BASE); - Context ctx = context(); - - String importS = "(import convex.trust :as trust)"; - ctx = step(ctx, importS); - assertNotError(ctx); - trusted = (Address)ctx.getResult(); - - CONTEXT=ctx.fork(); - INITIAL=ctx.getState(); - } - - /** - * Test that re-deployment of Fungible matches what is expected - */ - @Test - public void testLibraryProperties() { - assertTrue(CONTEXT.getAccountStatus(trusted).isActor()); - - // check alias is set up correctly - assertEquals(trusted, eval(CONTEXT, "trust")); - } - - @Test - public void testSelfTrust() { - Context ctx = CONTEXT.fork(); - - assertTrue(evalB(ctx, "(trust/trusted? *address* *address*)")); - assertFalse(evalB(ctx, "(trust/trusted? *address* nil)")); - assertFalse(evalB(ctx, "(trust/trusted? *address* :foo)")); - assertFalse(evalB(ctx, - "(trust/trusted? *address* (address 666666))")); - } - - @Test - public void testUpgradeWhitelist() { - Context ctx = CONTEXT.fork(); - - // deploy a whitelist with default config and upgradable capability - ctx = step(ctx, "(def wlist (deploy [(trust/build-whitelist nil) (trust/add-trusted-upgrade nil)]))"); - Address wl = (Address) ctx.getResult(); - assertNotNull(wl); - - assertTrue(evalB(ctx, "(trust/trusted? wlist *address*)")); - - // do an upgrade that blanks the whitelist - ctx = step(ctx, "(call wlist (upgrade '(do (def whitelist #{}))))"); - - { - // check our villain cannot upgrade the actor! - Address a1 = VILLAIN; - - Context c = ctx.forkWithAddress(a1); - c = step(c, "(do (import " + trusted + " :as trust) (def wlist " + wl + "))"); - - assertTrustError(step(c, "(call wlist (upgrade '(do :foo)))")); - } - - // check that our edit has updated actor - assertFalse(evalB(ctx, "(trust/trusted? wlist *address*)")); - - // check we can permanently remove upgradability - ctx = step(ctx, "(trust/remove-upgradability! wlist)"); - assertNotError(ctx); - assertStateError(step(ctx, "(call wlist (upgrade '(do :foo)))")); - - // actor functionality should still work otherwise - assertFalse(evalB(ctx, "(trust/trusted? wlist *address*)")); - } - - @Test - public void testWhitelist() { - // check our alias is right - Context ctx = CONTEXT.fork(); - - // deploy a whitelist with default config - ctx = step(ctx, "(def wlist (deploy (trust/build-whitelist nil)))"); - Address wl = (Address) ctx.getResult(); - assertNotNull(wl); - - // initial creator should be on whitelist - assertTrue(evalB(ctx, "(trust/trusted? wlist *address*)")); - - assertCastError(step(ctx, "(trust/trusted? wlist nil)")); - assertCastError(step(ctx, "(trust/trusted? wlist [])")); - - assertCastError(step(ctx, "(trust/trusted? nil *address*)")); - assertCastError(step(ctx, "(trust/trusted? [] *address*)")); - - { // check adding and removing to whitelist - Address a1 = Samples.BAD_ADDRESS; - Context c = ctx; - - // Check not initially on whitelist - assertFalse(evalB(c, "(trust/trusted? wlist " + a1 + ")")); - - // Add address to whitelist, shouldn't matter if it exists or not - c = step(c, "(call wlist (set-trusted " + a1 + " true))"); - assertNotError(c); - assertTrue(evalB(c, "(trust/trusted? wlist " + a1 + ")")); - - // Check removal from whitelist - c = step(c, "(call wlist (set-trusted " + a1 + " false))"); - assertNotError(c); - assertFalse(evalB(c, "(trust/trusted? wlist " + a1 + ")")); - } - - { // check the villain is excluded - Address a1 = VILLAIN; - Address a2 = HERO; - - Context c = ctx.forkWithAddress(a1); - c = step(c, "(do (import " + trusted + " :as trust) (def wlist (address " + wl + ")))"); - assertNotError(c); - - // villain can still check monitor - assertFalse(evalB(c, "(trust/trusted? wlist " + a1 + ")")); - assertTrue(evalB(c, "(trust/trusted? wlist " + a2 + ")")); - - // villain can't change whitelist - assertTrustError(step(c, "(call wlist (set-trusted " + a1 + " true))")); - assertTrustError(step(c, "(call wlist (set-trusted " + a2 + " false))")); - } - } - - @Test - public void testBlacklist() { - Context ctx = CONTEXT.fork(); - - // deploy a blacklist with default config - ctx = step(ctx, "(def blist (deploy (trust/build-blacklist {:blacklist [" + VILLAIN + "]})))"); - Address wl = (Address) ctx.getResult(); - assertNotNull(wl); - - // initial creator should not be on blacklist - assertTrue(evalB(ctx, "(trust/trusted? blist *address*)")); - - // our villain should be on the blacklist - assertFalse(evalB(ctx, "(trust/trusted? blist " + VILLAIN + ")")); - - assertCastError(step(ctx, "(trust/trusted? blist nil)")); - assertCastError(step(ctx, "(trust/trusted? blist [])")); - - assertCastError(step(ctx, "(trust/trusted? nil *address*)")); - assertCastError(step(ctx, "(trust/trusted? [] *address*)")); - - { // check adding and removing to blacklist - Address a1 = Samples.BAD_ADDRESS; - Context c = ctx; - - // Check not initially on blacklist - assertTrue(evalB(c, "(trust/trusted? blist " + a1 + ")")); - - // Add address to blacklist, shouldn't matter if it exists or not - c = step(c, "(call blist (set-trusted " + a1 + " false))"); - assertNotError(c); - assertFalse(evalB(c, "(trust/trusted? blist " + a1 + ")")); - - // Check removal from blacklist - c = step(c, "(call blist (set-trusted " + a1 + " true))"); - assertNotError(c); - assertTrue(evalB(c, "(trust/trusted? blist " + a1 + ")")); - } - - { // check the villain is excluded - Address a1 = VILLAIN; - Address a2 = HERO; - - Context c = ctx.forkWithAddress(a1); - c = step(c, "(do (import " + trusted + " :as trust) (def blist (address " + wl + ")))"); - assertNotError(c); - - // villain can still check monitor - assertFalse(evalB(c, "(trust/trusted? blist " + a1 + ")")); - assertTrue(evalB(c, "(trust/trusted? blist " + a2 + ")")); - - // villain can't change whitelist - assertTrustError(step(c, "(call blist (set-trusted " + a1 + " true))")); - assertTrustError(step(c, "(call blist (set-trusted " + a2 + " false))")); - } - } - - @Test - public void testWhitelistController() { - Context ctx = CONTEXT.fork(); - - // deploy an initially empty whitelist - ctx = step(ctx, "(def wlist (deploy (trust/build-whitelist {:whitelist []})))"); - - // deploy two actors - ctx = step(ctx, "(def alice (deploy `(set-controller ~*address*)))"); - ctx = step(ctx, "(def bob (deploy `(set-controller ~wlist)))"); - - // check initial trust - assertEquals(Keywords.FOO, eval(ctx, "(eval-as alice :foo)")); - assertTrustError(step(ctx, "(eval-as bob :foo)")); - - // add alice to the whitelist - ctx = step(ctx, "(call wlist (set-trusted alice true))"); - - // eval-as should work from alice to bob - assertEquals(eval(ctx, "bob"), (Object) eval(ctx, "(eval-as alice `(eval-as ~bob '*address*))")); - - // remove alice from the whitelist - ctx = step(ctx, "(call wlist (set-trusted alice false))"); - - // eval-as should now fail - assertTrustError(step(ctx, "(eval-as alice `(eval-as ~bob :foo))")); - } -} diff --git a/convex-core/src/test/java/convex/store/EtchInitTest.java b/convex-core/src/test/java/convex/store/EtchInitTest.java deleted file mode 100644 index a246fa74e..000000000 --- a/convex-core/src/test/java/convex/store/EtchInitTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package convex.store; - -import org.junit.jupiter.api.Test; - -import convex.core.State; -import convex.core.data.ACell; -import convex.core.data.Hash; -import convex.core.data.Ref; -import convex.core.exceptions.InvalidDataException; -import convex.core.init.InitTest; -import convex.core.store.AStore; -import convex.core.store.Stores; -import etch.EtchStore; - -public class EtchInitTest { - - @Test public void testInitState() throws InvalidDataException { - AStore temp=Stores.current(); - try { - Stores.setCurrent(EtchStore.createTemp()); - - // Use fresh State - State s=InitTest.createState(); - Ref sr=ACell.createPersisted(s); - - Hash hash=sr.getHash(); - - Ref sr2=Ref.forHash(hash); - State s2=sr2.getValue(); - s2.validate(); - } finally { - Stores.setCurrent(temp); - } - } -} diff --git a/convex-core/src/test/java/convex/store/EtchStoreTest.java b/convex-core/src/test/java/convex/store/EtchStoreTest.java deleted file mode 100644 index efcab849e..000000000 --- a/convex-core/src/test/java/convex/store/EtchStoreTest.java +++ /dev/null @@ -1,240 +0,0 @@ -package convex.store; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertSame; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Random; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; - -import org.junit.Test; - -import convex.core.Belief; -import convex.core.Block; -import convex.core.Order; -import convex.core.crypto.AKeyPair; -import convex.core.data.ACell; -import convex.core.data.AMap; -import convex.core.data.AVector; -import convex.core.data.Address; -import convex.core.data.Blob; -import convex.core.data.Format; -import convex.core.data.Hash; -import convex.core.data.Keywords; -import convex.core.data.Lists; -import convex.core.data.Maps; -import convex.core.data.Ref; -import convex.core.data.Vectors; -import convex.core.exceptions.BadFormatException; -import convex.core.init.InitTest; -import convex.core.lang.Symbols; -import convex.core.store.AStore; -import convex.core.store.Stores; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Invoke; -import convex.core.transactions.Transfer; -import convex.core.util.Utils; -import convex.test.Samples; -import etch.EtchStore; - -public class EtchStoreTest { - - private static final Hash BAD_HASH = Samples.BAD_HASH; - private EtchStore store = EtchStore.createTemp(); - - @Test - public void testEmptyStore() { - AStore oldStore = Stores.current(); - try { - Stores.setCurrent(store); - assertTrue(oldStore != store); - assertEquals(store, Stores.current()); - - assertNull(store.refForHash(BAD_HASH)); - - AMap data = Maps.of(Keywords.FOO,Symbols.FOO); - Ref> goodRef = data.getRef(); - Hash goodHash = goodRef.getHash(); - assertNull(store.refForHash(goodHash)); - - goodRef.persist(); - - if (!data.isEmbedded()) { - Ref> recRef = store.refForHash(goodHash); - assertNotNull(recRef); - - assertEquals(data, recRef.getValue()); - } - } finally { - Stores.setCurrent(oldStore); - } - } - - @Test - public void testPersistedStatus() throws BadFormatException { - AStore oldStore = Stores.current(); - try { - Stores.setCurrent(store); - - // generate Hash of unique secure random bytes to test - should not already be - // in store - Blob randomBlob = Blob.createRandom(new Random(), Format.MAX_EMBEDDED_LENGTH+1); - Hash hash = randomBlob.getHash(); - assertNotEquals(hash, randomBlob); - - Ref initialRef = randomBlob.getRef(); - assertEquals(Ref.UNKNOWN, initialRef.getStatus()); - assertNull(Stores.current().refForHash(hash)); - - // shallow persistence first - Ref refShallow=initialRef.persistShallow(); - assertEquals(Ref.STORED, refShallow.getStatus()); - - Ref ref = initialRef.persist(); - assertEquals(Ref.PERSISTED, ref.getStatus()); - assertTrue(ref.isPersisted()); - - Ref newRef = Stores.current().refForHash(hash); - assertEquals(initialRef, newRef); - assertEquals(randomBlob, newRef.getValue()); - } finally { - Stores.setCurrent(oldStore); - } - } - - @Test - public void testBeliefAnnounce() { - AStore oldStore = Stores.current(); - AtomicLong counter=new AtomicLong(0L); - - AKeyPair kp=InitTest.HERO_KEYPAIR; - try { - Stores.setCurrent(store); - - ATransaction t1=Invoke.create(InitTest.HERO,0, Lists.of(Symbols.PLUS, Symbols.STAR_BALANCE, 1000L)); - ATransaction t2=Transfer.create(InitTest.HERO,1, InitTest.VILLAIN,1000000); - Block b=Block.of(Utils.getCurrentTimestamp(),InitTest.FIRST_PEER_KEY,kp.signData(t1),kp.signData(t2)); - assertNotNull(b.getPeer()); - - Order ord=Order.create().propose(b); - - Belief belief=Belief.create(kp,ord); - - Ref rb=belief.getRef(); - Ref rt=t1.getRef(); - assertEquals(Ref.UNKNOWN,rb.getStatus()); - assertEquals(Ref.UNKNOWN,rt.getStatus()); - - assertEquals(3,Utils.refCount(t1)); - assertEquals(0,Utils.refCount(t2)); - assertEquals(11,Utils.totalRefCount(belief)); - - - Consumer> noveltyHandler=r-> { - counter.incrementAndGet(); - }; - - // First try shallow persistence - counter.set(0L); - Ref srb=rb.persistShallow(noveltyHandler); - assertEquals(Ref.STORED,srb.getStatus()); - assertEquals(1L,counter.get()); // One cell persisted - - // assertEquals(srb,store.refForHash(rb.getHash())); - assertNull(store.refForHash(t1.getRef().getHash())); - - // Persist belief - counter.set(0L); - Ref prb=srb.persist(noveltyHandler); - assertEquals(3L,counter.get()); - - // Persist again. Should be no new novelty - counter.set(0L); - Ref prb2=srb.persist(noveltyHandler); - assertEquals(prb2,prb); - assertEquals(0L,counter.get()); // Nothing new persisted - - // Announce belief - counter.set(0L); - Ref arb=belief.announce(noveltyHandler).getRef(); - assertEquals(srb,arb); - assertEquals(4L,counter.get()); - - // Announce again. Should be no new novelty - counter.set(0L); - Ref arb2=belief.announce(noveltyHandler).getRef(); - assertEquals(srb,arb2); - assertEquals(0L,counter.get()); // Nothing new announced - - // Check re-stored ref has correct status - counter.set(0L); - Ref arb3=srb.persistShallow(noveltyHandler); - assertEquals(0L,counter.get()); // Nothing new persisted - assertTrue(Ref.STORED<=arb3.getStatus()); - - // Recover Belief from store. Should be top level stored - Belief recb=(Belief) store.refForHash(belief.getHash()).getValue(); - assertEquals(belief,recb); - } finally { - Stores.setCurrent(oldStore); - } - } - - @Test - public void testNoveltyHandler() { - AStore oldStore = Stores.current(); - ArrayList> al = new ArrayList<>(); - try { - Stores.setCurrent(store); - // create a random item that shouldn't already be in the store - AVector data = Vectors.of(Blob.createRandom(new Random(), 100),Blob.createRandom(new Random(), 100)); - - // handler that records added refs - Consumer> handler = r -> al.add(r); - - Ref> dataRef = data.getRef(); - Hash dataHash = dataRef.getHash(); - assertNull(store.refForHash(dataHash)); - - ACell.createPersisted(data,handler); - int num=al.size(); // number of novel cells persisted - assertTrue(num>0); // got new novelty - assertEquals(data, al.get(num-1).getValue()); - - data.getRef().persist(); - assertEquals(num, al.size()); // no new novelty transmitted - } finally { - Stores.setCurrent(oldStore); - } - } - - @Test public void testDecodeCache() throws BadFormatException { - Address a1=Address.create(12345678); - ACell cell=store.decode(a1.getEncoding()); - assertNotSame(cell,a1); - assertEquals(cell,a1); - - // decoding again should get same value with very high probability - ACell cell2=store.decode(a1.getEncoding()); - assertSame(cell,cell2); - } - - @Test - public void testReopen() throws IOException { - File file=File.createTempFile("etch",null); - EtchStore es=EtchStore.create(file); - es.setRootHash(Hash.NULL_HASH); - es.close(); - - EtchStore es2=EtchStore.create(file); - assertEquals(Hash.NULL_HASH,es2.getRootHash()); - } -} diff --git a/convex-core/src/test/java/convex/store/MemoryStoreTest.java b/convex-core/src/test/java/convex/store/MemoryStoreTest.java deleted file mode 100644 index bd35117a2..000000000 --- a/convex-core/src/test/java/convex/store/MemoryStoreTest.java +++ /dev/null @@ -1,114 +0,0 @@ -package convex.store; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import java.util.ArrayList; -import java.util.Random; -import java.util.function.Consumer; - -import org.junit.Test; - -import convex.core.data.ACell; -import convex.core.data.AMap; -import convex.core.data.AVector; -import convex.core.data.Address; -import convex.core.data.Blob; -import convex.core.data.Format; -import convex.core.data.Hash; -import convex.core.data.Keywords; -import convex.core.data.Maps; -import convex.core.data.Ref; -import convex.core.data.Sets; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.store.AStore; -import convex.core.store.MemoryStore; -import convex.core.store.Stores; -import convex.test.Samples; - -public class MemoryStoreTest { - - private static final Hash BAD_HASH = Samples.BAD_HASH; - - @Test - public void testEmptyStore() { - AStore oldStore = Stores.current(); - MemoryStore ms = new MemoryStore(); - try { - Stores.setCurrent(ms); - assertTrue(oldStore != ms); - assertEquals(ms, Stores.current()); - - assertNull(ms.refForHash(BAD_HASH)); - - AMap data = Maps.of(Keywords.CODE,Address.ZERO); - Ref> goodRef = data.getRef(); - Hash goodHash = goodRef.getHash(); - assertNull(ms.refForHash(goodHash)); - - goodRef.persist(); - - if (!(data.isEmbedded())) { - Ref> recRef = ms.refForHash(goodHash); - assertNotNull(recRef); - assertEquals(data, recRef.getValue()); - } - - } finally { - Stores.setCurrent(oldStore); - } - } - - @Test - public void testPersistedStatus() throws BadFormatException { - // generate Hash of unique secure random bytes to test - should not already be - // in store - Blob value = Blob.createRandom(new Random(), Format.MAX_EMBEDDED_LENGTH); - Hash hash = value.getHash(); - assertNotEquals(hash, value); - - Ref initialRef = value.getRef(); - assertEquals(Ref.UNKNOWN, initialRef.getStatus()); - assertNull(Stores.current().refForHash(hash)); - Ref ref = initialRef.persist(); - assertEquals(Ref.PERSISTED, ref.getStatus()); - assertTrue(ref.isPersisted()); - - if (!(value.isEmbedded())) { - Ref newRef = Stores.current().refForHash(hash); - assertEquals(initialRef, newRef); - assertEquals(value, newRef.getValue()); - } - } - - @Test - public void testNoveltyHandler() { - AStore oldStore = Stores.current(); - MemoryStore ms = new MemoryStore(); - ArrayList> al = new ArrayList<>(); - try { - Stores.setCurrent(ms); - ACell data = Sets.of(Samples.INT_SET_10,15685995L,Samples.INT_VECTOR_300,Samples.MAX_EMBEDDED_BLOB); // should be novel - - Consumer> handler = r -> al.add(r); - - Ref> dataRef = data.getRef(); - Hash dataHash = dataRef.getHash(); - assertNull(ms.refForHash(dataHash)); - - dataRef.persist(handler); - int num=al.size(); - assertTrue(num>0); - assertEquals(data, al.get(num-1).getValue()); - - data.getRef().persist(); - assertEquals(num, al.size()); // no new novelty transmitted - } finally { - Stores.setCurrent(oldStore); - } - } -} diff --git a/convex-core/src/test/java/convex/store/ParamTestStores.java b/convex-core/src/test/java/convex/store/ParamTestStores.java deleted file mode 100644 index 9f89043f8..000000000 --- a/convex-core/src/test/java/convex/store/ParamTestStores.java +++ /dev/null @@ -1,5 +0,0 @@ -package convex.store; - -public class ParamTestStores { - -} diff --git a/convex-core/src/test/java/convex/test/Assertions.java b/convex-core/src/test/java/convex/test/Assertions.java deleted file mode 100644 index 9fe9066a6..000000000 --- a/convex-core/src/test/java/convex/test/Assertions.java +++ /dev/null @@ -1,115 +0,0 @@ -package convex.test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.fail; - -import convex.core.ErrorCodes; -import convex.core.lang.Context; -import convex.core.lang.RT; -import convex.core.util.Utils; - -public class Assertions { - - public static void assertNotError(Context ctx) { - if(ctx.isExceptional()) { - fail("Expected no error but got: " + ctx.getValue()); - } - } - - public static void assertTotalRefCount(long expected, Object o) { - long count = Utils.totalRefCount(o); - assertEquals(expected, count, - () -> "Wrong number of Refs, expected " + expected + " but got " + o + " in object " + o); - } - - public static void assertCVMEquals(Object expected, Object result) { - assertEquals((Object)RT.cvm(expected),RT.cvm(result)); - } - - public static void assertError(Object et, Context ctx) { - Object cet = ctx.getErrorCode(); - assertEquals(et, cet, "Expected error type " + et + " but got result: " + ctx.getValue()); - } - - public static void assertError(Context ctx) { - Object cet = ctx.getErrorCode(); - assertNotNull(cet, "Expected an error but got result: " + ctx.getValue()); - } - - public static void assertArityError(Context ctx) { - Object cet = ctx.getErrorCode(); - assertEquals(ErrorCodes.ARITY, cet, "Expected ARITY error but got result: " + ctx.getValue()); - } - - public static void assertTrustError(Context ctx) { - Object cet = ctx.getErrorCode(); - assertEquals(ErrorCodes.TRUST, cet, "Expected TRUST error but got result: " + ctx.getValue()); - } - - public static void assertCompileError(Context ctx) { - Object cet = ctx.getErrorCode(); - assertEquals(ErrorCodes.COMPILE, cet, "Expected COMPILE error but got result: " + ctx.getValue()); - } - - public static void assertBoundsError(Context ctx) { - Object cet = ctx.getErrorCode(); - assertEquals(ErrorCodes.BOUNDS, cet, "Expected BOUNDS error but got result: " + ctx.getValue()); - } - - public static void assertCastError(Context ctx) { - Object cet = ctx.getErrorCode(); - assertEquals(ErrorCodes.CAST, cet, "Expected CAST error but got result: " + ctx.getValue()); - } - - public static void assertDepthError(Context ctx) { - Object cet = ctx.getErrorCode(); - assertEquals(ErrorCodes.DEPTH, cet, "Expected DEPTH error but got: " + ctx.getValue()); - } - - public static void assertJuiceError(Context ctx) { - Object cet = ctx.getErrorCode(); - assertEquals(ErrorCodes.JUICE, cet, "Expected JUICE error but got: " + ctx.getValue()); - } - - public static void assertUndeclaredError(Context ctx) { - Object cet = ctx.getErrorCode(); - assertEquals(ErrorCodes.UNDECLARED, cet, "Expected UNDECLARED error but got: " + ctx.getValue()); - } - - public static void assertStateError(Context ctx) { - Object cet = ctx.getErrorCode(); - assertEquals(ErrorCodes.STATE, cet, "Expected STATE error but got: " + ctx.getValue()); - } - - public static void assertArgumentError(Context ctx) { - Object cet = ctx.getErrorCode(); - assertEquals(ErrorCodes.ARGUMENT, cet, "Expected ARGUMENT error but got: " + ctx.getValue()); - } - - public static void assertMemoryError(Context ctx) { - Object cet = ctx.getErrorCode(); - assertEquals(ErrorCodes.MEMORY, cet, "Expected MEMORY error but got: " + ctx.getValue()); - } - - - public static void assertFundsError(Context ctx) { - Object cet = ctx.getErrorCode(); - assertEquals(ErrorCodes.FUNDS, cet, "Expected FUNDS error but got: " + ctx.getValue()); - } - - public static void assertNobodyError(Context ctx) { - Object cet = ctx.getErrorCode(); - assertEquals(ErrorCodes.NOBODY, cet, "Expected NOBODY error but got: " + ctx.getValue()); - } - - public static void assertSequenceError(Context ctx) { - Object cet = ctx.getErrorCode(); - assertEquals(ErrorCodes.SEQUENCE, cet, "Expected SEQUENCE error but got: " + ctx.getValue()); - } - - public static void assertAssertError(Context ctx) { - Object cet = ctx.getErrorCode(); - assertEquals(ErrorCodes.ASSERT, cet, "Expected ASSERT error but got: " + ctx.getValue()); - } -} diff --git a/convex-core/src/test/java/convex/test/Samples.java b/convex-core/src/test/java/convex/test/Samples.java deleted file mode 100644 index 5ce414f11..000000000 --- a/convex-core/src/test/java/convex/test/Samples.java +++ /dev/null @@ -1,282 +0,0 @@ -package convex.test; - -import static org.junit.Assert.assertTrue; - -import java.util.Random; -import java.util.stream.Stream; - -import org.junit.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; - -import convex.core.crypto.AKeyPair; -import convex.core.crypto.ASignature; -import convex.core.crypto.Ed25519KeyPair; -import convex.core.crypto.Ed25519Signature; -import convex.core.data.ACell; -import convex.core.data.ADataStructure; -import convex.core.data.AMap; -import convex.core.data.ASequence; -import convex.core.data.ASet; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.Address; -import convex.core.data.Blob; -import convex.core.data.BlobMap; -import convex.core.data.BlobMaps; -import convex.core.data.BlobTree; -import convex.core.data.Blobs; -import convex.core.data.Format; -import convex.core.data.Hash; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.List; -import convex.core.data.Lists; -import convex.core.data.LongBlob; -import convex.core.data.MapLeaf; -import convex.core.data.MapTree; -import convex.core.data.Maps; -import convex.core.data.Sets; -import convex.core.data.StringShort; -import convex.core.data.StringTree; -import convex.core.data.Strings; -import convex.core.data.Syntax; -import convex.core.data.VectorLeaf; -import convex.core.data.VectorTree; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMBool; -import convex.core.data.prim.CVMByte; -import convex.core.data.prim.CVMChar; -import convex.core.data.prim.CVMDouble; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.InvalidDataException; -import convex.core.exceptions.ValidationException; -import convex.core.init.Init; -import convex.core.lang.RT; -import convex.core.lang.Symbols; -import convex.core.lang.ops.Do; -import convex.core.transactions.Invoke; - -/** - * Miscellaneous value objects for testing purposes - * - */ -public class Samples { - - public static Hash BAD_HASH = Hash.fromHex("1234000012340000123400001234000012340000123400001234000012340000"); - - /** - * An Address which cannot be valid - */ - public static final Address BAD_ADDRESS = Address.create(7777777777L); - public static final AccountKey BAD_ACCOUNTKEY = AccountKey.dummy("bbbb"); - - public static final AKeyPair KEY_PAIR=Ed25519KeyPair.createSeeded(13371337L); - public static final AccountKey ACCOUNT_KEY = KEY_PAIR.getAccountKey(); - - public static final ASignature BAD_SIGNATURE = Ed25519Signature.wrap(Blobs.createRandom(64).getBytes()); - - - - public static final VectorLeaf INT_VECTOR_10 = createTestIntVector(10); - public static final VectorLeaf INT_VECTOR_16 = createTestIntVector(16); - public static final VectorLeaf INT_VECTOR_23 = createTestIntVector(23); - public static final VectorTree INT_VECTOR_32 = createTestIntVector(32); - public static final VectorTree INT_VECTOR_256 = createTestIntVector(256); - public static final VectorLeaf INT_VECTOR_300 = createTestIntVector(300); - - public static final AVector> VECTOR_OF_VECTORS = Vectors.of(INT_VECTOR_10, INT_VECTOR_16, - INT_VECTOR_23); - - public static final List INT_LIST_10 = Lists.create(INT_VECTOR_10); - public static final List INT_LIST_300 = Lists.create(INT_VECTOR_300); - - public static final ASet INT_SET_10 = Sets.create(INT_VECTOR_10); - public static final ASet INT_SET_300 = Sets.create(INT_VECTOR_300); - - - public static final MapLeaf LONG_MAP_5 = createTestLongMap(5); - public static final MapTree LONG_MAP_10 = createTestLongMap(10); - public static final MapTree LONG_MAP_100 = createTestLongMap(100); - - public static final BlobMap INT_BLOBMAP_7 = BlobMaps.of(Blob.fromHex(""), 0, Blob.fromHex("0001"), 1, - Blob.fromHex("01"), 2, Blob.fromHex("010000"), 3, Blob.fromHex("010001"), 4, Blob.fromHex("ff0000"), 5, - Blob.fromHex("ff0101"), 6); - - public static final ASet LONG_SET_5 = Sets.of(1,2,3,4,5); - public static final ASet LONG_SET_10 = Sets.create(INT_VECTOR_10); - public static final ASet LONG_SET_100 = Sets.create(INT_VECTOR_300); - - public static final Blob ONE_ZERO_BYTE_DATA = Blob.fromHex("00"); - - public static final Keyword FOO = Keyword.create("foo"); - public static final Keyword BAR = Keyword.create("bar"); - - public static final AVector DIABOLICAL_VECTOR_30_30; - public static final AVector DIABOLICAL_VECTOR_2_10000; - public static final AMap DIABOLICAL_MAP_30_30; - public static final AMap DIABOLICAL_MAP_2_10000; - - public static final Random rand = new Random(123); - - public static final long BIG_BLOB_LENGTH = 10000; - public static final BlobTree BIG_BLOB_TREE = Blobs.createRandom(Samples.rand, BIG_BLOB_LENGTH); - public static final Blob FULL_BLOB = Blobs.createRandom(Samples.rand, Blob.CHUNK_LENGTH); - - public static final ASignature FAKE_SIGNATURE = Ed25519Signature.wrap(new byte[Ed25519Signature.SIGNATURE_LENGTH]); - - public static final Blob MAX_EMBEDDED_BLOB = createTestBlob(Format.MAX_EMBEDDED_LENGTH-Format.getVLCLength(Format.MAX_EMBEDDED_LENGTH)-1); - public static final Blob NON_EMBEDDED_BLOB = createTestBlob(MAX_EMBEDDED_BLOB.count()+1); - - public static final StringShort MAX_EMBEDDED_STRING= StringShort.create("[0x1234567812345678123456781234567812345678123456781234567812345678]"); - public static final StringShort NON_EMBEDDED_STRING= StringShort.create(MAX_EMBEDDED_STRING.toString()+" "); - public static final StringShort MAX_SHORT_STRING= StringShort.create(createRandomString(StringShort.MAX_LENGTH)); - public static final StringTree MIN_TREE_STRING= StringTree.create(createRandomString(StringTree.MINIMUM_LENGTH)); - - - - static { - try { - // we should be able to actually build these, thanks to structural sharing. - DIABOLICAL_VECTOR_30_30 = createNastyNestedVector(30, 30); - DIABOLICAL_VECTOR_2_10000 = createNastyNestedVector(2, 10000); - DIABOLICAL_MAP_30_30 = createNastyNestedMap(30, 30); - DIABOLICAL_MAP_2_10000 = createNastyNestedMap(2, 10000); - } catch (Throwable t) { - t.printStackTrace(); - throw t; - } - } - - - /** - * Create a random test Blob of the given size - * @param size - * @return - */ - static Blob createTestBlob(long size) { - Blob b=Blob.createRandom(new Random(), size); - return b; - } - - private static String createRandomString(int n) { - char [] cs=new char[n]; - for (int i=0; i> T createTestIntVector(int size) { - AVector v = Vectors.empty(); - for (int i = 0; i < size; i++) { - v = v.append(CVMLong.create(i)); - } - return (T) v; - } - - private static AMap createNastyNestedMap(int fanout, int depth) { - AMap m = Maps.empty(); - for (long i = 0; i < depth; i++) { - m = createRepeatedValueMap(m, fanout); - m.getHash(); // needed to to stop hash calculations getting too deep - } - return m; - } - - private static AMap createRepeatedValueMap(Object v, int count) { - Object[] obs = new Object[count * 2]; - for (int i = 0; i < count; i++) { - obs[i * 2] = RT.cvm(i); - obs[i * 2 + 1] = RT.cvm(v); - } - return Maps.of(obs); - } - - @SuppressWarnings("unchecked") - public static R createRandomSubset(ADataStructure v, double prob, int seed) { - ADataStructure result=v.empty(); - - Random r=new Random(seed); - for (T o: (ASequence)RT.sequence(v)) { - if (r.nextDouble()<=prob) { - result=result.conj(o); - } - } - return (R) result; - } - - private static AVector createNastyNestedVector(int fanout, int depth) { - AVector m = Vectors.empty(); - for (int i = 0; i < depth; i++) { - m = Vectors.repeat(m, fanout); - m.getHash(); // needed to to stop hash calculations getting too deep - } - return m; - } - - @SuppressWarnings("unchecked") - private static > T createTestLongMap(int n) { - AMap a = Maps.empty(); - for (long i = 0; i < n; i++) { - CVMLong cl=CVMLong.create(i); - a = a.assoc(cl, cl); - } - return (T) a; - } - - @Test - public void validateDataObjects() throws InvalidDataException, ValidationException { - INT_VECTOR_300.validate(); - assertTrue(INT_VECTOR_300.isCanonical()); - INT_VECTOR_10.validate(); - assertTrue(INT_VECTOR_10.isCanonical()); - BAD_HASH.validate(); - } - - public static ACell[] VALUES=new ACell[] { - null, - Keywords.FOO, - FULL_BLOB, - MAX_EMBEDDED_BLOB, - LongBlob.create(-1), - INT_LIST_10, - Lists.empty(), - INT_VECTOR_300, - Vectors.empty(), - LONG_MAP_100, - Syntax.of(1), - Syntax.create(Vectors.empty(),Maps.of(1,2)), - Invoke.create(Init.GENESIS_ADDRESS, 0, (ACell)null), - Maps.empty(), - LONG_SET_10, - Sets.empty(), - Sets.of(1,2,3), - CVMDouble.ONE, - CVMDouble.NaN, - CVMLong.MAX_VALUE, - CVMLong.MIN_VALUE, - CVMByte.ZERO, - CVMBool.TRUE, - CVMBool.FALSE, - MAX_SHORT_STRING, - BAD_HASH, - Symbols.FOO, - Strings.EMPTY, - MAX_EMBEDDED_STRING, - Do.EMPTY, - Address.ZERO, - Address.create(666666), - AccountKey.ZERO, - CVMChar.A - }; - - public static class ValueArgumentsProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(VALUES).map(cell -> Arguments.of(cell)); - } - } -} diff --git a/convex-core/src/test/java/convex/test/Testing.java b/convex-core/src/test/java/convex/test/Testing.java deleted file mode 100644 index 60d6ee612..000000000 --- a/convex-core/src/test/java/convex/test/Testing.java +++ /dev/null @@ -1,39 +0,0 @@ -package convex.test; - -import java.io.IOException; - -import convex.core.Constants; -import convex.core.data.ACell; -import convex.core.data.AList; -import convex.core.lang.Context; -import convex.core.lang.Reader; -import convex.core.util.Utils; - -public class Testing { - - /** - * Runs all tests in a forked context - * @param ctx - * @param resourceName - * @return Updates context after all test are run. This will be a new fork. - */ - public static Context runTests(Context ctx, String resourceName) { - ctx=ctx.fork(); - try { - String source=Utils.readResourceAsString(resourceName); - AList forms=Reader.readAll(source); - for (ACell form: forms) { - ctx=ctx.eval(form); - if (ctx.isExceptional()) { - System.err.println("Error in form: "+form); - return ctx; - } - ctx=ctx.withJuice(Constants.MAX_TRANSACTION_JUICE); - } - return ctx; - } catch (IOException e) { - throw Utils.sneakyThrow(e); - } - } - -} diff --git a/convex-core/src/test/java/convex/test/generators/AddressGen.java b/convex-core/src/test/java/convex/test/generators/AddressGen.java deleted file mode 100644 index 96b54ebbb..000000000 --- a/convex-core/src/test/java/convex/test/generators/AddressGen.java +++ /dev/null @@ -1,19 +0,0 @@ -package convex.test.generators; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.data.Address; - -public class AddressGen extends Generator
{ - public AddressGen() { - super(Address.class); - } - - @Override - public Address generate(SourceOfRandomness r, GenerationStatus status) { - - return Address.create(r.nextLong(0, Long.MAX_VALUE)); - } -} diff --git a/convex-core/src/test/java/convex/test/generators/AnyMapGen.java b/convex-core/src/test/java/convex/test/generators/AnyMapGen.java deleted file mode 100644 index 80810cb2f..000000000 --- a/convex-core/src/test/java/convex/test/generators/AnyMapGen.java +++ /dev/null @@ -1,60 +0,0 @@ -package convex.test.generators; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.data.ABlob; -import convex.core.data.AMap; -import convex.core.data.AString; -import convex.core.data.AVector; -import convex.core.data.BlobMap; -import convex.core.data.Format; -import convex.core.data.Maps; -import convex.test.Samples; - -/** - * Generator for arbitrary maps, including BlobMaps - * - */ -@SuppressWarnings("rawtypes") -public class AnyMapGen extends Generator { - public AnyMapGen() { - super(AMap.class); - } - - @Override - public AMap generate(SourceOfRandomness r, GenerationStatus status) { - - int type = r.nextInt(); - switch (type % 8) { - case 0: - return Maps.empty(); - case 1: - return Samples.LONG_MAP_5; - case 2: - return Samples.LONG_MAP_10; - case 3: - return Samples.LONG_MAP_100; - case 4: { - Object o1 = gen().make(PrimitiveGen.class).generate(r, status); - Object o2 = gen().make(StringGen.class).generate(r, status); - return Maps.of(o1, o2); - } - case 5: - return BlobMap.EMPTY; - case 6: { - ABlob o1 = Format.encodedBlob(gen().make(PrimitiveGen.class).generate(r, status)); - AString o2 = gen().make(StringGen.class).generate(r, status); - return BlobMap.create(o1, o2); - } - - default: { - AVector vec = gen().make(VectorGen.class).generate(r, status); - vec = (AVector) vec.subList(0, vec.size() & (~1)); - Object[] os = vec.toArray(); - return Maps.of(os); - } - } - } -} diff --git a/convex-core/src/test/java/convex/test/generators/BlobGen.java b/convex-core/src/test/java/convex/test/generators/BlobGen.java deleted file mode 100644 index 7fed957aa..000000000 --- a/convex-core/src/test/java/convex/test/generators/BlobGen.java +++ /dev/null @@ -1,48 +0,0 @@ -package convex.test.generators; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.data.ABlob; -import convex.core.data.Blobs; -import convex.core.data.LongBlob; -import convex.test.Samples; - -/** - * Generator for binary Blobs - * - */ -public class BlobGen extends Generator { - public BlobGen() { - super(ABlob.class); - } - - @Override - public ABlob generate(SourceOfRandomness r, GenerationStatus status) { - - long len = status.size(); - int type = r.nextInt(); - switch (type % 10) { - case 0: - return LongBlob.create(r.nextLong()); - case 1: - return Samples.FULL_BLOB; - case 2: - return Samples.BIG_BLOB_TREE; - case 3: - return Samples.MAX_EMBEDDED_BLOB; - case 4: - return Samples.NON_EMBEDDED_BLOB; - case 5: { - // use a slice from a big blob - long length=Math.min(len, Samples.BIG_BLOB_LENGTH); - length=r.nextLong(0, length); - long start=r.nextLong(0,Samples.BIG_BLOB_LENGTH-length); - return Samples.BIG_BLOB_TREE.slice(start,length); - } - default: - return Blobs.createRandom(r.toJDKRandom(), len); - } - } -} diff --git a/convex-core/src/test/java/convex/test/generators/CollectionGen.java b/convex-core/src/test/java/convex/test/generators/CollectionGen.java deleted file mode 100644 index 6a4d5d340..000000000 --- a/convex-core/src/test/java/convex/test/generators/CollectionGen.java +++ /dev/null @@ -1,38 +0,0 @@ -package convex.test.generators; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.data.ACell; -import convex.core.data.ACollection; - -/** - * Generator for arbitrary collections - */ -public class CollectionGen extends Generator> { - @SuppressWarnings("rawtypes") - private static final Class cls = (Class) ACollection.class; - - @SuppressWarnings("unchecked") - public CollectionGen() { - super(cls); - } - - @SuppressWarnings("unchecked") - @Override - public ACollection generate(SourceOfRandomness r, GenerationStatus status) { - int type = r.nextInt(3); - switch (type) { - case 0: - return gen().make(VectorGen.class).generate(r, status); - case 1: - return gen().make(ListGen.class).generate(r, status); - case 2: - return gen().make(SetGen.class).generate(r, status); - - default: - throw new Error("Unexpected type: " + type); - } - } -} diff --git a/convex-core/src/test/java/convex/test/generators/DataStructureGen.java b/convex-core/src/test/java/convex/test/generators/DataStructureGen.java deleted file mode 100644 index 0e33fae7f..000000000 --- a/convex-core/src/test/java/convex/test/generators/DataStructureGen.java +++ /dev/null @@ -1,46 +0,0 @@ -package convex.test.generators; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.data.ACell; -import convex.core.data.ADataStructure; -import convex.core.data.MapEntry; - -/** - * Generator for arbitrary collections - */ -public class DataStructureGen extends Generator> { - @SuppressWarnings("rawtypes") - private static final Class cls = (Class) ADataStructure.class; - - @SuppressWarnings("unchecked") - public DataStructureGen() { - super(cls); - } - - @SuppressWarnings("unchecked") - @Override - public ADataStructure generate(SourceOfRandomness r, GenerationStatus status) { - int type = r.nextInt(5); - - switch (type) { - case 0: - return gen().make(MapGen.class).generate(r, status); - case 1: - return gen().make(VectorGen.class).generate(r, status); - case 2: - return gen().make(ListGen.class).generate(r, status); - case 3: - return gen().make(SetGen.class).generate(r, status); - - // generate map entries as special cases of vectors - case 4: { - Generator vgen = gen().make(ValueGen.class); - return MapEntry.create(vgen.generate(r, status), vgen.generate(r, status)); - } - } - throw new Error("Bad Type!"); - } -} diff --git a/convex-core/src/test/java/convex/test/generators/FormGen.java b/convex-core/src/test/java/convex/test/generators/FormGen.java deleted file mode 100644 index 62788a2be..000000000 --- a/convex-core/src/test/java/convex/test/generators/FormGen.java +++ /dev/null @@ -1,72 +0,0 @@ -package convex.test.generators; - -import java.util.List; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.data.ACell; -import convex.core.data.AHashMap; -import convex.core.data.Lists; -import convex.core.data.Strings; -import convex.core.data.Symbol; -import convex.core.data.Syntax; -import convex.core.data.Vectors; -import convex.core.lang.Core; -import convex.core.lang.RT; - -/** - * Generator for plausible forms - */ -public class FormGen extends Generator { - public FormGen() { - super(ACell.class); - } - - @Override - public ACell generate(SourceOfRandomness r, GenerationStatus status) { - int type = r.nextInt(8); - switch (type) { - case 0: - return null; - - case 1: - return Syntax.create(generate(r, status)); - - case 2: - return gen().make(PrimitiveGen.class).generate(r, status); - case 3: - return Strings.create(gen().type(String.class).generate(r, status)); - - case 4: - return gen().make(NumericGen.class).generate(r, status); - - case 5: { - // random form containing core symbol at head - List subForms = this.times(r.nextInt(4)).generate(r, status); - AHashMap env = Core.ENVIRONMENT; - int n = (int) env.count(); - Symbol sym = env.entryAt(r.nextInt(n)).getKey(); - return RT.cons(sym, Lists.create(subForms)); - } - - case 6: { - // random core symbol - AHashMap env = Core.ENVIRONMENT; - int n = (int) env.count(); - Symbol sym = env.entryAt(r.nextInt(n)).getKey(); - return sym; - } - - case 7: { - // a vector of random subforms - List subForms = this.times(r.nextInt(4)).generate(r, status); - return Vectors.create(subForms); - } - - default: - throw new Error("Unexpected type: " + type); - } - } -} diff --git a/convex-core/src/test/java/convex/test/generators/KeywordGen.java b/convex-core/src/test/java/convex/test/generators/KeywordGen.java deleted file mode 100644 index 282943353..000000000 --- a/convex-core/src/test/java/convex/test/generators/KeywordGen.java +++ /dev/null @@ -1,33 +0,0 @@ -package convex.test.generators; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.data.Keyword; -import convex.test.Samples; - -/** - * Generator for Keyword objects - * - */ -public class KeywordGen extends Generator { - public KeywordGen() { - super(Keyword.class); - } - - @Override - public Keyword generate(SourceOfRandomness r, GenerationStatus status) { - - int type = r.nextInt(5); - switch (type) { - case 0: - return Samples.FOO; - case 1: - return Samples.BAR; - default: { - return Keyword.create("key" + r.nextLong(0, status.size())); - } - } - } -} diff --git a/convex-core/src/test/java/convex/test/generators/ListGen.java b/convex-core/src/test/java/convex/test/generators/ListGen.java deleted file mode 100644 index c64090fe7..000000000 --- a/convex-core/src/test/java/convex/test/generators/ListGen.java +++ /dev/null @@ -1,26 +0,0 @@ -package convex.test.generators; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.data.AList; -import convex.core.data.List; - -/** - * Generator for vectors of arbitrary values - * - */ -@SuppressWarnings("rawtypes") -public class ListGen extends Generator { - public ListGen() { - super(AList.class); - } - - @SuppressWarnings("unchecked") - @Override - public AList generate(SourceOfRandomness r, GenerationStatus status) { - - return List.reverse(gen().make(VectorGen.class).generate(r, status)); - } -} diff --git a/convex-core/src/test/java/convex/test/generators/MapGen.java b/convex-core/src/test/java/convex/test/generators/MapGen.java deleted file mode 100644 index fe67bbc82..000000000 --- a/convex-core/src/test/java/convex/test/generators/MapGen.java +++ /dev/null @@ -1,48 +0,0 @@ -package convex.test.generators; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.data.ACell; -import convex.core.data.AHashMap; -import convex.core.data.AVector; -import convex.core.data.Maps; -import convex.test.Samples; - -/** - * Generator for arbitrary maps - * - */ -@SuppressWarnings("rawtypes") -public class MapGen extends Generator { - public MapGen() { - super(AHashMap.class); - } - - @SuppressWarnings("unchecked") - @Override public AHashMap generate( - SourceOfRandomness r, - GenerationStatus status) { - - int type=r.nextInt(); - switch (type%6) { - case 0: return Maps.empty(); - case 1: return Samples.LONG_MAP_5; - case 2: return Samples.LONG_MAP_10; - case 3: return Samples.LONG_MAP_100; - case 4: { - ACell o1=gen().make(PrimitiveGen.class).generate(r, status); - ACell o2=gen().make(StringGen.class).generate(r, status); - return Maps.create(o1,o2); - } - - default: { - AVector vec=gen().make(VectorGen.class).generate(r, status); - vec=(AVector) vec.subList(0, vec.size()&(~1)); - Object[] os=vec.toArray(); - return Maps.of(os); - } - } - } -} diff --git a/convex-core/src/test/java/convex/test/generators/NumericGen.java b/convex-core/src/test/java/convex/test/generators/NumericGen.java deleted file mode 100644 index 27cbe9c30..000000000 --- a/convex-core/src/test/java/convex/test/generators/NumericGen.java +++ /dev/null @@ -1,39 +0,0 @@ -package convex.test.generators; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.data.prim.APrimitive; -import convex.core.data.prim.CVMByte; -import convex.core.data.prim.CVMDouble; -import convex.core.data.prim.CVMLong; - -/** - * Generator for arbitrary numeric values - * - */ -public class NumericGen extends Generator { - public NumericGen() { - super(APrimitive.class); - } - - @Override - public APrimitive generate(SourceOfRandomness r, GenerationStatus status) { - int type = r.nextInt(3); - switch (type) { - case 0: - return CVMByte.create(r.nextLong()); - case 1: - return CVMLong.create(r.nextLong()); - case 2: - return CVMDouble.create(r.nextDouble()); -// TODO: bigger numerics? -// case 6: -// return gen().type(BigInteger.class).generate(r, status); -// case 7: -// return gen().type(BigDecimal.class).generate(r, status); - } - throw new Error("Unexpected type: " + type); - } -} diff --git a/convex-core/src/test/java/convex/test/generators/PrimitiveGen.java b/convex-core/src/test/java/convex/test/generators/PrimitiveGen.java deleted file mode 100644 index 1eba23e18..000000000 --- a/convex-core/src/test/java/convex/test/generators/PrimitiveGen.java +++ /dev/null @@ -1,46 +0,0 @@ -package convex.test.generators; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.data.ACell; -import convex.core.data.prim.CVMBool; -import convex.core.data.prim.CVMByte; -import convex.core.data.prim.CVMChar; -import convex.core.data.prim.CVMDouble; -import convex.core.data.prim.CVMLong; - -/** - * Generator for primitive data values - */ -public class PrimitiveGen extends Generator { - public final static PrimitiveGen INSTANCE = new PrimitiveGen(); - - // public final Generator BYTE = gen().type(byte.class); - - public PrimitiveGen() { - super(ACell.class); - } - - @Override - public ACell generate(SourceOfRandomness r, GenerationStatus status) { - int type = r.nextInt(6); - switch (type) { - case 0: - return null; - case 1: - return CVMByte.create(r.nextLong()); - case 2: - return CVMChar.create(r.nextLong()); - case 3: - return CVMLong.create(r.nextLong()); - case 4: - return CVMDouble.create(r.nextDouble()); - case 5: - return CVMBool.create(r.nextBoolean()); - default: - throw new Error("Unexpected type: " + type); - } - } -} diff --git a/convex-core/src/test/java/convex/test/generators/RecordGen.java b/convex-core/src/test/java/convex/test/generators/RecordGen.java deleted file mode 100644 index 492fb300e..000000000 --- a/convex-core/src/test/java/convex/test/generators/RecordGen.java +++ /dev/null @@ -1,36 +0,0 @@ -package convex.test.generators; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.Belief; -import convex.core.Block; -import convex.core.Constants; -import convex.core.data.ARecord; -import convex.core.init.InitTest; -import convex.core.lang.TestState; - -/** - * Generator for binary Blobs - * - */ -public class RecordGen extends Generator { - public RecordGen() { - super(ARecord.class); - } - - @Override - public ARecord generate(SourceOfRandomness r, GenerationStatus status) { - - int type = r.nextInt(); - switch (type % 8) { - case 0: - return Belief.createSingleOrder(InitTest.HERO_KEYPAIR); - case 1: - return TestState.STATE; - default: - return Block.of(Constants.INITIAL_TIMESTAMP,InitTest.FIRST_PEER_KEY); - } - } -} diff --git a/convex-core/src/test/java/convex/test/generators/SetGen.java b/convex-core/src/test/java/convex/test/generators/SetGen.java deleted file mode 100644 index 2d343cff4..000000000 --- a/convex-core/src/test/java/convex/test/generators/SetGen.java +++ /dev/null @@ -1,46 +0,0 @@ -package convex.test.generators; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.data.ACell; -import convex.core.data.ASet; -import convex.core.data.AVector; -import convex.core.data.Sets; -import convex.test.Samples; - -/** - * Generator for sets of values - */ -@SuppressWarnings("rawtypes") -public class SetGen extends Generator { - public SetGen() { - super(ASet.class); - } - - @SuppressWarnings("unchecked") - @Override - public ASet generate(SourceOfRandomness r, GenerationStatus status) { - - int type = r.nextInt(); - switch (type % 6) { - case 0: - return Sets.empty(); - case 1: { - Object o1 = gen().make(ValueGen.class).generate(r, status); - return Sets.of(o1); - } - case 2: - return Samples.LONG_SET_5; - case 3: - return Samples.LONG_SET_10; - case 4: - return Samples.LONG_SET_100; - default: { - AVector o1 = gen().make(VectorGen.class).generate(r, status); - return Sets.create(o1); - } - } - } -} diff --git a/convex-core/src/test/java/convex/test/generators/StringGen.java b/convex-core/src/test/java/convex/test/generators/StringGen.java deleted file mode 100644 index 6de3dbce5..000000000 --- a/convex-core/src/test/java/convex/test/generators/StringGen.java +++ /dev/null @@ -1,32 +0,0 @@ -package convex.test.generators; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.data.AString; -import convex.core.data.Strings; -import convex.test.Samples; - -public class StringGen extends Generator { - public StringGen() { - super(AString.class); - } - - @Override - public AString generate(SourceOfRandomness r, GenerationStatus status) { - - int type=r.nextInt(); - switch (type%12) { - case 0: return Strings.empty(); - case 1: return Samples.MAX_EMBEDDED_STRING; - case 2: return Samples.NON_EMBEDDED_STRING; - case 3: return Samples.MAX_SHORT_STRING; - case 4: return Samples.MIN_TREE_STRING; - - default: { - return Strings.create(gen().type(String.class).generate(r, status)); - } - } - } -} diff --git a/convex-core/src/test/java/convex/test/generators/TransactionGen.java b/convex-core/src/test/java/convex/test/generators/TransactionGen.java deleted file mode 100644 index 8b691bf20..000000000 --- a/convex-core/src/test/java/convex/test/generators/TransactionGen.java +++ /dev/null @@ -1,39 +0,0 @@ -package convex.test.generators; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.Constants; -import convex.core.data.Address; -import convex.core.data.Vectors; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Invoke; -import convex.core.transactions.Transfer; -import convex.test.Samples; - -public class TransactionGen extends Generator { - public TransactionGen() { - super(ATransaction.class); - } - - @Override - public ATransaction generate(SourceOfRandomness r, GenerationStatus status) { - - long amt = r.nextLong(0, Constants.MAX_SUPPLY); - - Address src = Address.create(Samples.KEY_PAIR.getAccountKey()); - long seq = r.nextInt(10000); - int type = r.nextInt(2); - switch (type) { - case 0: { - return Transfer.create(src,seq, src, amt); - } - case 1: { - return Invoke.create(src,seq, Vectors.empty()); - } - default: - throw new Error("Invalid type: " + type); - } - } -} diff --git a/convex-core/src/test/java/convex/test/generators/ValueGen.java b/convex-core/src/test/java/convex/test/generators/ValueGen.java deleted file mode 100644 index 64ae987e7..000000000 --- a/convex-core/src/test/java/convex/test/generators/ValueGen.java +++ /dev/null @@ -1,59 +0,0 @@ -package convex.test.generators; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.data.ACell; -import convex.core.data.Symbol; -import convex.core.data.Syntax; -import convex.core.data.prim.CVMLong; - -/** - * Generator for arbitrary values - */ -public class ValueGen extends Generator { - public ValueGen() { - super(ACell.class); - } - - @Override - public ACell generate(SourceOfRandomness r, GenerationStatus status) { - int type = r.nextInt(15); - switch (type) { - case 0: - return null; - case 1: - return gen().make(PrimitiveGen.class).generate(r, status); - case 2: - return gen().make(StringGen.class).generate(r, status); - case 3: - return gen().make(VectorGen.class).generate(r, status); - case 4: - return gen().make(ListGen.class).generate(r, status); - case 5: - return gen().make(MapGen.class).generate(r, status); - case 6: - return gen().make(SetGen.class).generate(r, status); - case 7: - return gen().make(BlobGen.class).generate(r, status); - case 8: - return gen().make(AddressGen.class).generate(r, status); - case 9: - return gen().make(NumericGen.class).generate(r, status); - case 10: - return CVMLong.create(r.nextLong()); - case 11: - return Symbol.create("sym" + gen().type(Long.class).generate(r, status)); - case 12: - return gen().make(KeywordGen.class).generate(r, status); - case 13: - return gen().make(RecordGen.class).generate(r, status); - case 14: - return Syntax.create(generate(r, status)); - - default: - throw new Error("Unexpected type: " + type); - } - } -} diff --git a/convex-core/src/test/java/convex/test/generators/VectorGen.java b/convex-core/src/test/java/convex/test/generators/VectorGen.java deleted file mode 100644 index ed9636aaf..000000000 --- a/convex-core/src/test/java/convex/test/generators/VectorGen.java +++ /dev/null @@ -1,70 +0,0 @@ -package convex.test.generators; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; - -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.MapEntry; -import convex.core.data.Vectors; -import convex.test.Samples; - -/** - * Generator for vectors of arbitrary values - * - */ -@SuppressWarnings("rawtypes") -public class VectorGen extends Generator { - public VectorGen() { - super(AVector.class); - } - - @Override - public AVector generate(SourceOfRandomness r, GenerationStatus status) { - - int type = r.nextInt(15); - switch (type) { - case 0: { - Object o = gen().make(PrimitiveGen.class).generate(r, status); - return Vectors.of(o); - } - case 1: { - Object o1 = gen().make(PrimitiveGen.class).generate(r, status); - Object o2 = gen().make(StringGen.class).generate(r, status); - return Vectors.of(o1, o2); - } - case 2: { - Object o1 = gen().make(ValueGen.class).generate(r, status); - Object o2 = gen().make(StringGen.class).generate(r, status); - Object o3 = gen().make(FormGen.class).generate(r, status); - return Vectors.of(o1, o2, o3); - } - - case 3: - return Samples.INT_VECTOR_10; - case 4: - return Samples.INT_VECTOR_300; - case 5: - return Samples.INT_VECTOR_16; - case 6: - return Samples.INT_VECTOR_256; - case 7: - return Vectors.empty(); - case 8: { - ACell o1 = gen().make(ValueGen.class).generate(r, status); - ACell o2 = gen().make(ValueGen.class).generate(r, status); - return MapEntry.create(o1, o2); - } - default: { - int n = (int) (1 + (Math.sqrt(status.size()))); - Object[] obs = new Object[n]; - ValueGen g = gen().make(ValueGen.class); - for (int i = 0; i < n; i++) { - obs[i] = g.generate(r, status); - } - return Vectors.of(obs); - } - } - } -} diff --git a/convex-core/src/test/java/convex/util/BigIntegerParamTest.java b/convex-core/src/test/java/convex/util/BigIntegerParamTest.java deleted file mode 100644 index 56a02acac..000000000 --- a/convex-core/src/test/java/convex/util/BigIntegerParamTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package convex.util; - -import static org.junit.Assert.assertEquals; - -import java.math.BigInteger; -import java.util.Arrays; -import java.util.Collection; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import convex.core.data.AArrayBlob; -import convex.core.data.Blob; -import convex.core.util.Utils; - -@RunWith(Parameterized.class) -public class BigIntegerParamTest { - private BigInteger num; - - public BigIntegerParamTest(String label, BigInteger num) { - this.num = num; - } - - @Parameterized.Parameters(name = "{index}: {0}") - public static Collection dataExamples() { - return Arrays.asList(new Object[][] { { "Zero", BigInteger.ZERO }, - { "Short hex string CAFEBABE", Utils.hexToBigInt("CAFEBABE") }, - { "A big number", Utils.hexToBigInt( - "506bc1dc099358e5137292f4efdd57e400f29ba5132aa5d12b18dac1c1f6aaba645c0b7b58158babbfa6c6cd5a48aa7340a8749176b120e8516216787a13dc76") }, - { "Negative big number", Utils.hexToBigInt( - "506bc1dc099358e5137292f4efdd57e400f29ba5132aa5d12b18dac1c1f6aaba645c0b7b58158babbfa6c6cd5a48aa7340a8749176b120e8516216787a13dc76") - .negate() } }); - } - - @Test - public void testHexRoundTrip() { - if (num.signum() < 0) return; - String s = Utils.toHexString(num, (num.bitLength() / 4 + 2) & 0xFFFE); - AArrayBlob d = Blob.fromHex(s); - byte[] bs = d.getBytes(); - BigInteger b = new BigInteger(1, bs); - assertEquals(num, b); - } - -} diff --git a/convex-core/src/test/java/convex/util/BitsTest.java b/convex-core/src/test/java/convex/util/BitsTest.java deleted file mode 100644 index 5c1907b04..000000000 --- a/convex-core/src/test/java/convex/util/BitsTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package convex.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -import convex.core.util.Bits; - -public class BitsTest { - - @Test public void testLeadingZeros() { - assertEquals(16,Bits.leadingZeros(0x00FFFF)); - assertEquals(15,Bits.leadingZeros(0x010000)); - - assertEquals(18,Bits.leadingZeros(16383)); - assertEquals(17,Bits.leadingZeros(16384)); - } -} diff --git a/convex-core/src/test/java/convex/util/GenTestHuge.java b/convex-core/src/test/java/convex/util/GenTestHuge.java deleted file mode 100644 index 3be53524c..000000000 --- a/convex-core/src/test/java/convex/util/GenTestHuge.java +++ /dev/null @@ -1,64 +0,0 @@ -package convex.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.runner.RunWith; - -import com.pholser.junit.quickcheck.Property; -import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; - -import convex.core.util.Huge; - -@RunWith(JUnitQuickcheck.class) -public class GenTestHuge { - - @Property - public void pairOps(Long a, Long b) { - Huge ha=Huge.create(a); - Huge hb=Huge.create(b); - - // multiplication - Huge p = Huge.multiply(a, b); - assertEquals(p.hi,Math.multiplyHigh(a, b)); - assertEquals(p.lo,a*b); - - Huge p0=Huge.multiply(a, 0); - assertEquals(Huge.ZERO,p0); - - Huge p1=Huge.multiply(a, 1); - assertEquals(ha,p1); - - // addition - Huge s=Huge.add(a,b); - assertEquals(s.lo,a+b); - assertEquals(s,ha.add(hb)); - - Huge s2=s.add(-b); - assertEquals(ha,s2); - - - } - - @Property - public void singleOps(Long a) { - Huge ha=Huge.create(a); - - Huge hneg=ha.negate(); - - assertEquals(ha,Huge.ZERO.add(a)); - assertEquals(ha,Huge.ZERO.add(ha)); - - assertEquals(ha,Huge.multiply(1L,a)); - assertEquals(Huge.ZERO,Huge.multiply(0L,a)); - - assertEquals(ha,hneg.negate()); - assertEquals(Huge.ZERO,ha.add(hneg)); - - // TODO: fix 128-bit mul - // Huge h2=ha.mul(ha); - // assertEquals(h2,hneg.mul(hneg)); - - } - - -} diff --git a/convex-core/src/test/java/convex/util/GenTestUMath.java b/convex-core/src/test/java/convex/util/GenTestUMath.java deleted file mode 100644 index 08655b1ae..000000000 --- a/convex-core/src/test/java/convex/util/GenTestUMath.java +++ /dev/null @@ -1,24 +0,0 @@ -package convex.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.runner.RunWith; - -import com.pholser.junit.quickcheck.Property; -import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; - -import convex.core.util.UMath; - -@RunWith(JUnitQuickcheck.class) -public class GenTestUMath { - - - @Property - public void singleOps(Long a) { - long mulHigh=UMath.multiplyHigh(a, a); - - assertEquals(mulHigh,UMath.multiplyHigh(-a, -a)); - } - - -} diff --git a/convex-core/src/test/java/convex/util/HugeTest.java b/convex-core/src/test/java/convex/util/HugeTest.java deleted file mode 100644 index cb86b2672..000000000 --- a/convex-core/src/test/java/convex/util/HugeTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package convex.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -import convex.core.util.Huge; -import convex.core.util.UMath; - -public class HugeTest { - - - @Test public void testMul() { - Huge h1=Huge.multiply(1, -1); - assertEquals(-1,h1.hi); - assertEquals(-1,h1.lo); - - Huge h2=Huge.multiply(0x100000000L, 0x100000000L); - assertEquals(1,h2.hi); - assertEquals(0,h2.lo); - } - - @Test public void testConstants() { - Huge zero=Huge.ZERO; - Huge one=Huge.ONE; - assertEquals(zero, zero.add(0)); - assertEquals(zero, one.sub(one)); - } - - @Test public void testAddRegression() { - long b=-212944119L; - Huge hb=Huge.create(b); - Huge s3=Huge.ZERO.add(b); - assertEquals(hb,s3); - } - - @Test public void testUnsignedCarry() { - assertEquals(0L,UMath.unsignedAddCarry(1L, 1L)); - assertEquals(1L,UMath.unsignedAddCarry(-1L, -1L)); - assertEquals(0L,UMath.unsignedAddCarry(-1L, 0)); - assertEquals(0L,UMath.unsignedAddCarry(0, -1L)); - assertEquals(1L,UMath.unsignedAddCarry(Long.MIN_VALUE, Long.MIN_VALUE)); - } - - -} diff --git a/convex-core/src/test/java/convex/util/TextTest.java b/convex-core/src/test/java/convex/util/TextTest.java deleted file mode 100644 index 17506a8a2..000000000 --- a/convex-core/src/test/java/convex/util/TextTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package convex.util; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; - -import convex.core.util.Text; - -public class TextTest { - - @Test - public void testWhiteSpace() { - checkWhiteSpace(0); - checkWhiteSpace(10); - checkWhiteSpace(32); - checkWhiteSpace(33); - checkWhiteSpace(95); - checkWhiteSpace(96); - checkWhiteSpace(97); - checkWhiteSpace(100); - } - - private void checkWhiteSpace(int len) { - String s = Text.whiteSpace(len); - assertEquals(len, s.length()); - assertEquals("", s.trim()); - } -} diff --git a/convex-core/src/test/java/convex/util/UMathTest.java b/convex-core/src/test/java/convex/util/UMathTest.java deleted file mode 100644 index 0a792b677..000000000 --- a/convex-core/src/test/java/convex/util/UMathTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package convex.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -import convex.core.util.UMath; - -public class UMathTest { - - @Test - public void testMultiplyHigh() { - assertEquals(1L,UMath.multiplyHigh(0x100000000L, 0x100000000L)); - } -} diff --git a/convex-core/src/test/java/convex/util/UtilsTest.java b/convex-core/src/test/java/convex/util/UtilsTest.java deleted file mode 100644 index 0a5bebf5a..000000000 --- a/convex-core/src/test/java/convex/util/UtilsTest.java +++ /dev/null @@ -1,375 +0,0 @@ -package convex.util; - -import static convex.core.lang.TestState.STATE; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.math.BigInteger; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.util.Comparator; -import java.util.function.Function; - -import org.junit.Test; - -import convex.core.Block; -import convex.core.Peer; -import convex.core.State; -import convex.core.data.AVector; -import convex.core.data.Blob; -import convex.core.data.SignedData; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadSignatureException; -import convex.core.init.InitTest; -import convex.core.lang.TestState; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Invoke; -import convex.core.util.Bits; -import convex.core.util.Utils; - -public class UtilsTest { - - @Test - public void testToBigInteger() { - assertEquals(BigInteger.valueOf(255), Utils.toBigInteger(new byte[] { -1 })); - assertEquals(BigInteger.valueOf(256), Utils.toBigInteger(new byte[] { 1, 0 })); - assertEquals(BigInteger.valueOf(65536), Utils.toBigInteger(new byte[] { 1, 0, 0 })); - } - - @Test - public void testHexChar() { - assertEquals('f', Utils.toHexChar(15)); - assertEquals('a', Utils.toHexChar(10)); - assertEquals('9', Utils.toHexChar(9)); - assertEquals('0', Utils.toHexChar(0)); - } - - @Test(expected = IllegalArgumentException.class) - public void testBadHexCharNegative() { - Utils.toHexChar(-1); - } - - @Test(expected = IllegalArgumentException.class) - public void testBadHexChar1() { - Utils.toHexChar(16); - } - - @Test - public void testHexString() { - assertEquals("ff", Utils.toHexString(new byte[] { -1 })); - assertEquals("81", Utils.toHexString(new byte[] { -127 })); - assertEquals("7f", Utils.toHexString(new byte[] { 127 })); - assertEquals("7c", Utils.toHexString(new byte[] { 124 })); - assertEquals("0012457c", Utils.toHexString(0x0012457c)); - } - - @Test - public void testHexToBytes() { - byte[] header = Utils.hexToBytes( - "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c"); - assertEquals(80, header.length); - assertEquals(1, header[0]); - assertEquals(124, header[79]); - assertEquals("7c", Utils.toHexString(header[79])); - assertEquals(0xdeadbeef, Utils.readInt(Utils.hexToBytes("deadbeef", 8), 0)); - - assertNull(Utils.hexToBytes("deadbeef", 10)); // wrong length - assertNull(Utils.hexToBytes("deadb", 5)); // odd length - assertNull(Utils.hexToBytes("zzzz", 4)); // invalid characters - } - - @Test - public void testHexVals() { - assertEquals(0, Utils.hexVal('0')); - assertEquals(15, Utils.hexVal('f')); - assertEquals(12, Utils.hexVal('C')); - } - - @Test - public void testExtractDigit() { - assertEquals(0, Utils.extractDigit(Blob.fromHex("0123"), 0)); - assertEquals(3, Utils.extractDigit(Blob.fromHex("0123"), 3)); - - assertThrows(IndexOutOfBoundsException.class, () -> Utils.extractDigit(Blob.fromHex("0123"), 4)); - assertThrows(IndexOutOfBoundsException.class, () -> Utils.extractDigit(Blob.fromHex("0123"), -1)); - } - - @Test - public void testBigIntegerToHex() { - assertEquals("0101", Utils.toHexString(BigInteger.valueOf(257), 4)); - assertEquals("0", Utils.toHexString(BigInteger.valueOf(0), 1)); - - assertThrows(IllegalArgumentException.class, () -> Utils.toHexString(BigInteger.valueOf(-100), 4)); - assertThrows(IllegalArgumentException.class, () -> Utils.toHexString(BigInteger.valueOf(257), 2)); - } - - @Test - public void testHexVal() { - for (int i = -128; i <= 127; i++) { - char c = (char) i; - try { - char rt = Utils.toHexChar(Utils.hexVal(c)); - assertEquals(Character.toLowerCase(c), rt); - } catch (IllegalArgumentException t) { - assert (!"0123456789abcdefABCDEF".contains(Character.toString(c))); - } - } - } - - @Test - public void testBitLength() { - assertEquals(1, Utils.bitLength(0)); // binary 0 - assertEquals(1, Utils.bitLength(-1)); // binary 1 - assertEquals(2, Utils.bitLength(1)); // binary 01 - assertEquals(2, Utils.bitLength(-2)); // binary 10 - assertEquals(3, Utils.bitLength(2)); // binary 010 - assertEquals(3, Utils.bitLength(-3)); // binary 101 - assertEquals(64, Utils.bitLength(Long.MAX_VALUE)); // max value - assertEquals(64, Utils.bitLength(Long.MAX_VALUE + 1)); // overflow - } - - @Test - public void testIntLeadingZeros() { - assertEquals(32, Bits.leadingZeros(0x00000000)); - assertEquals(16, Bits.leadingZeros(0x00008000)); - assertEquals(0, Bits.leadingZeros(-1)); - } - - @Test - public void testLongLeadingZeros() { - assertEquals(64, Bits.leadingZeros(0L)); - assertEquals(48, Bits.leadingZeros(0x00008000L)); - assertEquals(0, Bits.leadingZeros(-1L)); - } - - @Test - public void testWriteUInt256() { - BigInteger n = BigInteger.valueOf(7); - ByteBuffer b = ByteBuffer.allocate(32); - Utils.writeUInt256(b, n); - b.flip(); - assertEquals(32, b.remaining()); - byte[] bs = Utils.toByteArray(b); - assertEquals(32, bs.length); - assertEquals(0, b.remaining()); - assertEquals("0000000000000000000000000000000000000000000000000000000000000007", Utils.toHexString(bs)); - } - - @Test - public void testWriteBigUInt() { - byte[] ds = new byte[4]; - assertEquals("00000000", Utils.toHexString(ds)); - Utils.writeUInt(BigInteger.valueOf(7), ds, 0, 4); - assertEquals("00000007", Utils.toHexString(ds)); - assertEquals((short) 7, Utils.readShort(ds, 2)); // check short encoding - Utils.writeUInt(BigInteger.valueOf(0xffffffffl), ds, 0, 4); - assertEquals("ffffffff", Utils.toHexString(ds)); - - assertThrows(IllegalArgumentException.class, - () -> Utils.writeUInt(BigInteger.valueOf(0x100000000L), ds, 0, 32)); - assertThrows(IllegalArgumentException.class, () -> Utils.writeUInt(BigInteger.valueOf(-1), ds, 0, 32)); - } - - @Test - public void testToByteArray() { - ByteBuffer buf = ByteBuffer.allocate(1000); - byte[] bs1 = Utils.hexToBytes("cafebabe"); - buf.put(bs1); - - buf.flip(); - byte[] bs2 = Utils.toByteArray(buf); - assertArrayEquals(bs1, bs2); - assertEquals(0, buf.remaining()); - } - - @Test - public void testExtractBitsPositive() { - byte[] bs = Utils.hexToBytes("0FF107"); - assertEquals(0, Utils.extractBits(bs, 5, 23)); // 4 bits beyond end, should be zero-extended - assertEquals(0, Utils.extractBits(bs, 0, 23)); // zero length of bits - assertEquals(0xFF, Utils.extractBits(bs, 8, 12)); // the ff part - assertEquals(1, Utils.extractBits(bs, 1, 0)); // lowest bit - assertEquals(2, Utils.extractBits(bs, 2, 7)); // pick out the 1 in second bit - } - - @Test - public void testExtractBitsNegative() { - byte[] bs = Utils.hexToBytes("80F107"); - assertEquals(0x1F, Utils.extractBits(bs, 5, 23)); // 4 bits beyond end, should be sign-extended - assertEquals(0x0F, Utils.extractBits(bs, 8, 12)); // the 0f part - assertEquals(0, Utils.extractBits(bs, 0, 8)); // zero length of bits - assertEquals(1, Utils.extractBits(bs, 1, 0)); // lowest bit - assertEquals(2, Utils.extractBits(bs, 2, 7)); // pick out the 1 in second bit - assertEquals(7, Utils.extractBits(bs, 3, 70)); // pick 3 bits beyond array - assertEquals(-1, Utils.extractBits(bs, 32, 70)); // pick 32 bits beyond array - } - - @Test - public void testSetBitsPositive() { - byte[] bs = Utils.hexToBytes("0ff107"); - Utils.setBits(bs, 4, 0, 9); // set lowest hex digit to 9 - assertEquals("0ff109", Utils.toHexString(bs)); - Utils.setBits(bs, 6, 13, 0); // set 6 bits in middle of ff to zero - assertEquals("081109", Utils.toHexString(bs)); - Utils.setBits(bs, 8, 23, 255); // set 8 bits to one starting from highest bit - assertEquals("881109", Utils.toHexString(bs)); - Utils.setBits(bs, 8, 23, 0); // set 8 bits to zero starting from highest bit - assertEquals("081109", Utils.toHexString(bs)); - Utils.setBits(bs, 8, 24, 0xFF); // set 8 bits to one beyond end of array - assertEquals("081109", Utils.toHexString(bs)); - Utils.setBits(bs, 24, 0, 0xFFFFFF); // set all 24 bits to 1 - assertEquals("ffffff", Utils.toHexString(bs)); - } - - @Test - public void testReadWriteInt() { - byte[] bs = new byte[8]; - assertEquals(0, Utils.readInt(bs, 0)); - int a = 0xcafebabe; - Utils.writeInt(bs, 0, a); - assertEquals(a, Utils.readInt(bs, 0)); - Utils.writeInt(bs, 4, a); - assertEquals(0xbabecafe, Utils.readInt(bs, 2)); - assertEquals(0xcafebabe, Utils.readInt(bs, 4)); - } - - @Test - public void testReadWriteLong() { - byte[] bs = new byte[20]; - assertEquals(0, Utils.readLong(bs, 0)); - long a = 0xffffffffcafebabeL; - Utils.writeLong(bs, 0, a); - assertEquals(a, Utils.readLong(bs, 0)); - Utils.writeLong(bs, 4, a); - assertEquals(0xffffffffffffffffL, Utils.readLong(bs, 0)); - assertEquals(0xcafebabe00000000L, Utils.readLong(bs, 8)); - } - - @Test - public void testCheckedCasts() { - assertEquals(-1, Utils.checkedInt(-1)); - } - - @Test - public void testToInt() { - assertEquals(1, Utils.toInt(1)); - assertEquals(7, Utils.toInt(7.0f)); - assertEquals(8, Utils.toInt("8")); - assertEquals(-1, Utils.toInt("-1")); - } - - @Test - public void testInetSocketAddress() { - String s = "http://www.something-unusual.com:18888"; - InetSocketAddress sa = Utils.toInetSocketAddress(s); - assertNotNull(sa); - - assertNotNull(Utils.toInetSocketAddress("localhost:8080")); - - InetSocketAddress sa1=Utils.toInetSocketAddress("12.13.14.15:8080"); - assertNotNull(sa1); - assertNotNull(Utils.toInetSocketAddress("http:12.13.14.15:8080")); - - assertNull(Utils.toInetSocketAddress("@@@")); - - } - - @Test - public void testBinarySearchLeftmost() { - AVector L = Vectors.of( - CVMLong.create(1), - CVMLong.create(2), - CVMLong.create(2), - CVMLong.create(3) - ); - - // No match. - assertNull(Utils.binarySearchLeftmost(L, Function.identity(), Comparator.comparingLong(CVMLong::longValue), CVMLong.create(0))); - - // Exact match. - assertEquals( - CVMLong.create(2), - Utils.binarySearchLeftmost(L, Function.identity(), Comparator.comparingLong(CVMLong::longValue), CVMLong.create(2)) - ); - - assertEquals( - CVMLong.create(3), - Utils.binarySearchLeftmost(L, Function.identity(), Comparator.comparingLong(CVMLong::longValue), CVMLong.create(3)) - ); - - // Approximate match: 3 is the leftmost element. - assertEquals( - CVMLong.create(3), - Utils.binarySearchLeftmost(L, Function.identity(), Comparator.comparingLong(CVMLong::longValue), CVMLong.create(1000)) - ); - } - - @Test - public void testBinarySearchLeftmost2() { - AVector> L = Vectors.of( - Vectors.of(1, 1), - Vectors.of(1, 2), - Vectors.of(2, 1), - Vectors.of(2, 2), - Vectors.of(2, 3), - Vectors.of(3, 1) - ); - - assertEquals( - Vectors.of(2, 1), - Utils.binarySearchLeftmost(L, a -> a.get(0), Comparator.comparingLong(CVMLong::longValue), CVMLong.create(2)) - ); - - assertEquals( - Vectors.of(1, 1), - Utils.binarySearchLeftmost(L, a -> a.get(0), Comparator.comparingLong(CVMLong::longValue), CVMLong.create(1)) - ); - - } - - @Test - public void testBinarySearchLeftmost3() { - assertNull(Utils.binarySearchLeftmost(Vectors.empty(), Function.identity(), Comparator.comparingLong(CVMLong::longValue), CVMLong.create(2))); - } - - @Test - public void testStatesAsOfRange() throws BadSignatureException { - Peer peer = Peer.create(InitTest.FIRST_PEER_KEYPAIR, TestState.STATE); - - AVector states = Vectors.of(STATE); - - for (int i = 0; i < 10; i++) { - State state0 = states.get(states.count() - 1); - - long timestamp = state0.getTimeStamp().longValue() + 100; - - String command = "(def x " + timestamp + ")"; - SignedData data = peer.sign(Invoke.create(InitTest.HERO, timestamp, command)); - - Block block = Block.of(timestamp, InitTest.FIRST_PEER_KEY, data); - - State state1 = state0.applyBlock(block).getState(); - - states = states.conj(state1); - } - - AVector statesInRange = Utils.statesAsOfRange(states, STATE.getTimeStamp(), 1000, 2); - - assertEquals(2, statesInRange.count()); - - // First State in range must be the INITIAL value. - assertEquals(STATE, statesInRange.get(0)); - - // Since each iteration creates a snapshot of State advances by 100 milliseconds, - // the last State's timestamp in the range is the same as the initial timestamp + 1000 milliseconds. - assertEquals( - CVMLong.create(STATE.getTimeStamp().longValue() + 1000), - statesInRange.get(statesInRange.count() - 1).getTimeStamp() - ); - } - -} diff --git a/convex-core/src/test/java/etch/api/TestEtch.java b/convex-core/src/test/java/etch/api/TestEtch.java deleted file mode 100644 index c5b1a91f9..000000000 --- a/convex-core/src/test/java/etch/api/TestEtch.java +++ /dev/null @@ -1,73 +0,0 @@ -package etch.api; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -import java.io.IOException; - -import org.junit.jupiter.api.Test; - -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.Hash; -import convex.core.data.Ref; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import etch.Etch; -import etch.EtchStore; - -public class TestEtch { - private static final int ITERATIONS = 3; - - @Test - public void testTempStore() throws IOException { - EtchStore store=EtchStore.createTemp(); - Etch etch = store.getEtch(); - - AVector v=Vectors.of(1,2,3); - Hash h = v.getHash(); - Ref r=v.getRef(); - - assertNull(etch.read(h)); - - // write the Ref - Ref r2=etch.write(h, r); - - assertEquals(v.getEncoding(), etch.read(h).getValue().getEncoding()); - - assertEquals(h,r2.getHash()); - } - - @Test - public void testRandomWritesStore() throws IOException, BadFormatException { - EtchStore store=EtchStore.createTemp(); - Etch etch = store.getEtch(); - - int COUNT = 1000; - for (int i = 0; i < COUNT; i++) { - Long a = (long) i; - AVector v=Vectors.of(a); - Hash key = v.getHash(); - - etch.write(key, v.getRef()); - - Ref r2 = etch.read(key); - assertEquals(v,r2.getValue()); - assertNotNull(r2, "Stored value not found for vector value: " + v); - } - - for (int ii = 0; ii < ITERATIONS; ii++) { - for (int i = 0; i < COUNT; i++) { - Long a = (long) i; - AVector v=Vectors.of(a); - Hash key = v.getHash(); - Ref r2 = etch.read(key); - - assertNotNull(r2, "Stored value not found for vector value: " + v); - assertEquals(v, r2.getValue()); - } - } - } -} diff --git a/convex-core/src/test/resources/contracts/box/test1.con b/convex-core/src/test/resources/contracts/box/test1.con deleted file mode 100644 index 1bf16dcff..000000000 --- a/convex-core/src/test/resources/contracts/box/test1.con +++ /dev/null @@ -1,33 +0,0 @@ -;; Assumed done in test setup already -;; (import convex.asset :as asset) -;; (import :as box) -;; (def box (get *aliases* 'box)) - -(def b1 (call box (create-box))) - -(assert (long? b1)) -(assert (= (asset/balance box *address*) #{b1})) -(assert (asset/owns? *address* [box #{b1}])) - -;; burn the box b1 -(call box (burn #{b1})) -(assert (not (asset/owns? *address* [box #{b1}]))) -(assert (= (asset/balance box *address*) #{})) - - -;; use a zombie actor -(def zombie (deploy `(do - (import convex.asset :as asset) - (def box ~box) - (set-controller *caller*) - (defn receive-asset ^{:callable? true} [a data] (asset/accept *caller* a))))) - -(def b2 (call box (create-box))) - -(assert (= #{b2} (asset/balance box *address*))) -(asset/transfer zombie [box #{b2}]) - -(assert (= #{b2} (asset/balance box zombie))) - -(eval-as zombie `(asset/transfer *caller* [box (asset/balance box *address*)])) -(assert (= #{b2} (asset/balance box *address*))) diff --git a/convex-core/src/test/resources/contracts/deposit-box.con b/convex-core/src/test/resources/contracts/deposit-box.con deleted file mode 100644 index 8d86a8a6d..000000000 --- a/convex-core/src/test/resources/contracts/deposit-box.con +++ /dev/null @@ -1,19 +0,0 @@ -;; A mostly stateless smart contract that allows deposits from any account -;; and a full withdrawal by anyone. A public charity box. -;; -(fn [] - - ;; Deposit function accepts any offer. - ;; - (defn deposit - ^{:callable? true} - [] - (accept *offer*)) - - - ;; Withdraw function sends the caller the complete balance. - ;; - (defn withdraw - ^{:callable? true} - [] - (transfer *caller* *balance*))) diff --git a/convex-core/src/test/resources/contracts/exceptional.con b/convex-core/src/test/resources/contracts/exceptional.con deleted file mode 100644 index c52bff489..000000000 --- a/convex-core/src/test/resources/contracts/exceptional.con +++ /dev/null @@ -1,30 +0,0 @@ -(do ;; Test contract for state changes and rollback. - - (def fragile :ok) - - (defn halt-fn - ^{:callable? true} - [x] - (halt x) - (return :foo) - :bar) - - (defn rollback-fn - ^{:callable? true} - [x] - (def fragile :broken) - (rollback x) - :bar) - - (defn break-fn - ^{:callable? true} - [x] - (def fragile :broken) - x) - - (defn get-fragile - ^{:callable? true} - [] - (return fragile)) - - ) diff --git a/convex-core/src/test/resources/contracts/funding.con b/convex-core/src/test/resources/contracts/funding.con deleted file mode 100644 index ca1e1cfe2..000000000 --- a/convex-core/src/test/resources/contracts/funding.con +++ /dev/null @@ -1,54 +0,0 @@ -(do ;; Testing contract for fund transfers via offer / accept - - - ;; function that accepts quarter of all funds offered - (defn accept-quarter - ^{:callable? true} - [] - (accept (long (* 0.25 *offer*)))) - - ;; function that accepts half all funds offered - (defn accept-all - ^{:callable? true} - [] - (accept *offer*)) - - ;; function that accepts funds then rolls back - (defn accept-rollback - ^{:callable? true} - [] - (accept *offer*) - (rollback :foo)) - - ;; function that accepts funds repeatedly - ;; Note: *offer* should be reduced to zero by first accept. - (defn accept-repeat - ^{:callable? true} - [] - (accept *offer*) - (assert (== 0 *offer*)) - (accept *offer*) - (accept *offer*) - *offer*) - - ;; function that accepts nothing - (defn accept-zero - ^{:callable? true} - [] - (accept 0)) - - ;; function that accepts offer, and forwards to self - (defn accept-forward - ^{:callable? true} - [] - (let [amt *offer*] - (accept amt) - (call *address* amt (accept-all)))) - - ;; function that accepts nothing, but returns the offered value - (defn echo-offer - ^{:callable? true} - [] - *offer*) - - ) diff --git a/convex-core/src/test/resources/contracts/hello.con b/convex-core/src/test/resources/contracts/hello.con deleted file mode 100644 index 3a28f9c69..000000000 --- a/convex-core/src/test/resources/contracts/hello.con +++ /dev/null @@ -1,16 +0,0 @@ -;; The Hello World of smart contracts -(do - - (def people #{}) - - (defn greet - ^{:callable? true} - [name] - (if (people name) - (str "Welcome back " name) - (do - (def people (conj people name)) - (str "Hello " name) - ))) - - ) diff --git a/convex-core/src/test/resources/contracts/nft/simple-nft-test.con b/convex-core/src/test/resources/contracts/nft/simple-nft-test.con deleted file mode 100644 index f75c92f35..000000000 --- a/convex-core/src/test/resources/contracts/nft/simple-nft-test.con +++ /dev/null @@ -1,21 +0,0 @@ -;; Assumed done in test setup already -;; (import convex.asset :as asset) -;; (def nft (import convex.simple-nft :as nft)) - -;; Testing with one account -(do - (def n1 (call nft (create))) - (assert (long? n1)) - (assert (contains-key? (asset/balance nft *address*) n1))) - -;; Testing quantities -(do - (assert (= #{} (asset/quantity-zero nft))) - (assert (= #{1 2 3 4} (asset/quantity-add nft #{1 2} #{3 4}))) - (assert (= #{1 2 3 4} (asset/quantity-add nft #{1 2 3} #{2 3 4}))) - (assert (= #{1 2} (asset/quantity-sub nft #{1 2 3} #{3 4 5}))) - - (assert (asset/quantity-contains? nft #{1 2 3} #{2 3})) - (assert (not (asset/quantity-contains? nft #{1 2 3} #{3 4}))) - (assert (not (asset/quantity-contains? nft #{1 2 3} #{4 5 6}))) - ) diff --git a/convex-core/src/test/resources/contracts/token.con b/convex-core/src/test/resources/contracts/token.con deleted file mode 100644 index a480220ef..000000000 --- a/convex-core/src/test/resources/contracts/token.con +++ /dev/null @@ -1,42 +0,0 @@ -;; Example token implementation -(fn [seed ;; a random number - supply ;; total token supply for smart contract - owner ;; initial owner of all the tokens - ] - (assert (address? owner) (long? supply) (> supply 0)) - `(do - ;; assets to set up the contract safely - - - ;; initial balances - (def balances {~owner ~supply}) - - ;;transfer function - (defn transfer - ^{:callable? true} - [target amount] - (assert (address? target) (long? amount) (<= 0 amount (balances *caller*))) - (if (not (= target *caller*)) - (let [srcbal (balances *caller*) - dstbal (balances target) - newbal (if dstbal (+ dstbal amount) amount)] - (def balances (assoc balances - *caller* (- srcbal amount) ;; new balance for caller - target newbal ;; new balance for target - ))))) - - ;; a function that should never be called, not exported - (defn bad-function [] (def balances {})) - - ;;return total supply - (defn total-supply - ^{:callable? true} - [] - ~supply) - - ;; get balance, or 0 if no balance for specified address - (defn balance - ^{:callable? true} - [acct] - (let [b (balances acct)] (if b b 0))) - )) diff --git a/convex-core/src/test/resources/examples/adventure.cvx b/convex-core/src/test/resources/examples/adventure.cvx deleted file mode 100644 index 4bb933239..000000000 --- a/convex-core/src/test/resources/examples/adventure.cvx +++ /dev/null @@ -1,21 +0,0 @@ -(do - - (defn runner [form] - (let [cmds (cond (list? form) - (let [n (count form)] - (cond (== n 0) [] - (= 'do ) (vec (next form)) - (vec form))) - [form])] - (cond - (empty? cmds) "You do nothing." - (= ['quit] cmds) (do (undef *lang*) (return "Exiting game... goodbye!"))) - (reduce (fn [s c] (str s " " c)) "You don't know how to:" cmds))) - - (defn start - ^{:doc {:description "Start the adventure!"}} - [] - (do - (def *lang* runner) "Welcome to the Adventure!")) - - ) \ No newline at end of file diff --git a/convex-core/src/test/resources/junit-platform.properties b/convex-core/src/test/resources/junit-platform.properties deleted file mode 100644 index 77ace3bc2..000000000 --- a/convex-core/src/test/resources/junit-platform.properties +++ /dev/null @@ -1,4 +0,0 @@ -junit.jupiter.execution.parallel.enabled=false -junit.jupiter.execution.parallel.mode.default = concurrent -junit.jupiter.execution.parallel.mode.classes.default = concurrent -junit.jupiter.execution.parallel.config.dynamic.factor = 2 diff --git a/convex-core/src/test/resources/testsource/min.con b/convex-core/src/test/resources/testsource/min.con deleted file mode 100644 index b8dcc281c..000000000 --- a/convex-core/src/test/resources/testsource/min.con +++ /dev/null @@ -1,8 +0,0 @@ -(defn min [& vals] - (let [fst (first vals) - n (count vals)] - (loop [min fst i 1] - (if (>= i n) - min - (let [v (nth vals i)] - (recur (if (< v min) v min) (inc i))))))) \ No newline at end of file diff --git a/convex-gui/.gitignore b/convex-gui/.gitignore deleted file mode 100644 index e661051a1..000000000 --- a/convex-gui/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -/target/ -/.settings/ -/.classpath -/.project -/pom.xml.versionsBackup diff --git a/convex-gui/pom.xml b/convex-gui/pom.xml deleted file mode 100644 index 1adc33041..000000000 --- a/convex-gui/pom.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - world.convex - convex - 0.7.0-rc3 - - 4.0.0 - - convex-gui - - Convex GUI - Convex desktop GUI and test applications - https://convex.world - - - - - maven-assembly-plugin - 3.3.0 - - ${project.directory} - - - convex.gui.manager.PeerGUI - - - - jar-with-dependencies - - - - - - create-archive - package - - single - - - - - - - - - - world.convex - convex-peer - ${convex.version} - - - - net.java.dev.jna - jna - 5.8.0 - - - - org.apache.commons - commons-text - 1.9 - - - - org.junit.jupiter - junit-jupiter-engine - ${junit.version} - test - - - - org.junit.jupiter - junit-jupiter-params - ${junit.version} - test - - - - io.github.vincenzopalazzo - material-ui-swing - 1.1.2 - - - ch.qos.logback - logback-classic - - - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.slf4j - slf4j-simple - ${slf4j.version} - - - - diff --git a/convex-gui/src/main/assembly/full.xml b/convex-gui/src/main/assembly/full.xml deleted file mode 100644 index e8be999d2..000000000 --- a/convex-gui/src/main/assembly/full.xml +++ /dev/null @@ -1,34 +0,0 @@ - - full - - jar - - - - ${project.basedir} - / - - README* - LICENSE* - NOTICE* - - - - ${project.build.directory} - / - - *.jar - - - - - - / - true - true - true - test - - - \ No newline at end of file diff --git a/convex-gui/src/main/assembly/testing.xml b/convex-gui/src/main/assembly/testing.xml deleted file mode 100644 index cf7092fbb..000000000 --- a/convex-gui/src/main/assembly/testing.xml +++ /dev/null @@ -1,30 +0,0 @@ - - testing - - jar - - false - - - / - true - true - true - test - - - - - - ${project.build.directory}/test-classes - / - - **/*.class - - true - - - \ No newline at end of file diff --git a/convex-gui/src/main/java/convex/gui/client/ConvexClient.java b/convex-gui/src/main/java/convex/gui/client/ConvexClient.java deleted file mode 100644 index 3dc04286e..000000000 --- a/convex-gui/src/main/java/convex/gui/client/ConvexClient.java +++ /dev/null @@ -1,116 +0,0 @@ -package convex.gui.client; - -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.EventQueue; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -import javax.swing.JFrame; -import javax.swing.JPanel; -import javax.swing.JTabbedPane; - -import convex.api.Convex; -import convex.core.State; -import convex.core.store.AStore; -import convex.core.store.Stores; -import convex.gui.client.panels.HomePanel; -import convex.gui.components.models.StateModel; -import convex.gui.manager.mainpanels.AboutPanel; -import convex.gui.utils.Toolkit; - -/** - * A Client application for the Convex Network. - * - * Doesn't run a Peer. Connects to convex.world. - */ -@SuppressWarnings("serial") -public class ConvexClient extends JPanel { - - private static final Logger log = LoggerFactory.getLogger(ConvexClient.class.getName()); - - public static final AStore CLIENT_STORE = Stores.getGlobalStore(); - - private static JFrame frame; - - private static StateModel latestState = StateModel.create(null); - - public static long maxBlock = 0; - - protected Convex convex=null; - - /** - * Launch the application. - * @param args Command line argument - */ - public static void main(String[] args) { - log.info("Running Convex Client"); - // call to set up Look and Feel - Toolkit.init(); - - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - try { - ConvexClient.frame = new JFrame(); - frame.setTitle("Convex Client"); - frame.setIconImage(Toolkit.getImage(ConvexClient.class.getResource("/images/Convex.png"))); - frame.setBounds(100, 100, 1024, 768); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - ConvexClient window = new ConvexClient(); - frame.getContentPane().add(window, BorderLayout.CENTER); - frame.pack(); - frame.setVisible(true); - } catch (Exception e) { - e.printStackTrace(); - } - } - }); - } - - /* - * Main component panel - */ - JPanel panel = new JPanel(); - - HomePanel homePanel = new HomePanel(); - AboutPanel aboutPanel = new AboutPanel(); - JTabbedPane tabs = new JTabbedPane(); - JPanel mainPanel = new JPanel(); - - /** - * Create the application. - */ - public ConvexClient() { - setLayout(new BorderLayout()); - this.add(tabs, BorderLayout.CENTER); - - tabs.add("Home", homePanel); - tabs.add("About", aboutPanel); - - } - - public void switchPanel(String title) { - int n = tabs.getTabCount(); - for (int i = 0; i < n; i++) { - if (tabs.getTitleAt(i).contentEquals(title)) { - tabs.setSelectedIndex(i); - return; - } - } - System.err.println("Missing tab: " + title); - } - - public static State getLatestState() { - return latestState.getValue(); - } - - public static Component getFrame() { - return frame; - } - - public static StateModel getStateModel() { - return latestState; - } -} diff --git a/convex-gui/src/main/java/convex/gui/client/panels/HomePanel.java b/convex-gui/src/main/java/convex/gui/client/panels/HomePanel.java deleted file mode 100644 index 9f72e8b86..000000000 --- a/convex-gui/src/main/java/convex/gui/client/panels/HomePanel.java +++ /dev/null @@ -1,41 +0,0 @@ -package convex.gui.client.panels; - -import javax.swing.JPanel; -import java.awt.BorderLayout; -import java.awt.Dimension; - -import javax.swing.JLabel; -import javax.swing.SwingConstants; - -import convex.gui.components.WorldPanel; - -import java.awt.Font; - -@SuppressWarnings("serial") -public class HomePanel extends JPanel { - - - /** - * Create the panel. - */ - public HomePanel() { - setPreferredSize(new Dimension(800,600)); - setLayout(new BorderLayout(0, 0)); - - JPanel panel = new JPanel(); - add(panel); - panel.setLayout(new BorderLayout(0, 0)); - - JLabel lblWelome = new JLabel("Welcome to Convex"); - lblWelome.setFont(new Font("Monospaced", Font.PLAIN, 18)); - lblWelome.setHorizontalAlignment(SwingConstants.CENTER); - panel.add(lblWelome, BorderLayout.NORTH); - - panel.add(new WorldPanel(), BorderLayout.CENTER); - - JLabel lblConn = new JLabel("Connecting.."); - panel.add(lblConn, BorderLayout.SOUTH); - - } - -} diff --git a/convex-gui/src/main/java/convex/gui/components/AccountChooserPanel.java b/convex-gui/src/main/java/convex/gui/components/AccountChooserPanel.java deleted file mode 100644 index 1caf0b6df..000000000 --- a/convex-gui/src/main/java/convex/gui/components/AccountChooserPanel.java +++ /dev/null @@ -1,102 +0,0 @@ -package convex.gui.components; - -import java.awt.FlowLayout; - -import javax.swing.ComboBoxModel; -import javax.swing.DefaultComboBoxModel; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.ListModel; - -import convex.core.State; -import convex.core.crypto.WalletEntry; -import convex.core.data.Address; -import convex.core.util.Text; -import convex.gui.manager.PeerGUI; -import convex.gui.manager.mainpanels.WalletPanel; - -/** - * Panel allowing the selection of account and query mode - */ -@SuppressWarnings("serial") -public class AccountChooserPanel extends JPanel { - - private JComboBox modeCombo; - public JComboBox addressCombo; - private JLabel lblNewLabel_1; - private JLabel lblNewLabel; - - private ComboBoxModel addressModel = createAddressList(WalletPanel.getListModel()); - private JLabel balanceLabel; - - public AccountChooserPanel() { - FlowLayout flowLayout = new FlowLayout(); - flowLayout.setAlignment(FlowLayout.LEFT); - setLayout(flowLayout); - - modeCombo = new JComboBox(); - modeCombo.setToolTipText("Use Transact to execute transactions (uses cash).\n\n" - + "Use Query to compute results without changing on-chain state (free)."); - modeCombo.addItem("Transact"); - modeCombo.addItem("Query"); - - lblNewLabel_1 = new JLabel("Mode:"); - add(lblNewLabel_1); - add(modeCombo); - - lblNewLabel = new JLabel("Account:"); - add(lblNewLabel); - - addressCombo = new JComboBox(); - addressCombo.setEditable(false); - add(addressCombo); - addressCombo.setModel(addressModel); - - balanceLabel = new JLabel("Balance: "); - add(balanceLabel); - - PeerGUI.getStateModel().addPropertyChangeListener(pc -> { - updateBalance((State) pc.getNewValue(), getSelectedAddress()); - }); - - addressCombo.addItemListener(e -> { - updateBalance(PeerGUI.getLatestState(), getSelectedAddress()); - }); - - updateBalance(PeerGUI.getLatestState(), getSelectedAddress()); - } - - public Address getSelectedAddress() { - WalletEntry we = (WalletEntry) addressModel.getSelectedItem(); - return (we == null) ? null : we.getAddress(); - } - - private ComboBoxModel createAddressList(ListModel m) { - int n = m.getSize(); - DefaultComboBoxModel cm = new DefaultComboBoxModel(); - for (int i = 0; i < n; i++) { - WalletEntry we = m.getElementAt(i); - cm.addElement(we); - } - cm.addElement(null); - return cm; - } - - private void updateBalance(State s, Address a) { - if ((s == null) || (a == null)) { - balanceLabel.setText("Balance: "); - } else { - Long amt= s.getBalance(a); - balanceLabel.setText("Balance: " + ((amt==null)?"Null":Text.toFriendlyBalance(amt))); - } - } - - public String getMode() { - return (String) modeCombo.getSelectedItem(); - } - - public WalletEntry getWalletEntry() { - return (WalletEntry) addressCombo.getSelectedItem(); - } -} diff --git a/convex-gui/src/main/java/convex/gui/components/ActionPanel.java b/convex-gui/src/main/java/convex/gui/components/ActionPanel.java deleted file mode 100644 index 84420afcb..000000000 --- a/convex-gui/src/main/java/convex/gui/components/ActionPanel.java +++ /dev/null @@ -1,23 +0,0 @@ -package convex.gui.components; - -import java.awt.FlowLayout; - -import javax.swing.JPanel; -import javax.swing.border.BevelBorder; - -/** - * A panel used for displaying a list of action buttons at the bottom of the - * screen. - */ -@SuppressWarnings("serial") -public class ActionPanel extends JPanel { - - public ActionPanel() { - super(); - - FlowLayout flowLayout = new FlowLayout(FlowLayout.LEFT); - setLayout(flowLayout); - - setBorder(new BevelBorder(BevelBorder.RAISED, null, null, null, null)); - } -} diff --git a/convex-gui/src/main/java/convex/gui/components/BaseListComponent.java b/convex-gui/src/main/java/convex/gui/components/BaseListComponent.java deleted file mode 100644 index e653e3863..000000000 --- a/convex-gui/src/main/java/convex/gui/components/BaseListComponent.java +++ /dev/null @@ -1,16 +0,0 @@ -package convex.gui.components; - -import javax.swing.JPanel; -import javax.swing.border.BevelBorder; -import javax.swing.border.CompoundBorder; -import javax.swing.border.EmptyBorder; - -@SuppressWarnings("serial") -public class BaseListComponent extends JPanel { - - public BaseListComponent() { - setBorder(new CompoundBorder(new BevelBorder(BevelBorder.RAISED, null, null, null, null), - new EmptyBorder(2, 2, 2, 2))); - - } -} diff --git a/convex-gui/src/main/java/convex/gui/components/BlockViewComponent.java b/convex-gui/src/main/java/convex/gui/components/BlockViewComponent.java deleted file mode 100644 index 5b99d15c2..000000000 --- a/convex-gui/src/main/java/convex/gui/components/BlockViewComponent.java +++ /dev/null @@ -1,82 +0,0 @@ -package convex.gui.components; - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Graphics; - -import javax.swing.JPanel; - -import convex.core.Block; -import convex.core.Order; -import convex.core.Peer; -import convex.core.State; -import convex.core.data.AVector; -import convex.core.data.Hash; -import convex.gui.components.models.StateModel; -import convex.gui.manager.PeerGUI; - -/** - * Panel presenting a summary graphic of the most recent blocks for a given - * PeerView - */ -@SuppressWarnings("serial") -public class BlockViewComponent extends JPanel { - - private PeerView peerView; - - public BlockViewComponent(PeerView peer) { - this.peerView = peer; - - setBackground(null); - setPreferredSize(new Dimension(1000, 10)); - - if (peer!=null) { - StateModel model=peer.getStateModel(); - model.addPropertyChangeListener(e -> { - repaint(); - }); - } - } - - @Override - public void paintComponent(Graphics g) { - g.setColor(Color.black); - int pw = getWidth(); - int ph = getHeight(); - g.fillRect(0, 0, pw, ph); - - Peer p = peerView.peerServer.getPeer(); - Order order = p.getPeerOrder(); - if (order==null) return; // no current peer order - maybe not a valid peer? - AVector blocks = order.getBlocks(); - int n = (int) blocks.count(); - - - int W = 10; - long tw = W * PeerGUI.maxBlock; - long offset = Math.max(0, tw - pw); - - for (int i = (int) (offset / W); i < n; i++) { - Color c = Color.orange; - if (i < order.getProposalPoint()) c = Color.yellow; - if (i < order.getConsensusPoint()) c = Color.green; - if (p.getConsensusPoint() != order.getConsensusPoint()) { - System.out.println("Strange consensus?"); - } - int x = (int) (W * i - offset); - g.setColor(c); - g.fillRect(x + 1, 1, W - 2, W - 2); - - if (c == Color.green) { - g.setColor(Color.black); - State s = p.getStates().get(i + 1); - for (int j = 0; j < 6; j++) { - Hash h = s.getHash(); - if (h.byteAt(j) < 0) { - g.fillRect(x + 2, 2 + j, 6, 1); - } - } - } - } - } -} diff --git a/convex-gui/src/main/java/convex/gui/components/CodeLabel.java b/convex-gui/src/main/java/convex/gui/components/CodeLabel.java deleted file mode 100644 index d3d34a6ee..000000000 --- a/convex-gui/src/main/java/convex/gui/components/CodeLabel.java +++ /dev/null @@ -1,16 +0,0 @@ -package convex.gui.components; - -import javax.swing.JTextArea; - -import convex.gui.utils.Toolkit; - -@SuppressWarnings("serial") -public class CodeLabel extends JTextArea { - - public CodeLabel(String text) { - this.setText(text); - this.setBackground(null); - this.setEditable(false); - this.setFont(Toolkit.SMALL_MONO_FONT); - } -} diff --git a/convex-gui/src/main/java/convex/gui/components/DefaultReceiveAction.java b/convex-gui/src/main/java/convex/gui/components/DefaultReceiveAction.java deleted file mode 100644 index 6b3ef374e..000000000 --- a/convex-gui/src/main/java/convex/gui/components/DefaultReceiveAction.java +++ /dev/null @@ -1,51 +0,0 @@ -package convex.gui.components; - -import java.util.function.Consumer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.JComponent; - -import convex.core.Result; -import convex.core.lang.RT; -import convex.core.util.Utils; - -public class DefaultReceiveAction implements Consumer { - - public static final Logger log = LoggerFactory.getLogger(DefaultReceiveAction.class.getName()); - - private JComponent parent; - - public DefaultReceiveAction(JComponent parent) { - this.parent = parent; - } - - @Override - public void accept(Result t) { - if (t.isError()) { - handleError(RT.jvm(t.getID()),t.getErrorCode(),t.getValue()); - } else { - handleResult(t.getValue()); - } - } - - protected void handleResult(Object m) { - showResult(m); - } - - protected void handleError(long id, Object code, Object msg) { - showError(code,msg); - } - - private void showError(Object code, Object msg) { - String resultString = "Error executing transaction: " + code + " "+msg; - log.info(resultString); - Toast.display(parent, resultString, Toast.FAIL); - } - - private void showResult(Object v) { - String resultString = "Transaction executed successfully\n" + "Result: " + Utils.toString(v); - log.info(resultString); - Toast.display(parent, resultString, Toast.SUCCESS); - } -} diff --git a/convex-gui/src/main/java/convex/gui/components/DropdownMenu.java b/convex-gui/src/main/java/convex/gui/components/DropdownMenu.java deleted file mode 100644 index bc8756ea5..000000000 --- a/convex-gui/src/main/java/convex/gui/components/DropdownMenu.java +++ /dev/null @@ -1,30 +0,0 @@ -package convex.gui.components; - -import javax.swing.JButton; -import javax.swing.JPopupMenu; - -import convex.gui.utils.Toolkit; - -/** - * A dropdown menu that can be used wherever an embedded menu is needed. - */ -@SuppressWarnings("serial") -public class DropdownMenu extends JButton { - - private JPopupMenu popupMenu; - - public DropdownMenu(JPopupMenu popupMenu) { - - super(); - this.popupMenu = popupMenu; - this.setBorder(null); - this.setIcon(Toolkit.COG); - this.addActionListener(e -> { - popupMenu.show(this, 0, this.getHeight()); - }); - } - - public JPopupMenu getMenu() { - return popupMenu; - } -} diff --git a/convex-gui/src/main/java/convex/gui/components/Identicon.java b/convex-gui/src/main/java/convex/gui/components/Identicon.java deleted file mode 100644 index ecbea0502..000000000 --- a/convex-gui/src/main/java/convex/gui/components/Identicon.java +++ /dev/null @@ -1,42 +0,0 @@ -package convex.gui.components; - -import java.awt.image.BufferedImage; - -import javax.swing.ImageIcon; -import javax.swing.JLabel; -import javax.swing.border.BevelBorder; - -import convex.core.data.ABlob; -import convex.core.util.Utils; -import convex.gui.utils.Toolkit; - -/** - * A simple identicon for visualising hash values. - */ -@SuppressWarnings("serial") -public class Identicon extends JLabel { - - public static BufferedImage createImage(ABlob data, int renderSize) { - int SIZE = 3; - byte[] bs = data.getBytes(); - - BufferedImage bi = new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_INT_RGB); - - for (int y = 0; y < SIZE; y++) { - for (int x = 0; x < SIZE; x++) { - int i = x + y * SIZE; - int bits = Utils.extractBits(bs, 9, 9 * i); // take 3 bits per channel - int rgb = ((bits & 0b111000000) << 15) + ((bits & 0b111000) << 10) + ((bits & 0b111) << 5); - bi.setRGB(x, y, rgb); - } - } - - return Toolkit.smoothResize(bi, renderSize, renderSize); - } - - public Identicon(ABlob a) { - super(new ImageIcon(Identicon.createImage(a, 36))); - - setBorder(new BevelBorder(BevelBorder.RAISED, null, null, null, null)); - } -} diff --git a/convex-gui/src/main/java/convex/gui/components/PeerComponent.java b/convex-gui/src/main/java/convex/gui/components/PeerComponent.java deleted file mode 100644 index b1ca24527..000000000 --- a/convex-gui/src/main/java/convex/gui/components/PeerComponent.java +++ /dev/null @@ -1,139 +0,0 @@ -package convex.gui.components; - -import java.awt.BorderLayout; - -import javax.swing.JButton; -import javax.swing.JMenuItem; -import javax.swing.JPanel; -import javax.swing.JPopupMenu; -import javax.swing.JTextArea; -import javax.swing.border.EmptyBorder; - -import convex.core.Peer; -import convex.core.data.ACell; -import convex.gui.components.models.StateModel; -import convex.gui.manager.PeerGUI; -import convex.gui.manager.windows.etch.EtchWindow; -import convex.gui.manager.windows.peer.PeerWindow; -import convex.gui.manager.windows.state.StateWindow; -import convex.gui.utils.Toolkit; -import convex.peer.Server; -import etch.EtchStore; - -@SuppressWarnings("serial") -public class PeerComponent extends BaseListComponent { - - public PeerView peer; - JTextArea description; - private PeerGUI manager; - - public void launchPeerWindow(PeerView peer) { - PeerWindow pw = new PeerWindow(manager, peer); - pw.launch(); - } - - public void launchEtchWindow(PeerView peer) { - EtchWindow ew = new EtchWindow(manager, peer); - ew.launch(); - } - - public void launchExploreWindow(PeerView peer) { - Server s = peer.peerServer; - ACell p = s.getPeer().getConsensusState(); - StateWindow pw = new StateWindow(manager, p); - pw.launch(); - } - - public PeerComponent(PeerGUI manager, PeerView value) { - this.manager = manager; - this.peer = value; - - setLayout(new BorderLayout(0, 0)); - - // setPreferredSize(new Dimension(1000, 90)); - - JButton button = new JButton(""); - button.setBorder(null); - add(button, BorderLayout.WEST); - button.setIcon(Toolkit.CONVEX); - button.addActionListener(e -> { - launchPeerWindow(this.peer); - }); - - JPanel panel = new JPanel(); - panel.setBorder(new EmptyBorder(5, 5, 5, 5)); - add(panel); - panel.setLayout(new BorderLayout(0, 0)); - - description = new JTextArea((peer == null) ? "No peer" : peer.toString()); - description.setFont(Toolkit.SMALL_MONO_FONT); - description.setEditable(false); - description.setBorder(null); - description.setBackground(null); - panel.add(description, BorderLayout.CENTER); - - // Setup popup menu for peer - JPopupMenu popupMenu = new JPopupMenu(); - if (peer.isLocal()) { - JMenuItem closeButton = new JMenuItem("Shutdown Peer"); - closeButton.addActionListener(e -> { - peer.close(); - }); - popupMenu.add(closeButton); - - JMenuItem exploreButton = new JMenuItem("Explore state"); - exploreButton.addActionListener(e -> { - launchExploreWindow(peer); - }); - popupMenu.add(exploreButton); - - if (peer.peerServer.getStore() instanceof EtchStore) { - JMenuItem storeButton = new JMenuItem("Explore Etch store"); - storeButton.addActionListener(e -> { - launchEtchWindow(peer); - }); - popupMenu.add(storeButton); - } - - - JMenuItem killConn = new JMenuItem("Kill Connections"); - killConn.addActionListener(e -> { - peer.peerServer.getConnectionManager().closeAllConnections(); - }); - popupMenu.add(killConn); - - } else { - JMenuItem closeButton = new JMenuItem("Close connection"); - closeButton.addActionListener(e -> { - peer.close(); - }); - popupMenu.add(closeButton); - } - - JMenuItem replButton = new JMenuItem("Launch REPL"); - replButton.addActionListener(e -> launchPeerWindow(this.peer)); - - popupMenu.add(replButton); - - JPanel blockView = new BlockViewComponent(peer); - add(blockView, BorderLayout.SOUTH); - - DropdownMenu dm = new DropdownMenu(popupMenu); - add(dm, BorderLayout.EAST); - - if (peer!=null) { - StateModel model=peer.peerModel; - if (model!=null) { - model.addPropertyChangeListener(e->{ - blockView.repaint(); - description.setText(peer.toString()); - }); - } - } - - PeerGUI.tickState.addPropertyChangeListener(e->{ - description.setText(peer.toString()); - }); - - } -} diff --git a/convex-gui/src/main/java/convex/gui/components/PeerView.java b/convex-gui/src/main/java/convex/gui/components/PeerView.java deleted file mode 100644 index 17f930173..000000000 --- a/convex-gui/src/main/java/convex/gui/components/PeerView.java +++ /dev/null @@ -1,92 +0,0 @@ -package convex.gui.components; - -import java.net.InetSocketAddress; - -import convex.api.Convex; -import convex.core.Peer; -import convex.core.State; -import convex.core.data.AccountKey; -import convex.core.data.PeerStatus; -import convex.core.util.Text; -import convex.gui.components.models.StateModel; -import convex.gui.manager.PeerGUI; -import convex.peer.ConnectionManager; -import convex.peer.Server; - -/** - * Class representing a lightweight view of a Peer. - * - * Peer may be either a local Server or remote. - */ -public class PeerView { - public Convex peerConnection = null; - public Server peerServer = null; - - public StateModel peerModel = new StateModel<>(null); - public StateModel stateModel = new StateModel<>(null); - - @Override - public String toString() { - StringBuilder sb=new StringBuilder(); - if (peerServer != null) { - State state=PeerGUI.getLatestState(); - AccountKey paddr=peerServer.getPeerKey(); - sb.append("0x"+paddr.toChecksumHex()+"\n"); - sb.append("Local peer on: " + peerServer.getHostAddress() + " with store "+peerServer.getStore()+"\n"); - - PeerStatus ps=state.getPeer(paddr); - if (ps!=null) { - sb.append("Peer Stake: "+Text.toFriendlyBalance(ps.getPeerStake())); - sb.append(" "); - sb.append("Delegated Stake: "+Text.toFriendlyBalance(ps.getDelegatedStake())); - } - ConnectionManager cm=peerServer.getConnectionManager(); - sb.append("\n"); - sb.append("Connections: "+cm.getConnectionCount()); - } else if (peerConnection != null) { - sb.append("Remote peer at: " + peerConnection.getRemoteAddress()+"\n"); - } else { - sb.append("Unknown"); - } - return sb.toString(); - } - - /** - * Poll the current peer state. Updates state models if necessary. - * - * Returns null if not a local peer. - * - * @return Peer state for this PeerView - */ - public Peer checkPeer() { - if (peerServer != null) { - Peer p = peerServer.getPeer(); - peerModel.setValue(p); - if (p!=null) stateModel.setValue(p.getConsensusState()); - return p; - } - return null; - } - - public void close() { - if (peerServer != null) peerServer.close(); - if (peerConnection != null) peerConnection.close(); - } - - public InetSocketAddress getHostAddress() { - // this is direct connection to a peer, so get its host address - if (peerServer != null) return peerServer.getHostAddress(); - - // need to get the remote address from the PeerConnection - return (InetSocketAddress) peerConnection.getRemoteAddress(); - } - - public boolean isLocal() { - return peerServer != null; - } - - public StateModel getStateModel() { - return stateModel; - } - -} \ No newline at end of file diff --git a/convex-gui/src/main/java/convex/gui/components/ScrollyList.java b/convex-gui/src/main/java/convex/gui/components/ScrollyList.java deleted file mode 100644 index 7187779ab..000000000 --- a/convex-gui/src/main/java/convex/gui/components/ScrollyList.java +++ /dev/null @@ -1,96 +0,0 @@ -package convex.gui.components; - -import java.awt.Component; -import java.awt.Dimension; -import java.awt.GridLayout; -import java.awt.Rectangle; -import java.util.function.Function; - -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.ListModel; -import javax.swing.Scrollable; -import javax.swing.event.ListDataEvent; -import javax.swing.event.ListDataListener; - -/** - * Component that represents a convenient Scrollable list of child components, - * based on a List model. - * - * @param Type of list model elements - */ -@SuppressWarnings("serial") -public class ScrollyList extends JScrollPane { - private final Function builder; - private final ListModel model; - private final ScrollablePanel listPanel = new ScrollablePanel(); - - private void refreshList() { - listPanel.removeAll(); - int n = model.getSize(); - for (int i = 0; i < n; i++) { - E we = model.getElementAt(i); - listPanel.add(builder.apply(we)); - } - this.revalidate(); - } - - private static class ScrollablePanel extends JPanel implements Scrollable { - - @Override - public Dimension getPreferredScrollableViewportSize() { - return new Dimension(800, 600); - } - - @Override - public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { - return 60; - } - - @Override - public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { - // TODO Auto-generated method stub - return 180; - } - - @Override - public boolean getScrollableTracksViewportWidth() { - return true; - } - - @Override - public boolean getScrollableTracksViewportHeight() { - return false; - } - } - - public ScrollyList(ListModel model, Function builder) { - super(); - this.builder = builder; - this.model = model; - this.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - - listPanel.setLayout(new GridLayout(0, 1)); - setViewportView(listPanel); - getViewport().setBackground(null); - - model.addListDataListener(new ListDataListener() { - @Override - public void intervalAdded(ListDataEvent e) { - refreshList(); - } - - @Override - public void intervalRemoved(ListDataEvent e) { - refreshList(); - } - - @Override - public void contentsChanged(ListDataEvent e) { - refreshList(); - } - }); - - refreshList(); - } -} diff --git a/convex-gui/src/main/java/convex/gui/components/Toast.java b/convex-gui/src/main/java/convex/gui/components/Toast.java deleted file mode 100644 index da4a347f5..000000000 --- a/convex-gui/src/main/java/convex/gui/components/Toast.java +++ /dev/null @@ -1,93 +0,0 @@ -package convex.gui.components; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Graphics; -import java.awt.Point; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; -import java.awt.geom.RoundRectangle2D; - -import javax.swing.JComponent; -import javax.swing.JTextArea; -import javax.swing.JWindow; - -/** - * A simple class for implementing a "Toast" style notification. - */ -@SuppressWarnings("serial") -public class Toast extends JWindow { - - public static final Color SUCCESS = new Color(100,150,0); - public static final Color FAIL = new Color(150,50,50); - public static final Color INFO = new Color(50,100,150); - - public Toast(JComponent parent, JComponent component, Color color) { - super(); - this.setLayout(new BorderLayout()); - this.setBackground(color); - this.getContentPane().setBackground(color); - add(component); - - Point pp=parent.getLocationOnScreen(); - - int px=(int) pp.getX(); - int py=(int) pp.getY(); - int pw=parent.getWidth(); - int ph=parent.getHeight(); - int h=50; - this.setLocation(px, py+ph-h); - setSize(pw,h); - - addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent e) { - setShape(new RoundRectangle2D.Float(0, 0,getWidth(), getHeight(), 16, 16)); - } - }); - } - - @Override - public void paint(Graphics g) { - super.paint(g); - } - - private void doDisplay(final long millis) { - setVisible(true); - long start=System.currentTimeMillis(); - new Thread(()->{ - try { - long time=start; - while (time<(start+millis)) { - Thread.sleep(100); - time=System.currentTimeMillis(); - // drop opacity after 50% of time has elapsed - double opac=Math.min(1.0,Math.max(0.0,(2*(1.0-(time-start)/(double)millis)))); - setOpacity((float)opac) ; - } - } catch (InterruptedException e) { - // set interrupted flag, clear toast and return - setOpacity(0.0f); - Thread.currentThread().interrupt(); - } finally { - setVisible(false); - } - }).start();; - } - - public static void display(JComponent parent, JComponent component, Color colour) { - Toast toast=new Toast(parent,component,colour); - toast.doDisplay(2000); - } - - public static void display(JComponent parent, JComponent component) { - display(parent,component,SUCCESS); - } - - public static void display(JComponent parent, String message, Color colour) { - JTextArea ta=new JTextArea(message); - ta.setBackground(null); - ta.setEditable(false); - display(parent,ta,colour); - } -} diff --git a/convex-gui/src/main/java/convex/gui/components/UnlockWalletDialog.java b/convex-gui/src/main/java/convex/gui/components/UnlockWalletDialog.java deleted file mode 100644 index 05371017f..000000000 --- a/convex-gui/src/main/java/convex/gui/components/UnlockWalletDialog.java +++ /dev/null @@ -1,93 +0,0 @@ -package convex.gui.components; - -import java.awt.BorderLayout; -import java.awt.Font; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; - -import javax.swing.AbstractAction; -import javax.swing.Action; -import javax.swing.JButton; -import javax.swing.JComponent; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JPasswordField; -import javax.swing.KeyStroke; - -import convex.gui.utils.Toolkit; - -@SuppressWarnings("serial") -public class UnlockWalletDialog extends JDialog { - private JPasswordField passwordField; - - private char[] passPhrase = null; - - public static UnlockWalletDialog show(WalletComponent parent) { - UnlockWalletDialog dialog = new UnlockWalletDialog(parent); - dialog.setLocationRelativeTo(parent); - dialog.setVisible(true); - return dialog; - } - - char[] getPassPhrase() { - return passPhrase; - } - - public UnlockWalletDialog(WalletComponent walletComponent) { - this.setIconImage(Toolkit.WARNING.getImage()); - setAlwaysOnTop(true); - - setModalityType(ModalityType.DOCUMENT_MODAL); - setTitle("Unlock Wallet"); - setModal(true); - - JPanel panel_2 = new JPanel(); - getContentPane().add(panel_2, BorderLayout.NORTH); - panel_2.setLayout(new BorderLayout(0, 0)); - - JPanel panel_1 = new JPanel(); - panel_2.add(panel_1, BorderLayout.SOUTH); - - JButton btnUnlock = new JButton("Unlock"); - panel_1.add(btnUnlock); - btnUnlock.addActionListener(e -> { - this.passPhrase = passwordField.getPassword(); - close(); - }); - JButton btnCancel = new JButton("Cancel"); - panel_1.add(btnCancel); - - JPanel panel = new JPanel(); - panel_2.add(panel); - - JLabel lblPassphrase = new JLabel("Passphrase: "); - panel.add(lblPassphrase); - - passwordField = new JPasswordField(); - passwordField.setFont(new Font("Monospaced", Font.BOLD, 13)); - passwordField.setColumns(20); - panel.add(passwordField); - btnCancel.addActionListener(e -> close()); - - Action closeAction = new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - close(); - } - }; - - getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), - "close"); - getRootPane().getActionMap().put("close", closeAction); - - pack(); // set dialog to correct size given contents - - } - - public void close() { - passwordField = null; - setVisible(false); - } - -} diff --git a/convex-gui/src/main/java/convex/gui/components/WalletComponent.java b/convex-gui/src/main/java/convex/gui/components/WalletComponent.java deleted file mode 100644 index 2c82c6655..000000000 --- a/convex-gui/src/main/java/convex/gui/components/WalletComponent.java +++ /dev/null @@ -1,100 +0,0 @@ -package convex.gui.components; - -import java.awt.BorderLayout; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.GridLayout; -import java.awt.Insets; - -import javax.swing.Icon; -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; - -import convex.core.State; -import convex.core.crypto.WalletEntry; -import convex.core.data.Address; -import convex.core.util.Text; -import convex.gui.manager.PeerGUI; -import convex.gui.utils.Toolkit; - -@SuppressWarnings("serial") -public class WalletComponent extends BaseListComponent { - Icon icon = Toolkit.LOCKED_ICON; - - JButton lockButton; - - WalletEntry walletEntry; - - JPanel buttons = new JPanel(); - - private Address address; - - public WalletComponent(WalletEntry initialWalletEntry) { - this.walletEntry = initialWalletEntry; - address = walletEntry.getAddress(); - - setLayout(new BorderLayout()); - - // lock button - lockButton = new JButton(""); - buttons.add(lockButton); - lockButton.setIcon(walletEntry.isLocked() ? Toolkit.LOCKED_ICON : Toolkit.UNLOCKED_ICON); - lockButton.addActionListener(e -> { - if (walletEntry.isLocked()) { - UnlockWalletDialog dialog = UnlockWalletDialog.show(this); - char[] passPhrase = dialog.getPassPhrase(); - try { - walletEntry = walletEntry.unlock(passPhrase); - icon = Toolkit.UNLOCKED_ICON; - } catch (Throwable e1) { - JOptionPane.showMessageDialog(this, "Unable to unlock wallet: " + e1.getMessage()); - } - } else { - try { - walletEntry = walletEntry.lock(); - } catch (IllegalStateException e1) { - // OK, must be already locked. - } - icon = Toolkit.LOCKED_ICON; - } - lockButton.setIcon(icon); - }); - - // panel of buttons on right - add(buttons, BorderLayout.EAST); - - // identicon - JLabel identicon = new Identicon(walletEntry.getAddress().getHash()); - GridBagConstraints gbc_btnNewButton = new GridBagConstraints(); - gbc_btnNewButton.insets = new Insets(0, 0, 5, 5); - gbc_btnNewButton.gridx = 0; - gbc_btnNewButton.gridy = 0; - JPanel idPanel = new JPanel(); - idPanel.setLayout(new GridBagLayout()); - idPanel.add(identicon); - add(idPanel, BorderLayout.WEST); - - // address field - JPanel cPanel = new JPanel(); - cPanel.setLayout(new GridLayout(0, 1)); - CodeLabel addressLabel = new CodeLabel(address.toString()); - addressLabel.setFont(Toolkit.MONO_FONT); - cPanel.add(addressLabel); - CodeLabel infoLabel = new CodeLabel(getInfoString()); - cPanel.add(infoLabel); - add(cPanel, BorderLayout.CENTER); - - PeerGUI.getStateModel().addPropertyChangeListener(e -> { - infoLabel.setText(getInfoString()); - }); - } - - private String getInfoString() { - State s = PeerGUI.getLatestState(); - Long bal=s.getBalance(address); - return "Balance: " + ((bal==null)?"Null":Text.toFriendlyBalance(s.getBalance(address))); - } - -} diff --git a/convex-gui/src/main/java/convex/gui/components/WorldPanel.java b/convex-gui/src/main/java/convex/gui/components/WorldPanel.java deleted file mode 100644 index 3902bccb4..000000000 --- a/convex-gui/src/main/java/convex/gui/components/WorldPanel.java +++ /dev/null @@ -1,54 +0,0 @@ -package convex.gui.components; - -import java.awt.Color; -import java.awt.Graphics; -import java.awt.geom.Point2D; -import java.awt.image.BufferedImage; -import java.io.IOException; - -import javax.imageio.ImageIO; -import javax.swing.JPanel; - -import convex.gui.utils.RobinsonProjection; - -@SuppressWarnings("serial") -public class WorldPanel extends JPanel { - BufferedImage image; - - public WorldPanel() { - try { - image = ImageIO.read(Thread.currentThread().getContextClassLoader().getResource("images/world.png")); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void paintComponent(Graphics g) { - if (image == null) return; - - int w = this.getWidth(); - int h = this.getHeight(); - int sw = image.getWidth(); - int sh = image.getHeight(); - int dw = Math.min(w, h * sw / sh); - int dh = Math.min(h, w * sh / sw); - - int y = (h - dh) / 2; - - g.drawImage(image, 0, y, dw, y + dh, 0, 0, sw, sh, null); - - paintDot(g, 51.5073219, -0.1276474, 0, y, dw, dh); // London - paintDot(g, -33.928992, 18.417396, 0, y, dw, dh); // Cape Town - paintDot(g, 35.6828387, 139.7594549, 0, y, dw, dh); // Tokyo - paintDot(g, 23.135305, -82.3589631, 0, y, dw, dh); // Havana - } - - private void paintDot(Graphics g, double latitude, double longitude, int x, int y, int dw, int dh) { - g.setColor(Color.RED); - Point2D pt = RobinsonProjection.getPoint(latitude, longitude); - int px = (int) (x + dw * pt.getX()); - int py = (int) (y + dh * pt.getY()); - g.fillOval(px, py, 5, 5); - } -} diff --git a/convex-gui/src/main/java/convex/gui/components/models/AccountsTableModel.java b/convex-gui/src/main/java/convex/gui/components/models/AccountsTableModel.java deleted file mode 100644 index 41a5a077a..000000000 --- a/convex-gui/src/main/java/convex/gui/components/models/AccountsTableModel.java +++ /dev/null @@ -1,90 +0,0 @@ -package convex.gui.components.models; - -import javax.swing.table.AbstractTableModel; -import javax.swing.table.TableModel; - -import convex.core.State; -import convex.core.data.ACell; -import convex.core.data.AMap; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.core.data.Keyword; -import convex.core.init.Init; -import convex.core.util.Utils; - -@SuppressWarnings("serial") -public class AccountsTableModel extends AbstractTableModel implements TableModel { - - private State state; - - public AccountsTableModel(State state) { - this.state = state; - } - - private static final String[] FIXED_COLS = new String[] { "Address", "Type", "Count", "Balance", "Name", "Env.Size", "Allowance" }; - - public String getColumnName(int col) { - if (col < FIXED_COLS.length) return FIXED_COLS[col]; - return "FOO"; - } - - @Override - public int getRowCount() { - return Utils.checkedInt(state.getAccounts().count()); - } - - @Override - public int getColumnCount() { - // TODO token columns? - return FIXED_COLS.length; - } - - @Override - public boolean isCellEditable(int row, int col) { - return false; - } - - @SuppressWarnings("unchecked") - @Override - public Object getValueAt(int rowIndex, int columnIndex) { - Address address = Address.create(rowIndex); - AccountStatus as = getEntry(rowIndex); - switch (columnIndex) { - case 0: - return address.toString(); - case 1: - return as.isActor()?"Actor":"User"; - case 2: { - long seq=as.getSequence(); - return (seq>=0)?seq:""; - } - case 3: - return as.getBalance(); - case 4: { - ACell o = as.getHoldings().get(Init.REGISTRY_ADDRESS); - if (o == null) return ""; - if (!(o instanceof AMap)) return ""; - AMap a = (AMap) o; - return a.get(Keyword.create("name")); - } - case 5: - return as.getMemorySize(); - case 6: - return as.getMemory(); - default: - return ""; - } - } - - public void setState(State newState) { - if (state != newState) { - state = newState; - fireTableDataChanged(); - } - } - - public AccountStatus getEntry(long ix) { - return state.getAccounts().get(ix); - } - -} diff --git a/convex-gui/src/main/java/convex/gui/components/models/OracleTableModel.java b/convex-gui/src/main/java/convex/gui/components/models/OracleTableModel.java deleted file mode 100644 index c0c92597e..000000000 --- a/convex-gui/src/main/java/convex/gui/components/models/OracleTableModel.java +++ /dev/null @@ -1,109 +0,0 @@ -package convex.gui.components.models; - -import javax.swing.table.AbstractTableModel; -import javax.swing.table.TableModel; - -import convex.core.State; -import convex.core.data.ACell; -import convex.core.data.AMap; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.core.data.Keyword; -import convex.core.data.MapEntry; -import convex.core.data.Symbol; -import convex.core.util.Utils; - -/** - * Model for the Oracle table - */ -@SuppressWarnings("serial") -public class OracleTableModel extends AbstractTableModel implements TableModel { - - private State state; - private Address oracle; - - Symbol LIST_S=Symbol.create("*list*"); - Symbol RESULTS_S=Symbol.create("*results*"); - - Keyword DESC_K=Keyword.create("desc"); - - public OracleTableModel(State state, Address oracle) { - this.state = state; - this.oracle=oracle; - } - - private static final String[] FIXED_COLS=new String[] { - "Key","Description", "Finalised?","Value" - }; - - public String getColumnName(int col) { - if (col list=as.getEnvironmentValue(LIST_S); - if (list==null) { - System.err.println("OracleTableModel missing oracle list? in "+oracle); - return 0; - } - return Utils.checkedInt(list.count()); - } - - @Override - public int getColumnCount() { - return FIXED_COLS.length; - } - - @Override - public boolean isCellEditable(int row, int col) { - return false; - } - - @SuppressWarnings("unchecked") - public AMap getList() { - AMap env=state.getAccount(oracle).getEnvironment(); - AMap list=(AMap) env.get(LIST_S); - return list; - } - - @SuppressWarnings("unchecked") - @Override - public Object getValueAt(int rowIndex, int columnIndex) { - AMap env=state.getAccount(oracle).getEnvironment(); - AMap list=(AMap) env.get(LIST_S); - MapEntry me=list.entryAt(rowIndex); - ACell key=me.getKey(); - switch (columnIndex) { - case 0: return key.toString(); - case 1: { - AMap data=(AMap) me.getValue(); - return data.get(DESC_K); - } - case 2: { - boolean done=((AMap) env.get(RESULTS_S)).containsKey(key); - return done?"Yes":"No"; - } - case 3: { - AMap results=((AMap) env.get(RESULTS_S)); - MapEntry rme=results.getEntry(key); - return (rme==null)?"":rme.getValue(); - } - - default: return ""; - } - } - - public void setState(State newState) { - if (state!=newState) { - state=newState; - fireTableDataChanged(); - } - } -} diff --git a/convex-gui/src/main/java/convex/gui/components/models/StateModel.java b/convex-gui/src/main/java/convex/gui/components/models/StateModel.java deleted file mode 100644 index 5bb227167..000000000 --- a/convex-gui/src/main/java/convex/gui/components/models/StateModel.java +++ /dev/null @@ -1,68 +0,0 @@ -package convex.gui.components.models; - -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.beans.PropertyChangeSupport; -// import java.util.logging.Logger; - -import javax.swing.SwingUtilities; - -/** - * Model for state values which may be observer / listened to. - * - * Fires a property changed event for the property "value" whenever it is - * updated. - * - * @param - */ -public class StateModel { - - // private static final Logger log = Logger.getLogger(StateModel.class.getName()); - - - private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this); - - T value; - - public StateModel(T value) { - this.value = value; - } - - public StateModel() { - this(null); - } - - public static StateModel create(T value) { - return new StateModel(value); - } - - public T getValue() { - return value; - } - - /** - * Sets the value for this state model, firing any relevant property change - * listeners. - * - * @param newValue - */ - public void setValue(T newValue) { - T oldValue = this.value; - this.value = newValue; - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - // log.info("State update reported"); - propertyChangeSupport.firePropertyChange(new PropertyChangeEvent(this, "value", oldValue, newValue)); - } - }); - } - - public void addPropertyChangeListener(PropertyChangeListener listener) { - propertyChangeSupport.addPropertyChangeListener(listener); - } - - public void removePropertyChangeListener(PropertyChangeListener listener) { - propertyChangeSupport.removePropertyChangeListener(listener); - } -} diff --git a/convex-gui/src/main/java/convex/gui/etch/EtchExplorer.java b/convex-gui/src/main/java/convex/gui/etch/EtchExplorer.java deleted file mode 100644 index 9e9b4128c..000000000 --- a/convex-gui/src/main/java/convex/gui/etch/EtchExplorer.java +++ /dev/null @@ -1,112 +0,0 @@ -package convex.gui.etch; - -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.EventQueue; -import java.awt.Toolkit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.JFrame; -import javax.swing.JPanel; -import javax.swing.JTabbedPane; - -import convex.core.store.Stores; -import convex.gui.components.models.StateModel; -import convex.gui.etch.panels.DatabasePanel; -import etch.EtchStore; - -/** - * A Client application for the Convex Network - */ -@SuppressWarnings("serial") -public class EtchExplorer extends JPanel { - - public static final Logger log = LoggerFactory.getLogger(EtchExplorer.class.getName()); - - private static JFrame frame; - - public static long maxBlock = 0; - - /** - * Launch the application. - * @param args Command line args - */ - public static void main(String[] args) { - // call to set up Look and Feel - convex.gui.utils.Toolkit.init(); - - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - try { - EtchExplorer.frame = new JFrame(); - frame.setTitle("Etch Explorer"); - frame.setIconImage(Toolkit.getDefaultToolkit() - .getImage(EtchExplorer.class.getResource("/images/Convex.png"))); - frame.setBounds(100, 100, 1024, 768); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - EtchExplorer window = new EtchExplorer(); - frame.getContentPane().add(window, BorderLayout.CENTER); - frame.pack(); - frame.setVisible(true); - } catch (Exception e) { - e.printStackTrace(); - } - } - }); - } - - /* - * Main component panel - */ - JPanel panel = new JPanel(); - - private static StateModel etchState = StateModel.create((EtchStore)Stores.getGlobalStore()); - - DatabasePanel homePanel = new DatabasePanel(this); - JTabbedPane tabs = new JTabbedPane(); - JPanel mainPanel = new JPanel(); - - - /** - * Create the application. - */ - public EtchExplorer() { - - - setLayout(new BorderLayout()); - this.add(tabs, BorderLayout.CENTER); - - tabs.add("Database", homePanel); - } - - public void switchPanel(String title) { - int n = tabs.getTabCount(); - for (int i = 0; i < n; i++) { - if (tabs.getTitleAt(i).contentEquals(title)) { - tabs.setSelectedIndex(i); - return; - } - } - System.err.println("Missing tab: " + title); - } - - public static Component getFrame() { - return frame; - } - - public StateModel getEtchState() { - return etchState; - } - - public EtchStore getStore() { - return etchState.getValue(); - } - - public void setStore(EtchStore newEtch) { - EtchStore e=etchState.getValue(); - e.close(); - etchState.setValue(newEtch); - } -} diff --git a/convex-gui/src/main/java/convex/gui/etch/panels/DatabasePanel.java b/convex-gui/src/main/java/convex/gui/etch/panels/DatabasePanel.java deleted file mode 100644 index 91d799a6c..000000000 --- a/convex-gui/src/main/java/convex/gui/etch/panels/DatabasePanel.java +++ /dev/null @@ -1,98 +0,0 @@ -package convex.gui.etch.panels; - -import javax.swing.JPanel; -import java.awt.BorderLayout; -import java.awt.Dimension; - -import javax.swing.JLabel; -import javax.swing.SwingConstants; - -import java.awt.Font; -import java.awt.GridLayout; -import java.io.File; -import java.io.IOException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.Color; -import javax.swing.border.EtchedBorder; -import javax.swing.border.TitledBorder; - -import convex.gui.components.ActionPanel; -import convex.gui.etch.EtchExplorer; -import etch.EtchStore; - -import javax.swing.JButton; -import javax.swing.JFileChooser; - -@SuppressWarnings("serial") -public class DatabasePanel extends JPanel { - - public static final Logger log = LoggerFactory.getLogger(DatabasePanel.class.getName()); - - - /** - * Create the panel. - * @param explorer - */ - public DatabasePanel(EtchExplorer explorer) { - setPreferredSize(new Dimension(800,600)); - setLayout(new BorderLayout(0, 0)); - - JPanel panel = new JPanel(); - add(panel); - panel.setLayout(new GridLayout(0, 1, 0, 0)); - - JPanel filePanel = new JPanel(); - filePanel.setBorder(new TitledBorder(new EtchedBorder(EtchedBorder.LOWERED, new Color(255, 255, 255), new Color(160, 160, 160)), "File", TitledBorder.LEADING, TitledBorder.TOP, null, new Color(0, 0, 0))); - panel.add(filePanel); - filePanel.setLayout(new BorderLayout(0, 0)); - - JLabel lblSelectPrompt = new JLabel("Select an Etch Database to Explore"); - filePanel.add(lblSelectPrompt, BorderLayout.NORTH); - - JLabel lblDatabaseFile = new JLabel(); - filePanel.add(lblDatabaseFile); - lblDatabaseFile.setText(explorer.getStore().getFileName()); - explorer.getEtchState().addPropertyChangeListener(pc->{ - lblDatabaseFile.setText(((EtchStore)pc.getNewValue()).getFileName()); - }); - - - JPanel actionPanel = new ActionPanel(); - filePanel.add(actionPanel, BorderLayout.SOUTH); - - JButton btnOpen = new JButton("Open File..."); - actionPanel.add(btnOpen); - final JFileChooser fc = new JFileChooser(); - btnOpen.addActionListener(e->{ - if (e.getSource() == btnOpen) { - fc.setCurrentDirectory(explorer.getStore().getFile()); - int returnVal = fc.showOpenDialog(DatabasePanel.this); - - if (returnVal == JFileChooser.APPROVE_OPTION) { - File file = fc.getSelectedFile(); - log.info("Opening Etch Database: {}", file.getName()); - - if (file.exists()) { - try { - EtchStore newEtch=EtchStore.create(file); - explorer.setStore(newEtch); - } catch (IOException ex) { - log.error("Error opening Etch database: " + ex.getMessage()); - - } - } - } - } - }); - - - JLabel lblWelome = new JLabel("Welcome to Convex"); - lblWelome.setFont(new Font("Monospaced", Font.PLAIN, 18)); - lblWelome.setHorizontalAlignment(SwingConstants.CENTER); - panel.add(lblWelome); - - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/PeerGUI.java b/convex-gui/src/main/java/convex/gui/manager/PeerGUI.java deleted file mode 100644 index ba4d3033c..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/PeerGUI.java +++ /dev/null @@ -1,317 +0,0 @@ -package convex.gui.manager; - -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.EventQueue; -import java.awt.Toolkit; -import java.awt.event.WindowEvent; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeoutException; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -import javax.swing.JFrame; -import javax.swing.JPanel; -import javax.swing.JTabbedPane; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import convex.api.Convex; -import convex.core.Order; -import convex.core.Peer; -import convex.core.Result; -import convex.core.State; -import convex.core.crypto.AKeyPair; -import convex.core.crypto.WalletEntry; -import convex.core.data.ACell; -import convex.core.data.AccountKey; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.core.init.Init; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Invoke; -import convex.core.util.Utils; -import convex.gui.components.PeerView; -import convex.gui.components.models.StateModel; -import convex.gui.manager.mainpanels.AboutPanel; -import convex.gui.manager.mainpanels.AccountsPanel; -import convex.gui.manager.mainpanels.ActorsPanel; -import convex.gui.manager.mainpanels.HomePanel; -import convex.gui.manager.mainpanels.KeyGenPanel; -import convex.gui.manager.mainpanels.MessageFormatPanel; -import convex.gui.manager.mainpanels.PeersListPanel; -import convex.gui.manager.mainpanels.WalletPanel; -import convex.peer.Server; - -@SuppressWarnings("serial") -public class PeerGUI extends JPanel { - - private static final Logger log = LoggerFactory.getLogger(PeerGUI.class.getName()); - - private static JFrame frame; - - public static List KEYPAIRS=new ArrayList<>(); - - private static final int NUM_PEERS=8; - - static { - for (int i=0; i PEERKEYS=KEYPAIRS.stream().map(kp->kp.getAccountKey()).collect(Collectors.toList()); - - public static State genesisState=Init.createState(PEERKEYS); - private static StateModel latestState = StateModel.create(genesisState); - public static StateModel tickState = StateModel.create(0L); - - public static long maxBlock = 0; - - /** - * Launch the application. - * @param args Command line args - * @throws IOException - */ - public static void main(String[] args) throws IOException { - // TODO: Store config - // Stores.setGlobalStore(EtchStore.create(new File("peers-shared-db"))); - - // call to set up Look and Feel - convex.gui.utils.Toolkit.init(); - - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - try { - PeerGUI.frame = new JFrame(); - frame.setTitle("Convex Peer Manager"); - frame.setIconImage(Toolkit.getDefaultToolkit() - .getImage(PeerGUI.class.getResource("/images/Convex.png"))); - frame.setBounds(100, 100, 1024, 768); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - - PeerGUI window = new PeerGUI(); - frame.getContentPane().add(window, BorderLayout.CENTER); - frame.pack(); - frame.setVisible(true); - - frame.addWindowListener(new java.awt.event.WindowAdapter() { - public void windowClosing(WindowEvent winEvt) { - // shut down peers gracefully - window.peerPanel.closePeers(); - } - }); - - } catch (Exception e) { - e.printStackTrace(); - } - } - }); - } - - /* - * Main component panel - */ - JPanel panel = new JPanel(); - - HomePanel homePanel = new HomePanel(); - PeersListPanel peerPanel; - WalletPanel walletPanel = new WalletPanel(); - KeyGenPanel keyGenPanel = new KeyGenPanel(this); - MessageFormatPanel messagePanel = new MessageFormatPanel(this); - AboutPanel aboutPanel = new AboutPanel(); - JTabbedPane tabs = new JTabbedPane(); - JPanel mainPanel = new JPanel(); - JPanel accountsPanel = new AccountsPanel(this); - - /** - * Create the application. - */ - public PeerGUI() { - peerPanel= new PeersListPanel(this); - - setLayout(new BorderLayout()); - this.add(tabs, BorderLayout.CENTER); - - tabs.add("Home", homePanel); - tabs.add("Peers", peerPanel); - tabs.add("Wallet", getWalletPanel()); - tabs.add("Accounts", accountsPanel); - tabs.add("KeyGen", keyGenPanel); - tabs.add("Message", messagePanel); - tabs.add("Actors", new ActorsPanel(this)); - tabs.add("About", aboutPanel); - - // launch local peers for testing - EventQueue.invokeLater(() -> { - peerPanel.launchAllPeers(this); - }); - - updateThread.start(); - } - - private boolean updateRunning = true; - - private long cp = 0; - - private Thread updateThread = new Thread(new Runnable() { - @Override - public void run() { - while (updateRunning) { - try { - Thread.sleep(100); - tickState.setValue(tickState.getValue()+1); - - java.util.List peerViews = peerPanel.getPeerViews(); - peerPanel.repaint(); - State latest = latestState.getValue(); - for (PeerView s : peerViews) { - s.checkPeer(); - - Server serv=s.peerServer; - if (serv==null) continue; - - Peer p = serv.getPeer(); - if (p==null) continue; - - Order order=p.getPeerOrder(); - if (order==null) continue; // not an active peer? - maxBlock = Math.max(maxBlock, order.getBlockCount()); - - long pcp = p.getConsensusPoint(); - if (pcp > cp) { - cp = pcp; - //String ls="PeerGUI Consensus State update detected at depth "+cp; - //System.err.println(ls); - latest = p.getConsensusState(); - - } - } - latestState.setValue(latest); // trigger peer view repaints etc. - - } catch (InterruptedException e) { - // - log.warn("Update thread interrupted abnormally: "+e.getMessage()); - e.printStackTrace(); - Thread.currentThread().interrupt(); - } - } - log.info("Manager update thread ended"); - } - }, "GUI Manager state update thread"); - - - - @Override - public void finalize() { - // terminate the update thread - updateRunning = false; - } - - public void switchPanel(String title) { - int n = tabs.getTabCount(); - for (int i = 0; i < n; i++) { - if (tabs.getTitleAt(i).contentEquals(title)) { - tabs.setSelectedIndex(i); - return; - } - } - System.err.println("Missing tab: " + title); - } - - public WalletPanel getWalletPanel() { - return walletPanel; - } - - /** - * Builds a connection to the peer network - * @param address Address for connection - * @param kp Key Pair for connection - * @return Convex connection instance - * @throws IOException - * @throws TimeoutException - */ - public static Convex makeConnection(Address address,AKeyPair kp) throws IOException, TimeoutException { - InetSocketAddress host = getDefaultPeer().getHostAddress(); - return Convex.connect(host,address, kp); - } - - /** - * Executes a transaction using the given Wallet - * - * @param code Code to execute - * @param we Wallet to use - * @return Future for Result - */ - public static CompletableFuture execute(WalletEntry we, ACell code) { - Address address = we.getAddress(); - AccountStatus as = getLatestState().getAccount(address); - long sequence = as.getSequence() + 1; - ATransaction trans = Invoke.create(address,sequence, code); - return execute(we,trans); - } - - /** - * Executes a transaction using the given Wallet - * - * @param we Wallet to use - * @param trans Transaction to execute - * @return Future for Result - */ - public static CompletableFuture execute(WalletEntry we, ATransaction trans) { - try { - AKeyPair kp = we.getKeyPair(); - Convex convex = makeConnection(we.getAddress(),kp); - CompletableFuture fr= convex.transact(trans); - log.trace("Sent transaction: {}",trans); - return fr; - } catch (IOException | TimeoutException e) { - throw Utils.sneakyThrow(e); - } - } - - /** - * Executes a transaction using the given Wallet - * - * @param we Wallet to use - * @param trans Transaction to execute - * @param receiveAction - */ - public static void execute(WalletEntry we, ATransaction trans, Consumer receiveAction) { - execute(we,trans).thenAcceptAsync(receiveAction); - } - - public static State getLatestState() { - return latestState.getValue(); - } - - public static Component getFrame() { - return frame; - } - - public static StateModel getStateModel() { - return latestState; - } - - public static PeerView getDefaultPeer() { - return PeersListPanel.getFirst(); - } - - public static Address getUserAddress(int i) { - return Init.getGenesisPeerAddress(i); - } - - public static AKeyPair getUserKeyPair(int i) { - return KEYPAIRS.get(i); - } - - public static Address getGenesisAddress() { - return Init.getGenesisAddress(); - } -} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/AboutPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/AboutPanel.java deleted file mode 100644 index 69f65d8b8..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/mainpanels/AboutPanel.java +++ /dev/null @@ -1,81 +0,0 @@ -package convex.gui.manager.mainpanels; - -import java.awt.BorderLayout; - -import javax.swing.JButton; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JTextArea; - -import convex.core.State; -import convex.core.data.prim.CVMLong; -import convex.core.util.Text; -import convex.gui.components.ActionPanel; -import convex.gui.manager.PeerGUI; -import convex.gui.utils.Toolkit; - -@SuppressWarnings("serial") -public class AboutPanel extends JPanel { - - private final JTextArea textArea; - - public AboutPanel() { - setLayout(new BorderLayout(0, 0)); - - JPanel panel = new ActionPanel(); - add(panel, BorderLayout.SOUTH); - - JButton creditsButton = new JButton("Credits"); - panel.add(creditsButton); - - JPanel panel_1 = new JPanel(); - add(panel_1, BorderLayout.CENTER); - panel_1.setLayout(new BorderLayout(0, 0)); - - textArea = new JTextArea(); - textArea.setEditable(false); - textArea.setBackground(null); - textArea.setFont(Toolkit.SMALL_MONO_FONT); - - PeerGUI.getStateModel().addPropertyChangeListener(e -> { - updateState((State) e.getNewValue()); - }); - - panel_1.add(textArea); - creditsButton.addActionListener(e -> { - JOptionPane.showMessageDialog(null, - "Icons made by Freepik from www.flaticon.com\n" + "Royalty free map image by J. Bruce Jones", - "Credits", JOptionPane.PLAIN_MESSAGE); - }); - - updateState(PeerGUI.getLatestState()); - } - - private String lpad(Object s) { - return Text.leftPad(s.toString(), 25); - } - - private void updateState(State s) { - StringBuilder sb = new StringBuilder(); - CVMLong timestamp = s.getTimeStamp(); - - sb.append("Consensus state hash: " + s.getHash().toHexString() + "\n"); - sb.append("Timestamp: " + Text.dateFormat(timestamp.longValue()) + " (" + timestamp + ")\n"); - sb.append("\n"); - sb.append("Max Blocks: " + lpad(PeerGUI.maxBlock) + "\n"); - sb.append("\n"); - sb.append("Account statistics\n"); - sb.append(" # Accounts: " + lpad(s.getAccounts().count()) + "\n"); - sb.append(" # Peers: " + lpad(s.getPeers().count()) + "\n"); - sb.append("\n"); - sb.append("Globals\n"); - sb.append(" fees: " + lpad(Text.toFriendlyBalance(s.getGlobalFees().longValue())) + "\n"); - sb.append(" juice-price: " + lpad(Text.toFriendlyBalance(s.getJuicePrice().longValue())) + "\n"); - sb.append("\n"); - sb.append("Total funds: " + lpad(Text.toFriendlyBalance(s.computeTotalFunds())) + "\n"); - sb.append("Total stake: " + lpad(Text.toFriendlyBalance(s.computeStakes().get(null))) + "\n"); - - textArea.setText(sb.toString()); - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/AccountsPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/AccountsPanel.java deleted file mode 100644 index ce92725e3..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/mainpanels/AccountsPanel.java +++ /dev/null @@ -1,150 +0,0 @@ -package convex.gui.manager.mainpanels; - -import java.awt.BorderLayout; -import java.awt.datatransfer.Clipboard; -import java.awt.datatransfer.StringSelection; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JMenuItem; -import javax.swing.JPanel; -import javax.swing.JPopupMenu; -import javax.swing.JScrollPane; -import javax.swing.JTable; -import javax.swing.table.DefaultTableCellRenderer; - -import convex.core.State; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.gui.components.ActionPanel; -import convex.gui.components.models.AccountsTableModel; -import convex.gui.manager.PeerGUI; -import convex.gui.manager.windows.actor.ActorWindow; -import convex.gui.utils.Toolkit; - -@SuppressWarnings("serial") -public class AccountsPanel extends JPanel { - AccountsTableModel tableModel = new AccountsTableModel(PeerGUI.getLatestState()); - JTable table = new JTable(tableModel); - - static class ActorRenderer extends DefaultTableCellRenderer { - public ActorRenderer() { - super(); - } - - public void setValue(Object value) { - setText(value.toString()); - } - } - - public AccountsPanel(PeerGUI manager) { - setLayout(new BorderLayout()); - - PeerGUI.getStateModel().addPropertyChangeListener(pc -> { - State newState = (State) pc.getNewValue(); - tableModel.setState(newState); - }); - - DefaultTableCellRenderer leftRenderer = new DefaultTableCellRenderer(); - leftRenderer.setHorizontalAlignment(JLabel.LEFT); - DefaultTableCellRenderer rightRenderer = new DefaultTableCellRenderer(); - rightRenderer.setHorizontalAlignment(JLabel.RIGHT); - - table.getColumnModel().getColumn(0).setCellRenderer(leftRenderer); - table.getColumnModel().getColumn(0).setPreferredWidth(150); - - ActorRenderer actorRenderer = new ActorRenderer(); - actorRenderer.setHorizontalAlignment(JLabel.CENTER); - table.getColumnModel().getColumn(1).setPreferredWidth(70); - table.getColumnModel().getColumn(1).setCellRenderer(actorRenderer); - - table.getColumnModel().getColumn(2).setCellRenderer(rightRenderer); - table.getColumnModel().getColumn(2).setPreferredWidth(70); - - table.getColumnModel().getColumn(3).setCellRenderer(rightRenderer); - table.getColumnModel().getColumn(3).setPreferredWidth(180); - - table.getColumnModel().getColumn(4).setPreferredWidth(200); - table.getColumnModel().getColumn(4).setCellRenderer(leftRenderer); - - table.getColumnModel().getColumn(5).setPreferredWidth(100); - table.getColumnModel().getColumn(5).setCellRenderer(rightRenderer); - - table.getColumnModel().getColumn(6).setPreferredWidth(100); - table.getColumnModel().getColumn(6).setCellRenderer(rightRenderer); - - // popup menu, not sure why this doesn't work.... - final JPopupMenu popupMenu = new JPopupMenu(); - JMenuItem copyItem = new JMenuItem("Copy Address"); - copyItem.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - copyAddress(); - } - }); - - table.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - int r = table.rowAtPoint(e.getPoint()); - if (r >= 0 && r < table.getRowCount()) { - table.setRowSelectionInterval(r, r); - } else { - table.clearSelection(); - } - - if (e.isPopupTrigger() || (e.getButton() & MouseEvent.BUTTON3) > 0) { - popupMenu.show(e.getComponent(), e.getX(), e.getY()); - } - } - }); - - JPanel actionPanel = new ActionPanel(); - add(actionPanel, BorderLayout.SOUTH); - - JButton btnCopy = new JButton("Copy Address"); - actionPanel.add(btnCopy); - btnCopy.addActionListener(e -> { - copyAddress(); - }); - - JButton btnActor = new JButton("Examine Actor..."); - actionPanel.add(btnActor); - btnActor.addActionListener(e -> { - long ix=table.getSelectedRow(); - AccountStatus as = tableModel.getEntry(ix); - if (as == null) return; - Address addr = Address.create(ix); - if (!as.isActor()) return; - ActorWindow pw = new ActorWindow(manager, addr); - pw.launch(); - }); - - // Turn off auto-resize, since we want a scrollable table - table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); - - JScrollPane scrollPane = new JScrollPane(table); - scrollPane.getViewport().setBackground(null); - add(scrollPane, BorderLayout.CENTER); - - table.setFont(Toolkit.SMALL_MONO_FONT); - table.getTableHeader().setFont(Toolkit.SMALL_MONO_FONT); - ((DefaultTableCellRenderer) table.getTableHeader().getDefaultRenderer()).setHorizontalAlignment(JLabel.LEFT); - - } - - private void copyAddress() { - int row = table.getSelectedRow(); - if (row < 0) return; - - Address addr=Address.create(row); - Clipboard clipboard = java.awt.Toolkit.getDefaultToolkit().getSystemClipboard(); - StringSelection stringSelection = new StringSelection(addr.toHexString()); - clipboard.setContents(stringSelection, null); - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/ActorsPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/ActorsPanel.java deleted file mode 100644 index ae3da68cc..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/mainpanels/ActorsPanel.java +++ /dev/null @@ -1,38 +0,0 @@ -package convex.gui.manager.mainpanels; - -import java.awt.BorderLayout; - -import javax.swing.JPanel; -import javax.swing.JTabbedPane; - -import convex.gui.manager.PeerGUI; -import convex.gui.manager.mainpanels.actors.DeployPanel; -import convex.gui.manager.mainpanels.actors.MarketsPanel; -import convex.gui.manager.mainpanels.actors.OraclePanel; - -/** - * Top level panel that displays some standard Actors - */ -@SuppressWarnings("serial") -public class ActorsPanel extends JPanel { - - private JTabbedPane typePane; - - public ActorsPanel(PeerGUI manager) { - setLayout(new BorderLayout(0, 0)); - - typePane = new JTabbedPane(); - - typePane.add("Oracle", new OraclePanel()); - - // TODO: fix registry address - // typePane.add("Registry", new ActorInvokePanel(manager, ...)); - - typePane.add("Prediction Markets", new MarketsPanel(manager)); - - typePane.add("Deploy", new DeployPanel()); - - add(typePane, BorderLayout.CENTER); - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/DeployPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/DeployPanel.java deleted file mode 100644 index 95bdd379c..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/mainpanels/DeployPanel.java +++ /dev/null @@ -1,44 +0,0 @@ -package convex.gui.manager.mainpanels; - -import java.awt.BorderLayout; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; - -import convex.gui.components.AccountChooserPanel; -import convex.gui.components.ActionPanel; - -@SuppressWarnings("serial") -public class DeployPanel extends JPanel { - - - private AccountChooserPanel acctChooser; - - public DeployPanel() { - setLayout(new BorderLayout()); - - // =========================================== - // Top panel - acctChooser=new AccountChooserPanel(); - this.add(acctChooser, BorderLayout.NORTH); - - // =========================================== - // Centre panel - JPanel centrePanel=new JPanel(); - this.add(centrePanel,BorderLayout.CENTER); - - centrePanel.add(new JLabel("Tool for deploying standard Actors")); - - // ============================================ - // Action buttons - ActionPanel actionPanel=new ActionPanel(); - add(actionPanel, BorderLayout.SOUTH); - - JButton deployButton=new JButton("Deploy..."); - actionPanel.add(deployButton); - deployButton.addActionListener(e->{ - - }); - } -} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/HomePanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/HomePanel.java deleted file mode 100644 index 53a0dc69f..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/mainpanels/HomePanel.java +++ /dev/null @@ -1,36 +0,0 @@ -package convex.gui.manager.mainpanels; - -import javax.swing.JPanel; -import java.awt.BorderLayout; -import java.awt.Dimension; - -import javax.swing.JLabel; -import javax.swing.SwingConstants; - -import convex.gui.components.WorldPanel; - -import java.awt.Font; - -@SuppressWarnings("serial") -public class HomePanel extends JPanel { - - /** - * Create the panel. - */ - public HomePanel() { - setPreferredSize(new Dimension(800,600)); - setLayout(new BorderLayout(0, 0)); - - JPanel panel = new JPanel(); - add(panel); - panel.setLayout(new BorderLayout(0, 0)); - - JLabel lblWelome = new JLabel("Welome to Convex"); - lblWelome.setFont(new Font("Monospaced", Font.PLAIN, 18)); - lblWelome.setHorizontalAlignment(SwingConstants.CENTER); - panel.add(lblWelome, BorderLayout.NORTH); - - panel.add(new WorldPanel(), BorderLayout.CENTER); - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/KeyGenPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/KeyGenPanel.java deleted file mode 100644 index b23cf6ea4..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/mainpanels/KeyGenPanel.java +++ /dev/null @@ -1,221 +0,0 @@ -package convex.gui.manager.mainpanels; - -import java.awt.BorderLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JTextArea; -import javax.swing.border.EmptyBorder; - -import convex.core.crypto.AKeyPair; -import convex.core.crypto.Ed25519KeyPair; -import convex.core.crypto.Mnemonic; -import convex.core.crypto.WalletEntry; -import convex.core.data.Blob; -import convex.core.data.Hash; -import convex.core.util.Utils; -import convex.gui.components.ActionPanel; -import convex.gui.manager.PeerGUI; -import convex.gui.utils.Toolkit; - -@SuppressWarnings("serial") -public class KeyGenPanel extends JPanel { - - JTextArea mnemonicArea; - JTextArea privateKeyArea; - JTextArea publicKeyArea; - JTextArea addressArea; - - JButton addWalletButton = new JButton("Add to wallet"); - - private String hexKeyFormat(String pk) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < (pk.length() / 32); i++) { - if (i > 0) sb.append('\n'); - for (int j = 0; j < 4; j++) { - if (j > 0) sb.append(' '); - int ix = 8 * (j + (i * 4)); - sb.append(pk.substring(ix, ix + 8)); - } - } - return sb.toString(); - } - - private void updateMnemonic() { - String s = mnemonicArea.getText(); - try { - byte[] bs = Mnemonic.decode(s, 128); - Hash h = Blob.wrap(bs).getContentHash(); - AKeyPair kp = AKeyPair.create(h.getBytes()); - String privateKeyString = kp.getEncodedPrivateKey().toHexString(); - privateKeyArea.setText(hexKeyFormat(privateKeyString)); - } catch (Exception ex) { - String pks = ""; - if (s.isBlank()) pks = ""; - privateKeyArea.setText(pks); - } - updatePublicKeys(); - } - - private void updatePrivateKey() { - try { - mnemonicArea.setText(""); - updatePublicKeys(); - } catch (Exception ex) { - System.err.println(ex.getMessage()); - return; - } - } - - private void updatePublicKeys() { - String s = privateKeyArea.getText(); - try { - Blob b = Blob.fromHex(Utils.stripWhiteSpace(s)); - AKeyPair kp = Ed25519KeyPair.create(b.getBytes()); - // String pk=Utils.toHexString(kp.getPrivateKey(),64); - addressArea.setText(kp.getAccountKey().toChecksumHex()); - publicKeyArea.setText(hexKeyFormat(kp.getAccountKey().toChecksumHex())); - addWalletButton.setEnabled(true); - } catch (Exception ex) { - addressArea.setText(""); - publicKeyArea.setText(""); - addWalletButton.setEnabled(false); - - return; - } - } - - /** - * Create the panel. - * @param manager - */ - public KeyGenPanel(PeerGUI manager) { - setLayout(new BorderLayout(0, 0)); - - JPanel actionPanel = new ActionPanel(); - add(actionPanel, BorderLayout.SOUTH); - - JButton btnRecreate = new JButton("Generate"); - actionPanel.add(btnRecreate); - btnRecreate.addActionListener(e -> { - mnemonicArea.setText(Mnemonic.createSecureRandom()); - updateMnemonic(); - }); - - JButton btnNewButton = new JButton("Export..."); - actionPanel.add(btnNewButton); - - actionPanel.add(addWalletButton); - addWalletButton.addActionListener(e -> { - String pks = privateKeyArea.getText(); - pks = Utils.stripWhiteSpace(pks); - WalletEntry we = WalletEntry.create(null,AKeyPair.create(Utils.hexToBytes(pks))); - manager.getWalletPanel().addWalletEntry(we); - manager.switchPanel("Wallet"); - - }); - - JPanel formPanel = new JPanel(); - formPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); - add(formPanel, BorderLayout.NORTH); - GridBagLayout gbl_formPanel = new GridBagLayout(); - gbl_formPanel.columnWidths = new int[] { 156, 347, 0 }; - gbl_formPanel.rowHeights = new int[] { 22, 0, 0, 0, 0 }; - gbl_formPanel.columnWeights = new double[] { 1.0, 1.0, Double.MIN_VALUE }; - gbl_formPanel.rowWeights = new double[] { 0.0, 1.0, 1.0, 1.0, Double.MIN_VALUE }; - formPanel.setLayout(gbl_formPanel); - - JLabel lblNewLabel = new JLabel("Mnenomic Phrase"); - GridBagConstraints gbc_lblNewLabel = new GridBagConstraints(); - gbc_lblNewLabel.anchor = GridBagConstraints.WEST; - gbc_lblNewLabel.anchor = GridBagConstraints.WEST; - gbc_lblNewLabel.insets = new Insets(0, 0, 5, 5); - gbc_lblNewLabel.gridx = 0; - gbc_lblNewLabel.gridy = 0; - formPanel.add(lblNewLabel, gbc_lblNewLabel); - - mnemonicArea = new JTextArea(); - mnemonicArea.setWrapStyleWord(true); - mnemonicArea.setLineWrap(true); - mnemonicArea.setRows(3); - GridBagConstraints gbc_mnemonicArea = new GridBagConstraints(); - gbc_mnemonicArea.fill = GridBagConstraints.HORIZONTAL; - gbc_mnemonicArea.insets = new Insets(0, 0, 5, 0); - gbc_mnemonicArea.gridx = 1; - gbc_mnemonicArea.gridy = 0; - mnemonicArea.setColumns(32); - mnemonicArea.setFont(new Font("Monospaced", Font.BOLD, 13)); - formPanel.add(mnemonicArea, gbc_mnemonicArea); - mnemonicArea.getDocument().addDocumentListener(Toolkit.createDocumentListener(() -> { - if (!mnemonicArea.isFocusOwner()) return; - updateMnemonic(); - })); - - JLabel lblPrivateKey = new JLabel("Private key"); - GridBagConstraints gbc_lblPrivateKey = new GridBagConstraints(); - gbc_lblPrivateKey.anchor = GridBagConstraints.WEST; - gbc_lblPrivateKey.insets = new Insets(0, 0, 5, 5); - gbc_lblPrivateKey.gridx = 0; - gbc_lblPrivateKey.gridy = 1; - formPanel.add(lblPrivateKey, gbc_lblPrivateKey); - - privateKeyArea = new JTextArea(); - privateKeyArea.setFont(new Font("Monospaced", Font.BOLD, 13)); - GridBagConstraints gbc_privateKeyArea = new GridBagConstraints(); - gbc_privateKeyArea.insets = new Insets(0, 0, 5, 0); - gbc_privateKeyArea.fill = GridBagConstraints.HORIZONTAL; - gbc_privateKeyArea.gridx = 1; - gbc_privateKeyArea.gridy = 1; - formPanel.add(privateKeyArea, gbc_privateKeyArea); - privateKeyArea.setText("(mnemonic not ready)"); - privateKeyArea.getDocument().addDocumentListener(Toolkit.createDocumentListener(() -> { - if (!privateKeyArea.isFocusOwner()) return; - updatePrivateKey(); - })); - - JLabel lblPublicKey = new JLabel("Public Key"); - GridBagConstraints gbc_lblPublicKey = new GridBagConstraints(); - gbc_lblPublicKey.anchor = GridBagConstraints.WEST; - gbc_lblPublicKey.insets = new Insets(0, 0, 5, 5); - gbc_lblPublicKey.gridx = 0; - gbc_lblPublicKey.gridy = 2; - formPanel.add(lblPublicKey, gbc_lblPublicKey); - - publicKeyArea = new JTextArea(); - publicKeyArea.setEditable(false); - publicKeyArea.setRows(4); - publicKeyArea.setText("(private key not ready)"); - publicKeyArea.setFont(new Font("Monospaced", Font.BOLD, 13)); - GridBagConstraints gbc_publicKeyArea = new GridBagConstraints(); - gbc_publicKeyArea.insets = new Insets(0, 0, 5, 0); - gbc_publicKeyArea.fill = GridBagConstraints.HORIZONTAL; - gbc_publicKeyArea.gridx = 1; - gbc_publicKeyArea.gridy = 2; - formPanel.add(publicKeyArea, gbc_publicKeyArea); - - JLabel lblNewLabel_1 = new JLabel("Address"); - GridBagConstraints gbc_lblNewLabel_1 = new GridBagConstraints(); - gbc_lblNewLabel_1.anchor = GridBagConstraints.WEST; - gbc_lblNewLabel_1.insets = new Insets(0, 0, 0, 5); - gbc_lblNewLabel_1.gridx = 0; - gbc_lblNewLabel_1.gridy = 3; - formPanel.add(lblNewLabel_1, gbc_lblNewLabel_1); - - addressArea = new JTextArea(); - addressArea.setEditable(false); - addressArea.setFont(new Font("Monospaced", Font.BOLD, 13)); - addressArea.setColumns(40); - GridBagConstraints gbc_addressArea = new GridBagConstraints(); - gbc_addressArea.fill = GridBagConstraints.HORIZONTAL; - gbc_addressArea.gridx = 1; - gbc_addressArea.gridy = 3; - formPanel.add(addressArea, gbc_addressArea); - - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/MessageFormatPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/MessageFormatPanel.java deleted file mode 100644 index 9be83fffe..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/mainpanels/MessageFormatPanel.java +++ /dev/null @@ -1,134 +0,0 @@ -package convex.gui.manager.mainpanels; - -import java.awt.BorderLayout; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSplitPane; -import javax.swing.JTextArea; -import javax.swing.JTextField; - -import convex.core.data.ACell; -import convex.core.data.Blob; -import convex.core.data.Format; -import convex.core.exceptions.ParseException; -import convex.core.lang.Reader; -import convex.core.util.Utils; -import convex.gui.components.ActionPanel; -import convex.gui.manager.PeerGUI; -import convex.gui.utils.Toolkit; - -@SuppressWarnings("serial") -public class MessageFormatPanel extends JPanel { - - final JTextArea dataArea; - final JTextArea messageArea; - private JPanel buttonPanel; - protected PeerGUI manager; - private JButton clearButton; - private JPanel upperPanel; - private JPanel instructionsPanel; - private JLabel lblNewLabel; - private JTextField hashLabel; - - private static String HASHLABEL = "Hash: "; - - public MessageFormatPanel(PeerGUI manager) { - this.manager = manager; - setLayout(new BorderLayout(0, 0)); - - JSplitPane splitPane = new JSplitPane(); - splitPane.setOneTouchExpandable(true); - splitPane.setResizeWeight(0.5); - splitPane.setOrientation(JSplitPane.VERTICAL_SPLIT); - add(splitPane, BorderLayout.CENTER); - - // Top panel component - upperPanel = new JPanel(); - upperPanel.setLayout(new BorderLayout(0, 0)); - dataArea = new JTextArea(); - dataArea.setToolTipText("Enter data objects here"); - upperPanel.add(dataArea, BorderLayout.CENTER); - dataArea.setFont(Toolkit.MONO_FONT); - dataArea.setLineWrap(true); - dataArea.getDocument().addDocumentListener(Toolkit.createDocumentListener(() -> updateData())); - - // Bottom panel component - JPanel lowerPanel = new JPanel(); - lowerPanel.setLayout(new BorderLayout(0, 0)); - - messageArea = new JTextArea(); - messageArea.setToolTipText("Enter binary hex representation here"); - messageArea.setFont(Toolkit.MONO_FONT); - lowerPanel.add(messageArea, BorderLayout.CENTER); - - splitPane.setRightComponent(lowerPanel); - - hashLabel = new JTextField(HASHLABEL); - hashLabel.setToolTipText("Hash code of the data object's serilaised representation = Data Object ID"); - hashLabel.setBorder(null); - hashLabel.setBackground(null); - hashLabel.setFont(Toolkit.MONO_FONT); - lowerPanel.add(hashLabel, BorderLayout.SOUTH); - messageArea.getDocument().addDocumentListener(Toolkit.createDocumentListener(() -> updateMessage())); - - splitPane.setLeftComponent(upperPanel); - - buttonPanel = new ActionPanel(); - add(buttonPanel, BorderLayout.SOUTH); - - clearButton = new JButton("Clear"); - clearButton.setToolTipText("Press to clear the input areas"); - buttonPanel.add(clearButton); - - instructionsPanel = new JPanel(); - add(instructionsPanel, BorderLayout.NORTH); - - lblNewLabel = new JLabel("Use this fine tool to convert data to formatted binary messages, and vice versa"); - instructionsPanel.add(lblNewLabel); - clearButton.addActionListener(e -> { - dataArea.setText(""); - messageArea.setText(""); - }); - } - - private void updateMessage() { - if (!messageArea.isFocusOwner()) return; // prevent mutual recursion - String data = ""; - String msg = messageArea.getText(); - try { - Blob b = Blob.fromHex(Utils.stripWhiteSpace(msg)); - Object o = Format.read(b); - data = Utils.print(o); - hashLabel.setText(HASHLABEL + b.getContentHash().toHexString()); - } catch (ParseException e) { - data = "Unable to interpret message: " + e.getMessage(); - hashLabel.setText(HASHLABEL + " "); - } catch (Exception e) { - data = e.getMessage(); - } - dataArea.setText(data); - } - - private void updateData() { - if (!dataArea.isFocusOwner()) return; // prevent mutual recursion - String msg = ""; - String data = dataArea.getText(); - hashLabel.setText(HASHLABEL + " "); - if (!data.isBlank()) try { - messageArea.setEnabled(false); - ACell o = Reader.read(data); - Blob b = Format.encodedBlob(o); - hashLabel.setText(HASHLABEL + b.getContentHash().toHexString()); - msg = b.toHexString(); - messageArea.setEnabled(true); - } catch (ParseException e) { - msg = e.getMessage(); - } catch (Exception e) { - msg = e.getMessage(); - } - messageArea.setText(msg); - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/PeersListPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/PeersListPanel.java deleted file mode 100644 index 3b704f10f..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/mainpanels/PeersListPanel.java +++ /dev/null @@ -1,157 +0,0 @@ -package convex.gui.manager.mainpanels; - -import java.awt.BorderLayout; -import java.io.File; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.channels.ClosedChannelException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.DefaultListModel; -import javax.swing.JButton; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; - -import convex.api.Convex; -import convex.core.crypto.AKeyPair; -import convex.core.data.Address; -import convex.core.data.Hash; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.store.Stores; -import convex.gui.components.ActionPanel; -import convex.gui.components.PeerComponent; -import convex.gui.components.PeerView; -import convex.gui.components.ScrollyList; -import convex.gui.manager.PeerGUI; -import convex.peer.API; -import convex.peer.Server; -import etch.EtchStore; - -@SuppressWarnings({ "serial", "unused" }) -public class PeersListPanel extends JPanel { - - JPanel peersPanel; - static DefaultListModel peerList = new DefaultListModel(); - - JPanel peerViewPanel; - JScrollPane scrollPane; - - private static final Logger log = LoggerFactory.getLogger(PeersListPanel.class.getName()); - - public void launchAllPeers(PeerGUI manager) { - try { - int N=PeerGUI.KEYPAIRS.size(); - List serverList = API.launchLocalPeers(PeerGUI.KEYPAIRS,PeerGUI.genesisState); - for (Server server: serverList) { - PeerView peer = new PeerView(); - peer.peerServer = server; - // InetSocketAddress sa = server.getHostAddress(); - addPeer(peer); - } - } catch (Exception e) { - if (e instanceof ClosedChannelException) { - // Ignore - } else { - throw(e); - } - - } - - } - - // TODO - public void launchPeer(PeerGUI manager) { - - AKeyPair kp=AKeyPair.generate(); - HashMap config=new HashMap<>(); - config.put(Keywords.KEYPAIR, kp); - config.put(Keywords.STATE, PeerGUI.genesisState); - Server server=API.launchPeer(config); - // server. - - PeerView peer = new PeerView(); - peer.peerServer = server; - addPeer(peer); - } - - public static PeerView getFirst() { - return peerList.elementAt(0); - } - - /** - * Gets a list of all locally operating Servers from the current peer list. - * - * @return List of local PeerView objects - */ - public List getPeerViews() { - ArrayList al = new ArrayList<>(); - int n = peerList.getSize(); - for (int i = 0; i < n; i++) { - PeerView p = peerList.getElementAt(i); - al.add(p); - } - return al; - } - - private void addPeer(PeerView peer) { - peerList.addElement(peer); - } - - /** - * Create the panel. - * @param manager - */ - public PeersListPanel(PeerGUI manager) { - setLayout(new BorderLayout(0, 0)); - - JPanel toolBar = new ActionPanel(); - add(toolBar, BorderLayout.SOUTH); - - JButton btnLaunch = new JButton("Launch!"); - toolBar.add(btnLaunch); - btnLaunch.addActionListener(e -> launchPeer(manager)); - - JButton btnConnect = new JButton("Connect..."); - toolBar.add(btnConnect); - btnConnect.addActionListener(e -> { - String input = JOptionPane.showInputDialog("Enter host address: ", ""); - if (input==null) return; // no result? - - String[] ss = input.split(":"); - String host = ss[0].trim(); - int port = (ss.length > 1) ? Integer.parseInt(ss[1].trim()) : 0; - InetSocketAddress hostAddress = new InetSocketAddress(host, port); - Convex pc; - try { - // TODO: we want to receive anything? - pc = Convex.connect(hostAddress, null,null); - PeerView pv = new PeerView(); - pv.peerConnection = pc; - addPeer(pv); - } catch (Throwable e1) { - JOptionPane.showMessageDialog(this, "Connect failed: " + e1.toString()); - } - - }); - - ScrollyList scrollyList = new ScrollyList(peerList, - peer -> new PeerComponent(manager, peer)); - add(scrollyList, BorderLayout.CENTER); - } - - public void closePeers() { - int n = peerList.getSize(); - for (int i = 0; i < n; i++) { - PeerView p = peerList.getElementAt(i); - p.peerServer.close(); - } - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/WalletPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/WalletPanel.java deleted file mode 100644 index ac092f7ed..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/mainpanels/WalletPanel.java +++ /dev/null @@ -1,60 +0,0 @@ -package convex.gui.manager.mainpanels; - -import java.awt.BorderLayout; - -import javax.swing.DefaultListModel; -import javax.swing.JButton; -import javax.swing.JPanel; -import javax.swing.ListModel; - -import convex.core.crypto.AKeyPair; -import convex.core.crypto.WalletEntry; -import convex.gui.components.ActionPanel; -import convex.gui.components.ScrollyList; -import convex.gui.components.WalletComponent; -import convex.gui.manager.PeerGUI; - -@SuppressWarnings("serial") -public class WalletPanel extends JPanel { - - public static WalletEntry HERO; - - private static DefaultListModel listModel = new DefaultListModel<>();; - ScrollyList walletList; - - public void addWalletEntry(WalletEntry we) { - listModel.addElement(we); - } - - /** - * Create the panel. - */ - public WalletPanel() { - setLayout(new BorderLayout(0, 0)); - - JPanel toolBar = new ActionPanel(); - add(toolBar, BorderLayout.SOUTH); - - // new wallet button - JButton btnNew = new JButton("New"); - toolBar.add(btnNew); - btnNew.addActionListener(e -> { - listModel.addElement(WalletEntry.create(null,AKeyPair.generate())); - }); - - // inital list - HERO = WalletEntry.create(PeerGUI.getUserAddress(0), PeerGUI.getUserKeyPair(0)); - addWalletEntry(HERO); - //addWalletEntry(WalletEntry.create(PeerGUI.getUserAddress(1), PeerGUI.getUserKeyPair(1))); - //addWalletEntry(WalletEntry.create(PeerGUI.getUserAddress(2),PeerGUI.getUserKeyPair(2))); - - // create and add ScrollyList - walletList = new ScrollyList(listModel, we -> new WalletComponent(we)); - add(walletList, BorderLayout.CENTER); - } - - public static ListModel getListModel() { - return listModel; - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/DeployPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/DeployPanel.java deleted file mode 100644 index 17dadb3a4..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/DeployPanel.java +++ /dev/null @@ -1,43 +0,0 @@ -package convex.gui.manager.mainpanels.actors; - -import java.awt.BorderLayout; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; - -import convex.gui.components.AccountChooserPanel; -import convex.gui.components.ActionPanel; - -@SuppressWarnings("serial") -public class DeployPanel extends JPanel { - - private AccountChooserPanel acctChooser; - - public DeployPanel() { - setLayout(new BorderLayout()); - - // =========================================== - // Top panel - acctChooser = new AccountChooserPanel(); - this.add(acctChooser, BorderLayout.NORTH); - - // =========================================== - // Centre panel - JPanel centrePanel = new JPanel(); - this.add(centrePanel, BorderLayout.CENTER); - - centrePanel.add(new JLabel("Tool for deploying standard Actors")); - - // ============================================ - // Action buttons - ActionPanel actionPanel = new ActionPanel(); - add(actionPanel, BorderLayout.SOUTH); - - JButton deployButton = new JButton("Deploy..."); - actionPanel.add(deployButton); - deployButton.addActionListener(e -> { - - }); - } -} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/MarketComponent.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/MarketComponent.java deleted file mode 100644 index 6672c191b..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/MarketComponent.java +++ /dev/null @@ -1,214 +0,0 @@ -package convex.gui.manager.mainpanels.actors; - -import java.awt.BorderLayout; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.GridLayout; -import java.text.DecimalFormat; -import java.util.HashMap; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.SwingConstants; - -import convex.core.State; -import convex.core.crypto.WalletEntry; -import convex.core.data.ACell; -import convex.core.data.AList; -import convex.core.data.AMap; -import convex.core.data.AVector; -import convex.core.data.Address; -import convex.core.data.Keyword; -import convex.core.data.List; -import convex.core.data.Lists; -import convex.core.data.Symbol; -import convex.core.data.prim.CVMLong; -import convex.core.lang.Context; -import convex.core.lang.RT; -import convex.core.lang.Symbols; -import convex.gui.components.BaseListComponent; -import convex.gui.components.CodeLabel; -import convex.gui.components.DefaultReceiveAction; -import convex.gui.manager.PeerGUI; -import convex.gui.utils.Toolkit; - -@SuppressWarnings("serial") -public class MarketComponent extends BaseListComponent { - - private Address address; - AVector outcomes; - - HashMap probLabels = new HashMap<>(); // probabilities - HashMap tsLabels = new HashMap<>(); // total stake - HashMap osLabels = new HashMap<>(); // owned stake - - CodeLabel title; - private int numOutcomes; - private MarketsPanel marketsPanel; - - @SuppressWarnings("unchecked") - public MarketComponent(MarketsPanel marketsPanel, Address addr) { - this.marketsPanel = marketsPanel; - this.address = addr; - State state = PeerGUI.getLatestState(); - - // prediction market data - AMap pmEnv = state.getEnvironment(addr); - outcomes = RT.keys(pmEnv.get(Symbol.create("totals"))); - - numOutcomes = outcomes.size(); - - // oracle data - Address oracleAddress = (Address) pmEnv.get(Symbol.create("oracle")); - if (oracleAddress == null) throw new Error("No oracle symbol in environment?"); - Object key = pmEnv.get(Symbol.create("oracle-key")); - - AMap oracleEnv = state.getEnvironment(oracleAddress); - AMap fullList = (AMap) oracleEnv.get(Symbol.create("full-list")); - AMap oracleData = (AMap) fullList.get(key); - if (oracleData == null) throw new Error("No oracle data for key?"); - - // Layout - setLayout(new BorderLayout()); - - // Top label - String oName = RT.jvm( oracleData.get(Keyword.create("desc"))); - if (oName == null) oName = "Nameless Oracle"; - title = new CodeLabel(oName); - title.setFont(Toolkit.MONO_FONT); - add(title, BorderLayout.NORTH); - - // Centre panel - JPanel jp = new JPanel(); - add(jp, BorderLayout.CENTER); - - jp.setLayout(new GridBagLayout()); - GridBagConstraints gbc = new GridBagConstraints(); - gbc.ipadx = 10; - gbc.weightx = 1.0; - gbc.weighty = 1.0; - - gbc.gridx = 0; - jp.add(new JLabel("Outcome")); - gbc.gridx = 1; - jp.add(new JLabel("Probability")); - gbc.gridx = 2; - jp.add(new JLabel("Total Stake")); - gbc.gridx = 3; - jp.add(new JLabel("Owned Stake")); - gbc.gridx = 4; - jp.add(new JLabel("Actions")); - for (int i = 0; i < numOutcomes; i++) { - Object outcome = outcomes.get(i); - - gbc.gridy = 1 + i; - gbc.gridx = 0; - JLabel oLabel = new JLabel(outcome.toString() + " "); - oLabel.setHorizontalAlignment(SwingConstants.LEFT); - jp.add(oLabel, gbc); - - gbc.gridx = 1; - JLabel pLabel = new JLabel("??"); - jp.add(pLabel, gbc); - probLabels.put(outcome, pLabel); - - gbc.gridx = 2; - JLabel tsLabel = new JLabel("0"); - jp.add(tsLabel, gbc); - tsLabels.put(outcome, tsLabel); - - gbc.gridx = 3; - JLabel osLabel = new JLabel("0"); - jp.add(osLabel, gbc); - osLabels.put(outcome, osLabel); - - gbc.gridx = 4; - JPanel bp = new JPanel(); - bp.setLayout(new GridLayout(0, 2)); - JButton buyButton = new JButton("Buy"); - buyButton.addActionListener(e -> buy(outcome)); - bp.add(buyButton); - JButton sellButton = new JButton("Sell"); - sellButton.addActionListener(e -> sell(outcome)); - bp.add(sellButton); - jp.add(bp, gbc); - } - - // Top label - add(new CodeLabel("Market Address: " + address.toString()), BorderLayout.SOUTH); - - // state updates - updateStatus(state); - - PeerGUI.getStateModel().addPropertyChangeListener(e -> { - State s = (State) e.getNewValue(); - updateStatus(s); - }); - - marketsPanel.acctChooser.addressCombo.addActionListener(e -> { - State s = PeerGUI.getLatestState(); - updateStatus(s); - }); - } - - private void sell(Object outcome) { - changeStake(outcome, -1000000); - } - - private void buy(Object outcome) { - changeStake(outcome, 1000000); - } - - private void changeStake(Object outcome, long delta) { - State state = PeerGUI.getLatestState(); - Long stk = getStake(state, outcome); - if (stk == null) stk = 0L; - long newStake = Math.max(0L, stk + delta); - long offer = Math.max(0, delta); // covers stake increase for sure? - - WalletEntry we = marketsPanel.acctChooser.getWalletEntry(); - AList cc = Lists.of(Symbol.create("stake"), outcome, newStake); - AList cmd = List.of(Symbols.CALL, address, offer, cc); - PeerGUI.execute(we, cmd).thenAcceptAsync(new DefaultReceiveAction(marketsPanel)); - } - - static DecimalFormat probFormatter = new DecimalFormat("0.0"); - - private void updateStatus(State state) { - try { - Address caller = marketsPanel.acctChooser.getSelectedAddress(); - Context ctx = Context.createFake(state, caller); // fake for caller - for (int i = 0; i < numOutcomes; i++) { - ACell outcome = outcomes.get(i); - - double p = RT.jvm(ctx.actorCall(address, 0, "price", outcome).getResult()); - if (Double.isNaN(p)) p = 1.0 / numOutcomes; - String prob = probFormatter.format(p * 100.0) + "%"; - probLabels.get(outcome).setText(prob); - - Long ts = RT.jvm( ctx.actorCall(address, 0, "totals", outcome).getResult()); - String totalStake = ts.toString(); - tsLabels.get(outcome).setText(totalStake); - - @SuppressWarnings("unchecked") - AMap stks = (AMap) ctx.actorCall(address, 0, "stakes", outcome) - .getResult(); - CVMLong stk = stks.get(caller); - String ownStake = (stk == null) ? "0" : stk.toString(); - osLabels.get(outcome).setText(ownStake); - } - } catch (Throwable t) { - t.printStackTrace(); - } - } - - private Long getStake(State state, Object outcome) { - Address caller = marketsPanel.acctChooser.getSelectedAddress(); - Context ctx = Context.createFake(state, caller); - @SuppressWarnings("unchecked") - AMap stks = (AMap) ctx.actorCall(address, 0, "stakes", RT.cvm(outcome)).getResult(); - return stks.get(caller).longValue(); - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/MarketsPanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/MarketsPanel.java deleted file mode 100644 index 97151f897..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/MarketsPanel.java +++ /dev/null @@ -1,53 +0,0 @@ -package convex.gui.manager.mainpanels.actors; - -import java.awt.BorderLayout; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.DefaultListModel; -import javax.swing.JButton; -import javax.swing.JPanel; - -import convex.core.data.Address; -import convex.gui.components.AccountChooserPanel; -import convex.gui.components.ActionPanel; -import convex.gui.components.ScrollyList; -import convex.gui.manager.PeerGUI; - -/** - * Panel displaying current prediction markets - */ -@SuppressWarnings("serial") -public class MarketsPanel extends JPanel { - - private static final Logger log = LoggerFactory.getLogger(MarketsPanel.class.getName()); - AccountChooserPanel acctChooser; - - static DefaultListModel
marketList = new DefaultListModel
(); - - public MarketsPanel(PeerGUI manager) { - this.setLayout(new BorderLayout()); - - // =========================================== - // Top panel - acctChooser = new AccountChooserPanel(); - this.add(acctChooser, BorderLayout.NORTH); - - // =========================================== - // Central scrolling list - ScrollyList
scrollyList = new ScrollyList
(marketList, - addr -> new MarketComponent(this, addr)); - add(scrollyList, BorderLayout.CENTER); - - // ============================================ - // Action buttons - ActionPanel actionPanel = new ActionPanel(); - add(actionPanel, BorderLayout.SOUTH); - - JButton scanButton = new JButton("Scan"); - actionPanel.add(scanButton); - scanButton.addActionListener(e -> { - log.info("Scanning for prediction market Actors..."); - }); - } -} diff --git a/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/OraclePanel.java b/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/OraclePanel.java deleted file mode 100644 index 85af3c8d3..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/mainpanels/actors/OraclePanel.java +++ /dev/null @@ -1,193 +0,0 @@ -package convex.gui.manager.mainpanels.actors; - -import java.awt.BorderLayout; -import java.util.function.Consumer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTable; -import javax.swing.table.DefaultTableCellRenderer; - -import convex.core.Result; -import convex.core.State; -import convex.core.data.ACell; -import convex.core.data.Address; -import convex.core.data.MapEntry; -import convex.core.lang.RT; -import convex.core.lang.Reader; -import convex.core.util.Utils; -import convex.gui.components.ActionPanel; -import convex.gui.components.CodeLabel; -import convex.gui.components.DefaultReceiveAction; -import convex.gui.components.Toast; -import convex.gui.components.models.OracleTableModel; -import convex.gui.manager.PeerGUI; -import convex.gui.manager.mainpanels.WalletPanel; -import convex.gui.utils.Toolkit; - -@SuppressWarnings("serial") -public class OraclePanel extends JPanel { - - public static final Logger log = LoggerFactory.getLogger(OraclePanel.class.getName()); - - Address oracleAddress = PeerGUI.getLatestState().lookupCNS("convex.trusted-oracle"); - Address oracleActorAddress = PeerGUI.getLatestState().lookupCNS("convex.trusted-oracle.actor"); - - OracleTableModel tableModel = new OracleTableModel(PeerGUI.getLatestState(), oracleActorAddress); - JTable table = new JTable(tableModel); - - JScrollPane scrollPane = new JScrollPane(table);; - - long key = 1; - - public OraclePanel() { - this.setLayout(new BorderLayout()); - - // =========================================== - // Top label - add(new CodeLabel("Oracle at address: " + oracleAddress), BorderLayout.NORTH); - - // =========================================== - // Central table - PeerGUI.getStateModel().addPropertyChangeListener(pc -> { - State newState = (State) pc.getNewValue(); - tableModel.setState(newState); - }); - - // Column layouts - DefaultTableCellRenderer leftRenderer = new DefaultTableCellRenderer(); - leftRenderer.setHorizontalAlignment(JLabel.LEFT); - table.getColumnModel().getColumn(0).setCellRenderer(leftRenderer); - table.getColumnModel().getColumn(0).setPreferredWidth(80); - table.getColumnModel().getColumn(1).setCellRenderer(leftRenderer); - table.getColumnModel().getColumn(1).setPreferredWidth(300); - table.getColumnModel().getColumn(2).setCellRenderer(leftRenderer); - table.getColumnModel().getColumn(2).setPreferredWidth(80); - table.getColumnModel().getColumn(3).setCellRenderer(leftRenderer); - table.getColumnModel().getColumn(3).setPreferredWidth(300); - - // fonts - table.setFont(Toolkit.SMALL_MONO_FONT); - table.getTableHeader().setFont(Toolkit.SMALL_MONO_FONT); - - table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); // useful in scroll pane - scrollPane.getViewport().setBackground(null); - add(scrollPane, BorderLayout.CENTER); - - // ============================================ - // Action buttons - ActionPanel actionPanel = new ActionPanel(); - add(actionPanel, BorderLayout.SOUTH); - - JButton createButton = new JButton("Create..."); - actionPanel.add(createButton); - createButton.addActionListener(e -> { - String desc = JOptionPane.showInputDialog(this, "Enter Oracle description as plain text:"); - if ((desc == null) || (desc.isBlank())) return; - - ACell code = Reader.read("(call " + oracleAddress + " " + "(register " + (key++) - + " {:desc \"" + desc + "\" :trust #{*address*}}))"); - execute(code); - }); - - JButton finaliseButton = new JButton("Finalise..."); - actionPanel.add(finaliseButton); - finaliseButton.addActionListener(e -> { - String value = JOptionPane.showInputDialog(this, "Enter final value:"); - if ((value == null) || (value.isBlank())) return; - int ix = table.getSelectedRow(); - if (ix < 0) return; - MapEntry me = tableModel.getList().entryAt(ix); - - ACell code = Reader.read( - "(call " + oracleAddress + " " + "(provide " + me.getKey() + " " + value + "))"); - execute(code); - }); - - JButton makeMarketButton = new JButton("Make Market"); - actionPanel.add(makeMarketButton); - makeMarketButton.addActionListener(e -> { - int ix = table.getSelectedRow(); - if (ix < 0) return; - MapEntry me = tableModel.getList().entryAt(ix); - Object key = me.getKey(); - log.info("Making market: " + key); - - String opts = JOptionPane - .showInputDialog("Enter a list of possible values (forms, may separate with commas)"); - if ((opts == null) || opts.isBlank()) { - Toast.display(scrollPane, "Prediction market making cancelled", Toast.INFO); - return; - } - String outcomeString = "[" + opts + "]"; - - String actorCode; - try { - actorCode = Utils.readResourceAsString("actors/prediction-market.con"); - String source = "(let [pmc " + actorCode + " ] " + "(deploy (pmc " + " 0x" - + oracleAddress.toString() + " " + key + " " + outcomeString + ")))"; - ACell code = Reader.read(source); - PeerGUI.execute(WalletPanel.HERO, code).thenAcceptAsync(createMarketAction); - } catch (Exception e1) { - e1.printStackTrace(); - } - }); - } - - private void execute(ACell code) { - PeerGUI.execute(WalletPanel.HERO, code).thenAcceptAsync(receiveAction); - } - - private final Consumer createMarketAction = new Consumer() { - protected void handleResult(Object m) { - if (m instanceof Address) { - try { - Thread.sleep(200); - } catch (InterruptedException e) { - // ignore - } - Address addr = (Address) m; - MarketsPanel.marketList.addElement(addr); - showResult("Prediction market deployed: " + addr); - } else { - String resultString = "Expected Address but got: " + m; - log.warn(resultString); - Toast.display(scrollPane, resultString, Toast.FAIL); - } - } - - protected void handleError(long id, Object code, Object msg) { - showError(code,msg); - } - - @Override - public void accept(Result t) { - if (t.isError()) { - handleError(RT.jvm(t.getID()),t.getErrorCode(),t.getValue()); - } else { - handleResult(t.getValue()); - } - } - - }; - - private final DefaultReceiveAction receiveAction = new DefaultReceiveAction(scrollPane); - - private void showError(Object code, Object msg) { - String resultString = "Error executing transaction: " + code + " "+msg; - log.info(resultString); - Toast.display(scrollPane, resultString, Toast.FAIL); - } - - private void showResult(Object v) { - String resultString = "Transaction executed successfully\n" + "Result: " + Utils.toString(v); - log.info(resultString); - Toast.display(scrollPane, resultString, Toast.SUCCESS); - } -} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/BaseWindow.java b/convex-gui/src/main/java/convex/gui/manager/windows/BaseWindow.java deleted file mode 100644 index 92c8a84a9..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/windows/BaseWindow.java +++ /dev/null @@ -1,32 +0,0 @@ -package convex.gui.manager.windows; - -import java.awt.BorderLayout; - -import javax.swing.JFrame; -import javax.swing.JPanel; - -import convex.gui.manager.PeerGUI; - -@SuppressWarnings("serial") -public abstract class BaseWindow extends JPanel { - - protected final PeerGUI manager; - - public BaseWindow(PeerGUI manager) { - super(); - this.manager = manager; - setLayout(new BorderLayout()); - } - - public abstract String getTitle(); - - public JFrame launch() { - JFrame f = new JFrame(getTitle()); - f.getContentPane().add(this); - f.pack(); - f.setVisible(true); - f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); - f.setLocationRelativeTo(PeerGUI.getFrame()); - return f; - } -} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorInfoPanel.java b/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorInfoPanel.java deleted file mode 100644 index f2f740f9e..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorInfoPanel.java +++ /dev/null @@ -1,51 +0,0 @@ -package convex.gui.manager.windows.actor; - -import java.awt.BorderLayout; -import java.awt.Dimension; - -import javax.swing.JPanel; -import javax.swing.JTextArea; - -import convex.core.State; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.gui.manager.PeerGUI; -import convex.gui.utils.Toolkit; - -@SuppressWarnings("serial") -public class ActorInfoPanel extends JPanel { - - protected PeerGUI manager; - protected Address actor; - protected JTextArea infoArea; - - public ActorInfoPanel(PeerGUI manager, Address contract) { - this.manager = manager; - this.actor = contract; - setLayout(new BorderLayout(0, 0)); - - this.setPreferredSize(new Dimension(600, 400)); - - infoArea = new JTextArea(); - add(infoArea, BorderLayout.CENTER); - infoArea.setBackground(null); - infoArea.setFont(Toolkit.SMALL_MONO_FONT); - - PeerGUI.getStateModel().addPropertyChangeListener(e -> { - updateInfo((State) e.getNewValue()); - }); - updateInfo(PeerGUI.getLatestState()); - } - - private void updateInfo(State latestState) { - StringBuilder sb = new StringBuilder(); - AccountStatus as = latestState.getAccount(actor); - - sb.append("Actor Address: " + actor.toHexString() + "\n"); - sb.append("Actor Balance: " + as.getBalance() + "\n"); - sb.append("\n"); - - infoArea.setText(sb.toString()); - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorInvokePanel.java b/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorInvokePanel.java deleted file mode 100644 index 36b8e6ff0..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorInvokePanel.java +++ /dev/null @@ -1,44 +0,0 @@ -package convex.gui.manager.windows.actor; - -import java.awt.BorderLayout; - -import javax.swing.DefaultListModel; -import javax.swing.JPanel; - -import convex.core.data.ASet; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.core.data.Symbol; -import convex.gui.components.AccountChooserPanel; -import convex.gui.components.ScrollyList; -import convex.gui.manager.PeerGUI; - -@SuppressWarnings("serial") -public class ActorInvokePanel extends JPanel { - - protected PeerGUI manager; - protected Address contract; - AccountChooserPanel execPanel = new AccountChooserPanel(); - - DefaultListModel exportList = new DefaultListModel(); - - public ActorInvokePanel(PeerGUI manager, Address contract) { - this.manager = manager; - this.contract = contract; - - setLayout(new BorderLayout()); - - AccountStatus as = PeerGUI.getLatestState().getAccount(contract); - ASet exports = as.getCallableFunctions(); - for (Symbol s : exports) { - exportList.addElement(s); - } - - ScrollyList scrollyList = new ScrollyList(exportList, - sym -> new SmartOpComponent(this, contract, sym)); - add(scrollyList, BorderLayout.CENTER); - - add(execPanel, BorderLayout.NORTH); - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorWindow.java b/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorWindow.java deleted file mode 100644 index 4bd32d578..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/windows/actor/ActorWindow.java +++ /dev/null @@ -1,44 +0,0 @@ -package convex.gui.manager.windows.actor; - -import java.awt.BorderLayout; - -import javax.swing.JTabbedPane; - -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.gui.manager.PeerGUI; -import convex.gui.manager.windows.BaseWindow; -import convex.gui.manager.windows.state.StateTreePanel; - -@SuppressWarnings("serial") -public class ActorWindow extends BaseWindow { - Address contract; - - JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.TOP); - - public ActorWindow(PeerGUI manager, Address contract) { - super(manager); - this.contract = contract; - AccountStatus as = PeerGUI.getLatestState().getAccount(contract); - - PeerGUI.getStateModel().addPropertyChangeListener(e -> { - - }); - - add(tabbedPane, BorderLayout.CENTER); - - tabbedPane.add("Overview", new ActorInfoPanel(manager, contract)); - tabbedPane.add("Environment", new StateTreePanel(as.getEnvironment())); - tabbedPane.add("Operations", new ActorInvokePanel(manager, contract)); - } - - @Override - public String getTitle() { - try { - return "Contract view - " + contract.toHexString(); - } catch (Exception e) { - return "Contract view - Unknown"; - } - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/actor/ArgBox.java b/convex-gui/src/main/java/convex/gui/manager/windows/actor/ArgBox.java deleted file mode 100644 index e41ded4fc..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/windows/actor/ArgBox.java +++ /dev/null @@ -1,13 +0,0 @@ -package convex.gui.manager.windows.actor; - -import javax.swing.JTextField; - -import convex.gui.utils.Toolkit; - -@SuppressWarnings("serial") -public class ArgBox extends JTextField { - - public ArgBox() { - setFont(Toolkit.SMALL_MONO_FONT); - } -} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/actor/ParamLabel.java b/convex-gui/src/main/java/convex/gui/manager/windows/actor/ParamLabel.java deleted file mode 100644 index 66a7393a9..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/windows/actor/ParamLabel.java +++ /dev/null @@ -1,20 +0,0 @@ -package convex.gui.manager.windows.actor; - -import javax.swing.JLabel; -import javax.swing.SwingConstants; - -import convex.gui.utils.Toolkit; - -/** - * Generic label component for displaying code - */ -@SuppressWarnings("serial") -public class ParamLabel extends JLabel { - - public ParamLabel(String text) { - super(" " + text + " "); - this.setHorizontalAlignment(SwingConstants.RIGHT); - this.setFont(Toolkit.SMALL_MONO_FONT); - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/actor/SmartOpComponent.java b/convex-gui/src/main/java/convex/gui/manager/windows/actor/SmartOpComponent.java deleted file mode 100644 index 509689e09..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/windows/actor/SmartOpComponent.java +++ /dev/null @@ -1,177 +0,0 @@ -package convex.gui.manager.windows.actor; - -import java.awt.BorderLayout; -import java.awt.FlowLayout; -import java.awt.GridLayout; -import java.net.InetSocketAddress; -import java.util.HashMap; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JTextField; - -import convex.api.Convex; -import convex.core.Result; -import convex.core.crypto.WalletEntry; -import convex.core.data.ACell; -import convex.core.data.AList; -import convex.core.data.AVector; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.core.data.Lists; -import convex.core.data.Symbol; -import convex.core.data.Vectors; -import convex.core.lang.AFn; -import convex.core.lang.RT; -import convex.core.lang.Reader; -import convex.core.lang.Symbols; -import convex.core.lang.impl.Fn; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Invoke; -import convex.gui.components.AccountChooserPanel; -import convex.gui.components.BaseListComponent; -import convex.gui.components.CodeLabel; -import convex.gui.components.Toast; -import convex.gui.manager.PeerGUI; -import convex.gui.utils.Toolkit; - - -@SuppressWarnings("serial") -public class SmartOpComponent extends BaseListComponent { - - protected ActorInvokePanel parent; - protected Symbol sym; - int paramCount; - - /** - * Fields for each argument by position. - * - * Null entry used for funds offer - */ - private HashMap paramFields = new HashMap<>(); - - private static final Logger log = LoggerFactory.getLogger(SmartOpComponent.class.getName()); - - public SmartOpComponent(ActorInvokePanel parent, Address contract, Symbol sym) { - this.parent = parent; - this.sym = sym; - - setFont(Toolkit.SMALL_MONO_FONT); - setLayout(new BorderLayout(0, 0)); - - CodeLabel opName = new CodeLabel(sym.toString()); - opName.setFont(Toolkit.MONO_FONT); - add(opName, BorderLayout.NORTH); - - JPanel paramPanel = new JPanel(); - paramPanel.setLayout(new GridLayout(0, 3, 4, 4)); // 3 columns, small hgap and vgap - - AccountStatus as = PeerGUI.getLatestState().getAccount(contract); - - AFn fn = as.getCallableFunction(sym); - - // Function might be a map or set - AVector params = (fn instanceof Fn) ? ((Fn) fn).getParams() - : Vectors.of(Symbols.FOO); - paramCount = params.size(); - - for (int i = 0; i < paramCount; i++) { - ACell paramSym = params.get(i); - paramPanel.add(new ParamLabel(RT.str(paramSym))); - JTextField argBox = new ArgBox(); - paramPanel.add(argBox); - paramFields.put(i, argBox); - paramPanel.add(new JLabel("")); // TODO: descriptions? - } - paramPanel.add(new ParamLabel("")); - JTextField offerBox = new ArgBox(); - paramFields.put(null, offerBox); - paramPanel.add(offerBox); - paramPanel.add(new JLabel("Offer funds (0 or blank for no offer)")); - - add(paramPanel, BorderLayout.CENTER); - - JPanel aPanel = new JPanel(); - aPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); - JButton execButton = new JButton("Execute"); - aPanel.add(execButton); - execButton.addActionListener(e -> execute()); - - add(aPanel, BorderLayout.SOUTH); - - } - - private void execute() { - InetSocketAddress addr = PeerGUI.getDefaultPeer().getHostAddress(); - - AVector args = Vectors.empty(); - for (int i = 0; i < paramCount; i++) { - JTextField argBox = paramFields.get(i); - String s = argBox.getText(); - ACell arg = (s.isBlank()) ? null : Reader.read(s); - args = args.conj(arg); - } - String offerString = paramFields.get(null).getText(); - Long offer = (offerString.isBlank()) ? null : Long.parseLong(offerString.trim()); - - AList rest = Lists.of(Lists.create(args).cons(sym)); // (foo 1 2 3) - if (offer != null) { - rest = rest.cons(RT.cvm(offer)); - } - - try { - ACell message = RT.cons(Symbols.CALL, parent.contract, rest); - - AccountChooserPanel execPanel = parent.execPanel; - WalletEntry we = execPanel.getWalletEntry(); - Address myAddress=we.getAddress(); - - // connect to Peer as a client - Convex peerConnection = Convex.connect(addr, we.getAddress(),we.getKeyPair()); - - String mode = execPanel.getMode(); - Result r=null; - if (mode.equals("Query")) { - r=peerConnection.querySync(message); - } else if (mode.equals("Transact")) { - if (we.isLocked()) { - JOptionPane.showMessageDialog(this, - "Please select an unlocked wallet address to use for transactions before sending"); - return; - } - - ATransaction trans = Invoke.create(myAddress,-1, message); - r = peerConnection.transactSync(trans); - } else { - throw new Error("Unexpected mode: "+mode); - } - if (r.isError()) { - showError(r.getErrorCode(),r.getValue()); - } else { - showResult(r.getValue()); - } - - } catch (Throwable e) { - log.warn(e.getMessage()); - Toast.display(parent, "Unexpected Error: "+e.getMessage(), Toast.FAIL); - - } - - } - - private void showError(Object code, Object msg) { - String resultString = "Error executing transaction: " + code + " "+msg; - log.info(resultString); - Toast.display(parent, resultString, Toast.FAIL); - } - - private void showResult(Object v) { - String resultString = "Transaction executed successfully"; - log.info(resultString); - Toast.display(parent, resultString, Toast.SUCCESS); - } -} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/etch/EtchWindow.java b/convex-gui/src/main/java/convex/gui/manager/windows/etch/EtchWindow.java deleted file mode 100644 index 1d2593a3a..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/windows/etch/EtchWindow.java +++ /dev/null @@ -1,47 +0,0 @@ -package convex.gui.manager.windows.etch; - -import java.awt.BorderLayout; - -import javax.swing.JTabbedPane; - -import convex.gui.components.PeerComponent; -import convex.gui.components.PeerView; -import convex.gui.manager.PeerGUI; -import convex.gui.manager.windows.BaseWindow; -import etch.EtchStore; - -@SuppressWarnings("serial") -public class EtchWindow extends BaseWindow { - EtchStore store; - PeerView peer; - - - - public EtchStore getEtchStore() { - return store; - } - - JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.TOP); - - public EtchWindow(PeerGUI manager, PeerView peer) { - super(manager); - this.peer=peer; - this.store=(EtchStore) peer.peerServer.getStore(); - - PeerComponent pcom=new PeerComponent(manager,peer); - add(pcom, BorderLayout.NORTH); - - add(tabbedPane, BorderLayout.CENTER); - } - - @Override - public String getTitle() { - try { - return "Storage view - "+peer.getHostAddress(); - } - catch (Exception e) { - return "Storage view - Unknown"; - } - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/peer/PeerWindow.java b/convex-gui/src/main/java/convex/gui/manager/windows/peer/PeerWindow.java deleted file mode 100644 index ebd058b60..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/windows/peer/PeerWindow.java +++ /dev/null @@ -1,44 +0,0 @@ -package convex.gui.manager.windows.peer; - -import java.awt.BorderLayout; - -import javax.swing.JTabbedPane; - -import convex.gui.components.PeerComponent; -import convex.gui.components.PeerView; -import convex.gui.manager.PeerGUI; -import convex.gui.manager.windows.BaseWindow; - -@SuppressWarnings("serial") -public class PeerWindow extends BaseWindow { - PeerView peer; - - public PeerView getPeerView() { - return peer; - } - - JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.TOP); - - public PeerWindow(PeerGUI manager, PeerView peer) { - super(manager); - this.peer = peer; - - add(tabbedPane, BorderLayout.CENTER); - tabbedPane.addTab("REPL", null, new REPLPanel(this.getPeerView()), null); - tabbedPane.addTab("Stress", null, new StressPanel(this.getPeerView()), null); - - PeerComponent pcom = new PeerComponent(manager, peer); - add(pcom, BorderLayout.NORTH); - - } - - @Override - public String getTitle() { - try { - return "Peer view - " + peer.peerConnection.getRemoteAddress(); - } catch (Exception e) { - return "Peer view - Unknown"; - } - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/peer/REPLPanel.java b/convex-gui/src/main/java/convex/gui/manager/windows/peer/REPLPanel.java deleted file mode 100644 index f72bc9cd0..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/windows/peer/REPLPanel.java +++ /dev/null @@ -1,296 +0,0 @@ -package convex.gui.manager.windows.peer; - -import java.awt.BorderLayout; -import java.awt.Font; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; -import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -import javax.swing.JButton; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JSplitPane; -import javax.swing.JTextArea; -import javax.swing.SwingUtilities; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; - -import convex.api.Convex; -import convex.core.Result; -import convex.core.crypto.AKeyPair; -import convex.core.crypto.WalletEntry; -import convex.core.data.ACell; -import convex.core.data.AList; -import convex.core.data.AString; -import convex.core.data.AVector; -import convex.core.data.Address; -import convex.core.lang.Reader; -import convex.core.lang.Symbols; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Invoke; -import convex.core.util.Utils; -import convex.gui.components.AccountChooserPanel; -import convex.gui.components.ActionPanel; -import convex.gui.components.PeerView; - -@SuppressWarnings("serial") -public class REPLPanel extends JPanel { - - JTextArea inputArea; - JTextArea outputArea; - private JButton btnClear; - private JButton btnInfo; - - private ArrayList history=new ArrayList<>(); - private int historyPosition=0; - - private InputListener inputListener=new InputListener(); - - - private JPanel panel_1; - - private AccountChooserPanel execPanel = new AccountChooserPanel(); - - private final Convex convex; - - private static final Logger log = LoggerFactory.getLogger(REPLPanel.class.getName()); - - public void setInput(String s) { - inputArea.setText(s); - } - - @Override - public void setVisible(boolean value) { - super.setVisible(value); - if (value) inputArea.requestFocusInWindow(); - } - - protected void handleResult(Result r) { - if (r.isError()) { - handleError(r.getErrorCode(),r.getValue(),r.getTrace()); - } else { - handleResult((Object)r.getValue()); - } - } - - protected void handleResult(Object m) { - outputArea.append(" => " + m + "\n"); - outputArea.setCaretPosition(outputArea.getDocument().getLength()); - } - - protected void handleError(Object code, Object msg, AVector trace) { - outputArea.append(" Exception: " + code + " "+ msg+"\n"); - if (trace!=null) for (AString s: trace) { - outputArea.append(" - "+s.toString()+"\n"); - } - } - - /** - * Create the panel. - * @param peerView - */ - public REPLPanel(PeerView peerView) { - setLayout(new BorderLayout(0, 0)); - - InetSocketAddress addr = peerView.getHostAddress(); - if (addr == null) { - JOptionPane.showMessageDialog(this, "Error: peer shut down already?"); - throw new Error("Connect fail, no remote address"); - } - try { - // Connect to peer as a client - convex = Convex.connect(addr, getAddress(),getKeyPair()); - } catch (Exception ex) { - throw Utils.sneakyThrow(ex); - } - - JSplitPane splitPane = new JSplitPane(); - splitPane.setResizeWeight(0.8); - splitPane.setOneTouchExpandable(true); - splitPane.setOrientation(JSplitPane.VERTICAL_SPLIT); - add(splitPane, BorderLayout.CENTER); - - outputArea = new JTextArea(); - outputArea.setRows(15); - outputArea.setEditable(false); - outputArea.setLineWrap(true); - outputArea.setFont(new Font("Monospaced", Font.PLAIN, 13)); - //DefaultCaret caret = (DefaultCaret)(outputArea.getCaret()); - //caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); - splitPane.setLeftComponent(new JScrollPane(outputArea)); - - inputArea = new JTextArea(); - inputArea.setRows(5); - inputArea.setFont(new Font("Monospaced", Font.PLAIN, 13)); - inputArea.getDocument().addDocumentListener(inputListener); - inputArea.addKeyListener(inputListener); - splitPane.setRightComponent(new JScrollPane(inputArea)); - - // stop ctrl+arrow losing focus - setFocusTraversalKeysEnabled(false); - inputArea.setFocusTraversalKeysEnabled(false); - - - add(execPanel, BorderLayout.NORTH); - - panel_1 = new ActionPanel(); - add(panel_1, BorderLayout.SOUTH); - - btnClear = new JButton("Clear"); - panel_1.add(btnClear); - btnClear.addActionListener(e -> outputArea.setText("")); - - btnInfo = new JButton("Connection Info"); - panel_1.add(btnInfo); - btnInfo.addActionListener(e -> { - String infoString = ""; - infoString += "Remote host: " + convex.getRemoteAddress() + "\n"; - infoString += "Sequence: " + convex.getSequence() + "\n"; - infoString += "Connection Account: " + convex.getAddress() + "\n"; - - JOptionPane.showMessageDialog(this, infoString); - }); - - // Get initial focus in REPL input area - addComponentListener(new ComponentAdapter() { - public void componentShown(ComponentEvent ce) { - inputArea.requestFocusInWindow(); - } - }); - } - - private AKeyPair getKeyPair() { - WalletEntry we=execPanel.getWalletEntry(); - if (we==null) return null; - return we.getKeyPair(); - } - - private Address getAddress() { - WalletEntry we=execPanel.getWalletEntry(); - return we.getAddress(); - } - - private void sendMessage(String s) { - if (s.isBlank()) return; - - history.add(s); - historyPosition=history.size(); - - SwingUtilities.invokeLater(() -> { - outputArea.append(s); - outputArea.append("\n"); - try { - AList forms = Reader.readAll(s); - ACell message = (forms.count()==1)?forms.get(0):forms.cons(Symbols.DO); - Future future; - String mode = execPanel.getMode(); - if (mode.equals("Query")) { - AKeyPair kp=getKeyPair(); - if (kp == null) { - future = convex.query(message,null); - } else { - future = convex.query(message, getAddress()); - } - } else if (mode.equals("Transact")) { - WalletEntry we = execPanel.getWalletEntry(); - if ((we == null) || (we.isLocked())) { - JOptionPane.showMessageDialog(this, - "Please select an address to use for transactions before sending"); - return; - } - Address address = getAddress(); - ATransaction trans = Invoke.create(address,-1, message); - future = convex.transact(trans); - } else { - throw new Error("Unrecognosed REPL mode: " + mode); - } - log.trace("Sent message"); - - handleResult(future.get(5000, TimeUnit.MILLISECONDS)); - } catch (TimeoutException t) { - outputArea.append(" TIMEOUT waiting for result"); - } catch (Throwable t) { - outputArea.append(" SEND ERROR: "); - outputArea.append(t.getMessage() + "\n"); - t.printStackTrace(); - } - inputArea.setText(""); - }); - } - - /** - * Listener to detect returns at the end of the input box => send message - */ - private class InputListener implements DocumentListener, KeyListener { - - @Override - public void insertUpdate(DocumentEvent e) { - int len = e.getLength(); - int off = e.getOffset(); - String s = inputArea.getText(); - if ((len == 1) && (len + off == s.length()) && (s.charAt(off) == '\n')) { - sendMessage(s.trim()); - } - } - - @Override - public void removeUpdate(DocumentEvent e) { - // nothing special - } - - @Override - public void changedUpdate(DocumentEvent e) { - // nothing special - } - - @Override - public void keyTyped(KeyEvent e) { - - } - - @Override - public void keyPressed(KeyEvent e) { - // System.out.println(e); - if (e.isControlDown()) { - int code = e.getKeyCode(); - int hSize=history.size(); - if (code==KeyEvent.VK_UP) { - - if (historyPosition>0) { - if (historyPosition==hSize) { - // store current in history - String s=inputArea.getText(); - history.add(s); - } - historyPosition--; - setInput(history.get(historyPosition)); - } - e.consume(); // mark event consumed - } else if (code==KeyEvent.VK_DOWN) { - if (historyPosition { - btnRun.setEnabled(false); - SwingUtilities.invokeLater(() -> runStressTest()); - }); - - splitPane = new JSplitPane(); - add(splitPane, BorderLayout.CENTER); - - JPanel panel = new JPanel(); - splitPane.setLeftComponent(panel); - FlowLayout flowLayout = (FlowLayout) panel.getLayout(); - flowLayout.setAlignment(FlowLayout.LEFT); - flowLayout.setAlignOnBaseline(true); - - // ========================================= - // Option Panel - - JPanel optionPanel = new JPanel(); - panel.add(optionPanel); - optionPanel.setLayout(new GridLayout(0, 2, 0, 0)); - - JLabel lblNewLabel = new JLabel("Transactions per client"); - optionPanel.add(lblNewLabel); - transactionCountSpinner = new JSpinner(); - transactionCountSpinner.setModel(new SpinnerNumberModel(1000, 1, 1000000, 100)); - optionPanel.add(transactionCountSpinner); - - JLabel lblNewLabel2 = new JLabel("Ops per Transaction"); - optionPanel.add(lblNewLabel2); - opCountSpinner = new JSpinner(); - opCountSpinner.setModel(new SpinnerNumberModel(1, 1, 1000, 10)); - optionPanel.add(opCountSpinner); - - JLabel lblNewLabel3 = new JLabel("Clients"); - optionPanel.add(lblNewLabel3); - clientCountSpinner = new JSpinner(); - clientCountSpinner.setModel(new SpinnerNumberModel(1, 1, 100, 1)); - optionPanel.add(clientCountSpinner); - - // ========================================= - // Result Panel - - resultPanel = new JPanel(); - splitPane.setRightComponent(resultPanel); - resultPanel.setLayout(new BorderLayout(0, 0)); - - resultArea = new JTextArea(); - resultArea.setText("No results yet"); - resultArea.setLineWrap(true); - resultArea.setEditable(false); - resultPanel.add(resultArea); - resultArea.setFont(Toolkit.SMALL_MONO_FONT); - } - - long errors = 0; - long values = 0; - - private JSplitPane splitPane; - private JPanel resultPanel; - private JTextArea resultArea; - - NumberFormat formatter = new DecimalFormat("#0.000"); - - private synchronized void runStressTest() { - errors = 0; - values = 0; - Address address=PeerGUI.getGenesisAddress(); - - int transCount = (Integer) transactionCountSpinner.getValue(); - int opCount = (Integer) opCountSpinner.getValue(); - // TODO: enable multiple clients - int clientCount = (Integer) clientCountSpinner.getValue(); - - new SwingWorker() { - @Override - protected String doInBackground() throws Exception { - StringBuilder sb = new StringBuilder(); - try { - InetSocketAddress sa = peerView.peerServer.getHostAddress(); - - // Use client store - // Stores.setCurrent(Stores.CLIENT_STORE); - ArrayList> frs=new ArrayList<>(); - Convex pc = Convex.connect(sa, address,PeerGUI.getUserKeyPair(0)); - - ArrayList kps=new ArrayList<>(clientCount); - for (int i=0; i v=pc.transactSync(Invoke.create(address, -1, cmdsb.toString())).getValue(); - - ArrayList ccs=new ArrayList<>(clientCount); - for (int i=0; i> cfutures=Utils.futureMap (cc->{ - try { - for (int i = 0; i < transCount; i++) { - StringBuilder tsb = new StringBuilder(); - tsb.append("(def a (do "); - for (int j = 0; j < opCount; j++) { - tsb.append(" (* 10 " + i + ")"); - } - tsb.append("))"); - String source = tsb.toString(); - - ATransaction t = Invoke.create(cc.getAddress(),-1, Reader.read(source)); - CompletableFuture fr; - fr = cc.transact(t); - frs.add(fr); - } - } catch (IOException e) { - throw Utils.sneakyThrow(e); - } - return null; - },ccs); - // wait for everything to be sent - for (int i=0; i results = Utils.completeAll(frs).get(60, TimeUnit.SECONDS); - long endTime = Utils.getCurrentTimestamp(); - - for (Result r : results) { - if (r.isError()) { - errors++; - } else { - values++; - } - } - - for (int i=0; i implements TreeNode { - - private final T object; - private final boolean isContainer; - - public StateTreeNode(T o) { - this.object = o; - this.isContainer = Utils.refCount(o)>0; - } - - private static StateTreeNode create(R value) { - return new StateTreeNode(value); - } - - @Override - public TreeNode getChildAt(int childIndex) { - if (isContainer) { - ACell child = object.getRef(childIndex).getValue(); - return StateTreeNode.create(child); - } - return null; - } - - @Override - public int getChildCount() { - return isContainer ? ((ACell) object).getRefCount() : 0; - } - - @Override - public TreeNode getParent() { - return null; - } - - @Override - public int getIndex(TreeNode node) { - // TODO Auto-generated method stub - return 0; - } - - @Override - public boolean getAllowsChildren() { - return isContainer; - } - - @Override - public boolean isLeaf() { - return getChildCount() == 0; - } - - @SuppressWarnings("unchecked") - @Override - public Enumeration children() { - - Ref[] childRefs = (isContainer ? ((ACell) object).getChildRefs() : new Ref[0]); - ArrayList> tns = new ArrayList<>(); - for (Ref r : childRefs) { - tns.add(StateTreeNode.create(r.getValue())); - } - return Collections.enumeration(tns); - } - - @Override - public String toString() { - return Utils.getClassName(object); // +" : "+Utils.toString(object); - } - -} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/state/StateTreePanel.java b/convex-gui/src/main/java/convex/gui/manager/windows/state/StateTreePanel.java deleted file mode 100644 index 3a1d18b29..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/windows/state/StateTreePanel.java +++ /dev/null @@ -1,129 +0,0 @@ -package convex.gui.manager.windows.state; - -import java.awt.BorderLayout; -import java.awt.Dimension; - -import javax.swing.JPanel; -import javax.swing.JTree; -import javax.swing.event.TreeExpansionEvent; -import javax.swing.event.TreeWillExpandListener; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.ExpandVetoException; -import javax.swing.tree.TreeModel; -import javax.swing.tree.TreePath; - -import convex.core.data.ACell; -import convex.core.data.ARecord; -import convex.core.data.Keyword; -import convex.core.data.MapEntry; -import convex.core.data.MapLeaf; -import convex.core.util.Utils; - -@SuppressWarnings("serial") -public class StateTreePanel extends JPanel { - - private final ACell state; - - private static class Node extends DefaultMutableTreeNode { - private final boolean container; - private boolean loaded = false; - private final String name; - - public Node(String name, ACell val) { - super(val); - this.name = name; - container = Utils.refCount(val)>0; - } - - public Node(ACell val) { - this(null, val); - } - - @Override - public boolean isLeaf() { - return !container; - } - - private static String getString(Object val) { - if (val instanceof ACell) { - ACell r = (ACell) val; - return r.getClass().getSimpleName() + " [" + r.getHash().toHexString(6) + "...]"; - } - - return Utils.getClassName(val); - } - - @Override - public String toString() { - if (name != null) return name; - return getString(this.userObject); - } - - @SuppressWarnings("rawtypes") - public void loadChildren() { - if (loaded) return; - loaded = true; - - if (userObject instanceof ARecord) { - ARecord r = (ARecord) userObject; - for (Keyword k : r.getKeys()) { - ACell c = r.get(k); - add(new Node(k + " = " + getString(c), c)); - } - return; - } else if (userObject instanceof MapLeaf) { - MapLeaf m = (MapLeaf) userObject; - for (Object oe : m.entrySet()) { - MapEntry e = (MapEntry) oe; - ACell c = e.getValue(); - add(new Node(getString(e.getKey()) + " = " + getString(c), c)); - } - return; - } - - if (!container) return; - ACell rc = (ACell) userObject; - int n = rc.getRefCount(); - for (int i = 0; i < n; i++) { - ACell child = rc.getRef(i).getValue(); - add(new Node(child)); - } - - } - - } - - private static final TreeWillExpandListener expandListener = new TreeWillExpandListener() { - @Override - public void treeWillExpand(TreeExpansionEvent tee) throws ExpandVetoException { - TreePath path = tee.getPath(); - Node tn = (Node) path.getLastPathComponent(); - tn.loadChildren(); - } - - @Override - public void treeWillCollapse(TreeExpansionEvent tee) throws ExpandVetoException { - /* Nothing to do */ - } - }; - - public StateTreePanel(ACell state) { - this.state = state; - setLayout(new BorderLayout()); - setPreferredSize(new Dimension(600, 400)); - - // StateTreeNode root=new StateTreeNode<>(this.state); - - Node tNode = new Node(this.state); - - tNode.setAllowsChildren(true); - tNode.loadChildren(); - TreeModel tModel = new DefaultTreeModel(tNode); - JTree tree = new JTree(tModel); - tree.addTreeWillExpandListener(expandListener); - tree.expandPath(new TreePath(tNode.getPath())); - - add(tree, BorderLayout.CENTER); - } -} diff --git a/convex-gui/src/main/java/convex/gui/manager/windows/state/StateWindow.java b/convex-gui/src/main/java/convex/gui/manager/windows/state/StateWindow.java deleted file mode 100644 index d67491350..000000000 --- a/convex-gui/src/main/java/convex/gui/manager/windows/state/StateWindow.java +++ /dev/null @@ -1,30 +0,0 @@ -package convex.gui.manager.windows.state; - -import java.awt.BorderLayout; - -import javax.swing.JTabbedPane; - -import convex.core.data.ACell; -import convex.gui.manager.PeerGUI; -import convex.gui.manager.windows.BaseWindow; - -@SuppressWarnings("serial") -public class StateWindow extends BaseWindow { - - JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.TOP); - - public StateWindow(PeerGUI manager, ACell state) { - super(manager); - - add(tabbedPane, BorderLayout.CENTER); - - tabbedPane.addTab("State Tree", null, new StateTreePanel(state), null); - - } - - @Override - public String getTitle() { - return "State explorer"; - } - -} diff --git a/convex-gui/src/main/java/convex/gui/utils/RobinsonProjection.java b/convex-gui/src/main/java/convex/gui/utils/RobinsonProjection.java deleted file mode 100644 index 23192f9a7..000000000 --- a/convex-gui/src/main/java/convex/gui/utils/RobinsonProjection.java +++ /dev/null @@ -1,112 +0,0 @@ -package convex.gui.utils; - -import java.awt.geom.Point2D; - -public class RobinsonProjection { - public static final double HALFPI = Math.PI * 0.5; - - private final static double X[] = { 1, -5.67239e-12, -7.15511e-05, 3.11028e-06, 0.9986, -0.000482241, -2.4897e-05, - -1.33094e-06, 0.9954, -0.000831031, -4.4861e-05, -9.86588e-07, 0.99, -0.00135363, -5.96598e-05, 3.67749e-06, - 0.9822, -0.00167442, -4.4975e-06, -5.72394e-06, 0.973, -0.00214869, -9.03565e-05, 1.88767e-08, 0.96, - -0.00305084, -9.00732e-05, 1.64869e-06, 0.9427, -0.00382792, -6.53428e-05, -2.61493e-06, 0.9216, - -0.00467747, -0.000104566, 4.8122e-06, 0.8962, -0.00536222, -3.23834e-05, -5.43445e-06, 0.8679, -0.00609364, - -0.0001139, 3.32521e-06, 0.835, -0.00698325, -6.40219e-05, 9.34582e-07, 0.7986, -0.00755337, -5.00038e-05, - 9.35532e-07, 0.7597, -0.00798325, -3.59716e-05, -2.27604e-06, 0.7186, -0.00851366, -7.0112e-05, - -8.63072e-06, 0.6732, -0.00986209, -0.000199572, 1.91978e-05, 0.6213, -0.010418, 8.83948e-05, 6.24031e-06, - 0.5722, -0.00906601, 0.000181999, 6.24033e-06, 0.5322, 0., 0., 0. }; - - private final static double Y[] = { 0, 0.0124, 3.72529e-10, 1.15484e-09, 0.062, 0.0124001, 1.76951e-08, - -5.92321e-09, 0.124, 0.0123998, -7.09668e-08, 2.25753e-08, 0.186, 0.0124008, 2.66917e-07, -8.44523e-08, - 0.248, 0.0123971, -9.99682e-07, 3.15569e-07, 0.31, 0.0124108, 3.73349e-06, -1.1779e-06, 0.372, 0.0123598, - -1.3935e-05, 4.39588e-06, 0.434, 0.0125501, 5.20034e-05, -1.00051e-05, 0.4968, 0.0123198, -9.80735e-05, - 9.22397e-06, 0.5571, 0.0120308, 4.02857e-05, -5.2901e-06, 0.6176, 0.0120369, -3.90662e-05, 7.36117e-07, - 0.6769, 0.0117015, -2.80246e-05, -8.54283e-07, 0.7346, 0.0113572, -4.08389e-05, -5.18524e-07, 0.7903, - 0.0109099, -4.86169e-05, -1.0718e-06, 0.8435, 0.0103433, -6.46934e-05, 5.36384e-09, 0.8936, 0.00969679, - -6.46129e-05, -8.54894e-06, 0.9394, 0.00840949, -0.000192847, -4.21023e-06, 0.9761, 0.00616525, - -0.000256001, -4.21021e-06, 1., 0., 0., 0 }; - - private final int NODES = 18; - private final static double FXC = 0.8487; - private final static double FYC = 1.3523; - private final static double C1 = 11.45915590261646417544; - private final static double RC1 = 0.08726646259971647884; - private final static double EPS = 1e-8; - - private RobinsonProjection() { - } - - private static final RobinsonProjection INSTANCE = new RobinsonProjection(); - - public static Point2D getPoint(double latitude, double longitude) { - Point2D.Double pt = new Point2D.Double(); - - double lam = longitude / 360.0 * Math.PI; - double phi = latitude / 90.0; - INSTANCE.project(lam, phi, pt); - pt.x = 0.45 + (pt.x * 0.357); - pt.y = 0.5 - (pt.y * 0.56); - return pt; - } - - private double poly(double[] array, int offset, double z) { - return (array[offset] + z * (array[offset + 1] + z * (array[offset + 2] + z * array[offset + 3]))); - } - - public Point2D.Double project(double lplam, double lpphi, Point2D.Double xy) { - double phi = Math.abs(lpphi); - int i = (int) Math.floor(phi * C1); - if (i >= NODES) i = NODES - 1; - phi = Math.toDegrees(phi - RC1 * i); - i *= 4; - xy.x = poly(X, i, phi) * FXC * lplam; - xy.y = poly(Y, i, phi) * FYC; - if (lpphi < 0.0) xy.y = -xy.y; - return xy; - } - - public Point2D.Double projectInverse(double x, double y, Point2D.Double lp) { - int i; - double t, t1; - - lp.x = x / FXC; - lp.y = Math.abs(y / FYC); - if (lp.y >= 1.0) { - if (lp.y > 1.000001) { - throw new Error("Out of range y?"); - } else { - lp.y = y < 0. ? -HALFPI : HALFPI; - lp.x /= X[4 * NODES]; - } - } else { - for (i = 4 * (int) Math.floor(lp.y * NODES);;) { - if (Y[i] > lp.y) i -= 4; - else if (Y[i + 4] <= lp.y) i += 4; - else break; - } - t = 5. * (lp.y - Y[i]) / (Y[i + 4] - Y[i]); - double Tc0 = Y[i]; - double Tc1 = Y[i + 1]; - double Tc2 = Y[i + 2]; - double Tc3 = Y[i + 3]; - t = 5. * (lp.y - Tc0) / (Y[i + 1] - Tc0); - Tc0 -= lp.y; - for (;;) { // Newton-Raphson - t -= t1 = (Tc0 + t * (Tc1 + t * (Tc2 + t * Tc3))) / (Tc1 + t * (Tc2 + Tc2 + t * 3. * Tc3)); - if (Math.abs(t1) < EPS) break; - } - lp.y = Math.toRadians(5 * i + t); - if (y < 0.) lp.y = -lp.y; - lp.x /= poly(X, i, t); - } - return lp; - } - - public boolean hasInverse() { - return true; - } - - public String toString() { - return "Robinson"; - } - -} diff --git a/convex-gui/src/main/java/convex/gui/utils/Toolkit.java b/convex-gui/src/main/java/convex/gui/utils/Toolkit.java deleted file mode 100644 index 8f16e2178..000000000 --- a/convex-gui/src/main/java/convex/gui/utils/Toolkit.java +++ /dev/null @@ -1,139 +0,0 @@ -package convex.gui.utils; - -import java.awt.Color; -import java.awt.Font; -import java.awt.Graphics2D; -import java.awt.Image; -import java.awt.RenderingHints; -import java.awt.image.BufferedImage; -import java.net.URL; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -import javax.swing.ImageIcon; -import javax.swing.JLabel; -import javax.swing.SwingUtilities; -import javax.swing.UIManager; -import javax.swing.UIManager.LookAndFeelInfo; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.plaf.FontUIResource; - -import mdlaf.MaterialLookAndFeel; -import mdlaf.themes.AbstractMaterialTheme; -import mdlaf.themes.MaterialOceanicTheme; - -public class Toolkit { - - private static final Logger log = LoggerFactory.getLogger(Toolkit.class.getName()); - - static { - try { - UIManager.installLookAndFeel("Material", "mdlaf.MaterialLookAndFeel"); - Class.forName("mdlaf.MaterialLookAndFeel"); - // search for Nimbus look and feel if it is available - for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { - String name = info.getName(); - // log.info("Found L&F: " + name); - if (name.equals("Nimbus")) { // Nimbus - UIManager.setLookAndFeel(info.getClassName()); - // UIManager.put("nimbusBase", new Color(130,89,171)); - // UIManager.put("menu", new Color(61,89,171)); - // UIManager.put("control", new Color(200,180,160)); - } - } - - // prefer MaterialLookAndFeel if we have it - AbstractMaterialTheme theme = new MaterialOceanicTheme(); - MaterialLookAndFeel material = new MaterialLookAndFeel(theme); - UIManager.setLookAndFeel(material); - theme.setFontBold(new FontUIResource(theme.getFontBold().deriveFont(14.0f))); - theme.setFontItalic(new FontUIResource(theme.getFontItalic().deriveFont(14.0f))); - theme.setFontMedium(new FontUIResource(theme.getFontMedium().deriveFont(14.0f))); - theme.setFontRegular(new FontUIResource(theme.getFontRegular().deriveFont(14.0f))); - - UIManager.getLookAndFeelDefaults().put("TextField.caretForeground", Color.white); - } catch (Exception e) { - e.printStackTrace(); - log.warn("Unable to set look and feel: {}", e); - } - } - - // public static final ImageIcon LOCKED_ICON = - // scaledIcon(36,"/images/ic_lock_outline_black_36dp.png"); - // public static final ImageIcon UNLOCKED_ICON = - // scaledIcon(36,"/images/ic_lock_open_black_36dp.png"); - - public static final ImageIcon LOCKED_ICON = scaledIcon(36, "/images/padlock.png"); - public static final ImageIcon UNLOCKED_ICON = scaledIcon(36, "/images/padlock-open.png"); - public static final ImageIcon WARNING = scaledIcon(36, "/images/ic_priority_high_black_36dp.png"); - public static final ImageIcon CAKE = scaledIcon(36, "/images/ic_cake_black_36dp.png"); - public static final ImageIcon CONVEX = scaledIcon(36, "/images/Convex.png"); - public static final ImageIcon COG = scaledIcon(36, "/images/cog.png"); - - public static final Font DEFAULT_FONT = new JLabel().getFont(); - - public static final Font MONO_FONT = new Font(Font.MONOSPACED, Font.BOLD, 16); - - public static final Font SMALL_MONO_FONT = new Font(Font.MONOSPACED, Font.PLAIN, 10); - public static final Font SMALL_MONO_BOLD = SMALL_MONO_FONT.deriveFont(Font.BOLD); - - public static ImageIcon scaledIcon(int size, String resourcePath) { - java.net.URL imgURL = Toolkit.class.getResource(resourcePath); - if (imgURL == null) throw new Error("No image: " + resourcePath); - ImageIcon imageIcon = new ImageIcon(imgURL); - Image image = imageIcon.getImage(); - image = image.getScaledInstance(size, size, Image.SCALE_SMOOTH); - - return new ImageIcon(image); - } - - /** - * Scale an image with interpolation / AA for nicer effects - * - * @param src - * @param w - * @param h - * @return A new, resized image - */ - public static BufferedImage smoothResize(BufferedImage src, int w, int h) { - BufferedImage newImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - Graphics2D graphics = (Graphics2D) newImage.getGraphics(); - - // set up rendering hints - graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); - graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - - graphics.drawImage(src, 0, 0, w, h, null); - return newImage; - } - - public static DocumentListener createDocumentListener(final Runnable a) { - return new DocumentListener() { - @Override - public void insertUpdate(DocumentEvent e) { - SwingUtilities.invokeLater(a); - } - - @Override - public void removeUpdate(DocumentEvent e) { - SwingUtilities.invokeLater(a); - } - - @Override - public void changedUpdate(DocumentEvent e) { - SwingUtilities.invokeLater(a); - } - }; - } - - public static void init() { - // Empty method, just triggers static initialisation - } - - public static Image getImage(URL resourceURL) { - return java.awt.Toolkit.getDefaultToolkit().getImage(resourceURL); - } -} diff --git a/convex-gui/src/main/java/convex/wallet/Wallet.java b/convex-gui/src/main/java/convex/wallet/Wallet.java deleted file mode 100644 index 103e419c5..000000000 --- a/convex-gui/src/main/java/convex/wallet/Wallet.java +++ /dev/null @@ -1,73 +0,0 @@ -package convex.wallet; - -import java.awt.BorderLayout; -import java.awt.EventQueue; -import java.awt.Toolkit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.JFrame; -import javax.swing.JPanel; -import javax.swing.JTabbedPane; - -import convex.gui.manager.mainpanels.HomePanel; - -@SuppressWarnings("serial") -public class Wallet extends JPanel { - - private static final Logger log = LoggerFactory.getLogger(Wallet.class.getName()); - - private static JFrame frame; - - public static long maxBlock = 0; - - /** - * Launch the application. - * @param args Command line args - */ - public static void main(String[] args) { - // call to set up Look and Feel - convex.gui.utils.Toolkit.init(); - - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - try { - Wallet.frame = new JFrame(); - frame.setTitle("Convex Secure Wallet"); - frame.setIconImage(Toolkit.getDefaultToolkit() - .getImage(Wallet.class.getResource("/images/ic_stars_black_36dp.png"))); - frame.setBounds(100, 100, 1024, 768); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - Wallet window = new Wallet(); - frame.getContentPane().add(window, BorderLayout.CENTER); - frame.pack(); - frame.setVisible(true); - log.debug("Wallet GUI launched"); - } catch (Exception e) { - e.printStackTrace(); - } - } - }); - } - - /* - * Main component panel - */ - JPanel panel = new JPanel(); - - HomePanel homePanel = new HomePanel(); - JTabbedPane tabs = new JTabbedPane(); - - /** - * Create the application. - */ - public Wallet() { - setLayout(new BorderLayout()); - this.add(tabs, BorderLayout.CENTER); - - tabs.add("Home", homePanel); - - } -} diff --git a/convex-gui/src/main/resources/images/Convex.png b/convex-gui/src/main/resources/images/Convex.png deleted file mode 100644 index cee27b2c31ef93ef7feb6b2577966fc69535c258..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10210 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 diff --git a/convex-gui/src/main/resources/images/cog.png b/convex-gui/src/main/resources/images/cog.png deleted file mode 100644 index d31d3cbc21751c902c14129b30efa116e5ed8d44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5007 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 diff --git a/convex-gui/src/main/resources/images/ic_lock_outline_black_36dp.png b/convex-gui/src/main/resources/images/ic_lock_outline_black_36dp.png deleted file mode 100644 index 98b7144e49cc3793b2703234a60eb727e46b73a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 504 zcmV=w#@+P+(DZa=@TvN*eNsEC(;c4me)_AUy>P~} zgd^PJ8zaB`#yt+Rm~qsFS?u8lQo`@-Vg@y4CEt(=eqk9kV+<#d7#?9vZF0^cF+GKS zQi-FJ;W<}-`J4|IGg2xik(FmIT+%*ogQ~h`JCHjv(p=qS%J02QlHE{(%vu5ydQW z`u%q>LGLi06Nk6@{a-O*ME}55;&4m9e;5-)y+9nUAZlqm$b&p+B7Hnz2&s}oJf?>U zq+~v?5d&W`H{H?sm1E=!#*>&n#L#XM^Tagh5s4Wx4H_UZf@<*KkX8-yAP@2&57LRE z)1tmXI#I%DVIP!WqP$>y{et?EO$+;=C&{LTeb7+EwD6$EC4(N;FQ}(vP-p#uCh)Q- z=UF{(dzj0MFo!wyesxTsn@0@%@*$m!_m^u*JjjDQ$b(wo-!YF(gC3HY9@C&slA+VW uz=(+wH7yLRJW*mz3#pDeQR;}}I6nbt1rk?Ha-Y2b00004QV=_mX^l- YE?JIa-Ns3KfNo*%boFyt=akR{0DZzlvH$=8 diff --git a/convex-gui/src/main/resources/images/ic_stars_black_36dp.png b/convex-gui/src/main/resources/images/ic_stars_black_36dp.png deleted file mode 100644 index 177745993033c2b1dc308b209f13ffffa6d00e7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 951 zcmV;o14#UdP)7f#LxKuJI;^~lkSH_hU7L`q<`j^|xPnV6Kzu)is{7&MeH8y9Q@btB36|D2mjt%OcHc?s{9D5{V7k>>4%GZl5TlZf&Y= zvW=pGHfT{rclupa*m3hz#b|GcDz!?z3XRt#s@OJ7D$wG9sB&MJs7&J>6p8itJbg%- zlwov9lXbTRBDwAAmF#tq+%M{l@@q-UyF_=0^gIWi7wMTNok=C_JA)>H$p%Z>ms%uy zPo!rffK4Jj>m`~kN~#UO%rbs6L1{2S3>P{j(z9Hy6(T)@nichsNY6hmlIwp?i}XC8s82T;OMbSQx=~_{=mkl-6O9qGhzb|Ss)N7(>S4ps26zMkq zx!sQgf>t%*S?}!5Rf<`Z7>{$JlX#ht=f!0da z>V(MKTbS<$k+R*AgbtVFMh8n#l&>UD}5^|3l-80Sk- zgL*V7(*^d|U^wTiz_=<;+*XY$G^&crdxbhxG2aPMVSR2=MXlZzWp~;d&8lve-J*;- z%}_HlZ80bk8}PpAYOcj%-5F2hzjeCLL}yWhIhI;$pYQ#q-$_Ruvd3zVxXx(9h7CJM Z{R=Q^kyNaPu$2G+002ovPDHLkV1kG6%wYfk diff --git a/convex-gui/src/main/resources/images/padlock-open.png b/convex-gui/src/main/resources/images/padlock-open.png deleted file mode 100644 index eddf1e66a9229058673de5527e7220b135294844..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1946 zcmV;L2W9w)P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2QNuPK~#8N?VEdS z6h#=oKaRH7*Fo=E9>vn47Q_IW8mnyp%S))Fij|0j_-IUkAcDmREd+yDl$elUc=Qia zq^JP|s+5;-gz!)-MggUi0!6VcebNWl$Cc~j>i5k}_pXIrw|l$WYuaD(%gk)ycJ`Z{ zncdml0iX`!(KvJxIu;#<`k{{dH=~tk9-4)oKrf)JXs?70?oIR$lnz3Q|DYeD-e|Al zX|%2NpM(^{gL6jM74g!4v$n8W`ZP_l8C*l-zHI zYlV$)`dU4lyjl;ICOUQjE4mKdz)CV9cK|*>x1t&<&;c%(I0%wn3xp?zdeSD*RMxh@ z)|1t+4Smo)5Zh7e7P25`0Op~4QFidbdJovOI2b04^`W9<@=6=w-P97uF0k+|jL*>Z zwybN&6@W45CDfl4g1z9#$`E+W;6X(Owc@cWb$EK4;Z{i_)HGP3wbkCT)FmFrvxK@z ze560<{RZ;C0MtLQ!u+kpa3+T)3I=tDS5PA>xquu2px-!+PNl-I!5(mKT`2Z*`@w(v ziwV{rtb{UCGvDGI3P$2=J$w>p0MCK8D%M-9Fzd@g_$yyH6aGWTp+Z!oAV&aZqsJ&i zKXw>fe(E@1Dne6BD=bbch5cEztlV9qM*F~l#1X>T@Nan&M6JySGY*)XmFOm0RvK~y z;0!v2GIL+l!R{r&lw~a3Q3`v1tKlUEMPd(n9xX%Lj)dxAKqJw&(Ge))g(tn?#OjgY z`6i;wQRZViciaXbUU#0JL*D2sU5;z9Fo&E`1C$ZP9SH&B3GS5VR|3)ZBs! z|954aLHOOG)bLj9B96a16O{}bv0We*Wla+?${WUoXekTLcd7+5Y1DVKHk>vi8I&DP*~Z-OKgx@h<5ih zU5(N}$7YZN7wuiiabHLjPbJ}M0BHJSJQHebed2sBHKM=~l=^CSq2WH=mIX-Ka}PfL zt{gTUt%Bm}W(Xhd#sA`309@rJ%KD;l%}Al2&UxP7v5&fP)%G&#_*^6j?;m&Xevs#-F37yn`wNwS=(xWE$?72__7j8Cmd=e~)4s{b%Q?X+}G0L8JWTk|`gptzD z0(um<>zoNT9jS8cs|;H1UN|iPR=sLqgRB&?3oQ`+X+AHp=~{Te(it+er#NV7d;XSU zSe{v z%-vihOP3IXDgmH5`iCt0;&6O?Jmlo$!13e9;n1N&aO1`eNJ>hA0sNJbs|6O=W~^p; zrBNjSsi*7*LoJ?`mIi@=_9=_bhUDaANK8!RTL5;PzR$)CrBOTpR=hYnpKHHyp_bNk zGL@`Yv4RDID5`3P+hxKHm0~C!fXccSXlS%IWkf^-&pnJzr-ShDa9#qqi$PEt#RK5& z*?y)qo1MSV;rMqge zKP4pv^7Dm@6#;7bWy|b$u~FeZ5TsN7Zc~*2ERWU0AUqR@^73++F=Gas`r0s=Ob{Cz z3zsg@i)J>G=d`=AQ4CcAFg(Bmw!c4&y)jBuR8&AlMux3`7>!2A&d%l~oDF6hI=%#< z7^(z-h>z64dohD~381OTemU%zpZJsyq|6KACFM~q0EEuF>)p|ysV3^~%?;@f08jfY z{Q=-9`s>J+hEfZo~`zR*g*ySk+_^{Ombh=E#uyRjP5+XsL? z&^T<%%ELaCRyTWt^x?JG#uW{%Q2dBKYKQJ5eUPO`6@j8U7d?s6AoHjS^n~<@3Vm|F g7`1<}*d+k}17iIVh$9lS9{>OV07*qoM6N<$g0Rn`xBvhE diff --git a/convex-gui/src/main/resources/images/padlock.png b/convex-gui/src/main/resources/images/padlock.png deleted file mode 100644 index 9cfd949dece2f3f740badbe0bc91b27a8983bc4f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1948 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

0KMI7<3;vof4=ljl1e6a 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 aW{V(XlkXtzu85hfEInEHrngBY;0 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%0AIzdd&2t31|r7_NI6(+DwNcUHgkqERw#0uF;g|() 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

6Y==MScw6or*=iv9hkaA9wJ=pJxZ zHI=twK*@pe^8`QXm5FiyWWJhpl) z#q8bi(WYHv3W?nmiTC1o$f%Fr9qgtkNJf`2tRl^R067Y9M1z=I$LHH%7-DqHPW|}t zm{}b{?MJ!e6BWJO|6HAyscET;T_5CU79Mp!rTxeIGbe^4$U* zGvY2th+BJU5R35{K;_P}eGe!%@I8$H)SDvBVV z8vU;Q5A(B!JL2}>u$DQv^e~_Vz)InW7_ccNmLK4^fXCw4cYht1wI9bW4KfUg0h|!T zXKF12Rnq_&dsP_=YsMuAK}mN66@UaH%XZQUaXH23Dox;3^QaU&5Ypo9g%|@L7#m^P zy4;A)3<&**Scd0a&nm$89A!aU9`M;JYGFYX;$J*wIz768`~IDA z001IsVtY{!keU>wdap&5VYr!lYf4RlSpvN4R_AlVdYDv z_TEpc50zcoTS0$#W~8P6HQ(JB;VVja`?|_3QCgsuE<-~d0_q2L0MG8-Uxrf8Us-;( z^YH&4#(kQ!Xg_Uw+2uScVjv{*u%H($I@|uvfcL;jtV|*?g6fiOW3AtcV>Jh7IH~ft zE38cpOQ$&{V3#>)D6B5}Yl41z3qX^yl%g7J z;S7Pf)gH%b^Vv-N>?0~C;-U>W5+f55Jb=u?u!Ihf9s=O3PWWE`Cd#WeiIrFEabjut zB&`E(Q4#hC31jHSSDUcWrHITxS1$aB^cvj%Wkwj=Qf@eg`p)kzJCXXHI3Ki(kb!pl zjr~5@{M`4Yt)V@mU!SZE)KQ^d&b7bx*Z+- z8J-+-#3UAZ@D>;+LJMG(qV;sA_*fNj0mDx)ImvU1uJodRr7+6^7=aBta-!jQ-(2g$ zaKJAHAu#pH&;h@gyxvkffBJiPIPV=CU2obQR)UX63EkwT*tXeA^}1Cq9`I1&R~h(Io|;L9xOGc$JX@QuTB!UexQ?WFO}3}*~xy?AerPq#&LnK!)Hh%=pRDGVqo ziZjoRTIuH)U$K@&EYG)2_AK8$j$3wRcf7Mcx;~c4X!9OdE~ngd&PqZea8XKZ8!0tx zEUG&!Gt+uY|4r-$jI(j9mElx4IoU8|cL@I(17*L;hU3)B6B1~S02(2fmJsTj4OSa+s@eYc(p%+ zmR1oXa|mJx)g2ZR66~+~=Je6*rNfy+C+x=e*C|K0vQ10Mw%yHL97<)Z#E~A)T~!c66xjUCtvJ*_>3(Phc2mU zNRsXZ@7*_lVjvnI#0=;xtXMTY@dg3j9LC)Q9g9`^0&B^rH4qD`6L{D+&mt>s#(e#C zwb3q)m`H$zxRsYzfjif)cvRtK@f;h;(ZF~}((*}EgSzmwin;Y@3Z>ADLLP)Qo+W;RQIYR4DE7fzLd0BYi9NtjhYldOla|JFUCH_`;1C|vnsR^rzHZ4l>ANlBP zdVHf#Ku{#uS{UrZc!6R5S;jg37@#`X7sTM@5Bf_Q3dPDDv|{r%v^X(!G_J^*q3iv4{hiUGoRFUadyN8$UO8vF6ag>RH zsW_VR3%bYSa8BbfnO63{-tJ#`l_PEDlSJqOU z5Xmuw3>ofRdK9F;H?j(Lv5?+gMTuj{

T$q~uxkYmyO7t}Oz|JH$OA&xsB924Vl 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�gG;yiLngLI+ZGjWUJ*A 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Ð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 - - world.convex - convex - 0.7.0-rc3 - - - 4.0.0 - - convex-peer - - Convex Peer - Convex Peer implementation and APIs - https://convex.world - - - - - - - - - - - world.convex - convex-core - ${convex.version} - - - - org.junit.jupiter - junit-jupiter-engine - ${junit.version} - test - - - org.junit.vintage - junit-vintage-engine - ${junit.version} - test - - - org.junit.jupiter - junit-jupiter-params - ${junit.version} - test - - - - org.slf4j - slf4j-jdk14 - ${slf4j.version} - test - - - - diff --git a/convex-peer/src/main/java/convex/api/Applications.java b/convex-peer/src/main/java/convex/api/Applications.java deleted file mode 100644 index 4a663b18f..000000000 --- a/convex-peer/src/main/java/convex/api/Applications.java +++ /dev/null @@ -1,37 +0,0 @@ -package convex.api; - -import java.io.IOException; - -public class Applications { - - /** - * Helper function to launch a different JVM process with the same classpath. - * - * @param c Main class to launch - * @param args Command line args for launched process - * @return Process instance that can be used to observe exit value etc. - * @throws IOException if IO error occurs - */ - public static Process launchApp(Class c, String... args) throws IOException { - // construct path to java executable - String separator = System.getProperty("file.separator"); - String classpath = System.getProperty("java.class.path"); - String path = System.getProperty("java.home") + separator + "bin" + separator + "java"; - - // construct process arguments - String mainClassName=c.getName(); - int nargs=args.length; - String[] pargs=new String[4+nargs]; - pargs[0]=path; - pargs[1]="-cp"; - pargs[2]=classpath; - pargs[3]=mainClassName; - System.arraycopy(args, 0, pargs, 4, nargs); - ProcessBuilder processBuilder = new ProcessBuilder(pargs); - - // Execute process and return Process instance - // Calling code can use Process.wairFor(), exitValue() etc. - Process process = processBuilder.start(); - return process; - } -} diff --git a/convex-peer/src/main/java/convex/api/Convex.java b/convex-peer/src/main/java/convex/api/Convex.java deleted file mode 100644 index f3bdfba95..000000000 --- a/convex-peer/src/main/java/convex/api/Convex.java +++ /dev/null @@ -1,898 +0,0 @@ -package convex.api; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.util.HashMap; -import java.util.HashSet; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.Consumer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import convex.core.Constants; -import convex.core.ErrorCodes; -import convex.core.Result; -import convex.core.crypto.AKeyPair; -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.Address; -import convex.core.data.Hash; -import convex.core.data.Keywords; -import convex.core.data.Ref; -import convex.core.data.SignedData; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.MissingDataException; -import convex.core.lang.RT; -import convex.core.lang.Reader; -import convex.core.lang.Symbols; -import convex.core.lang.ops.Lookup; -import convex.core.lang.ops.Special; -import convex.core.store.AStore; -import convex.core.store.Stores; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Invoke; -import convex.core.transactions.Transfer; -import convex.core.util.Utils; -import convex.core.State; -import convex.net.Connection; -import convex.net.Message; -import convex.net.ResultConsumer; -import convex.peer.Server; - -/** - * Class representing the client API to the Convex network when connected - * directly using the binary protocol. This can be more efficient than using a - * REST API. - * - * An Object of the type Convex represents a stateful client connection to the - * Convex network that can issue transactions both synchronously and - * asynchronously. This can be used by both peers and JVM-based clients. - * - * "I'm doing a (free) operating system (just a hobby, won't be big and - * professional like gnu)" - Linus Torvalds - */ -@SuppressWarnings("unused") -public class Convex { - - private static final Logger log = LoggerFactory.getLogger(Convex.class.getName()); - - private long timeout=Constants.DEFAULT_CLIENT_TIMEOUT; - - /** - * Key pair for this Client - */ - protected AKeyPair keyPair; - - /** - * Current address for this Client - */ - protected Address address; - - /** - * Current Connection to a Peer, may be null or a closed connection. - */ - protected Connection connection; - - /** - * Determines if auto-sequencing should be attempted - */ - private boolean autoSequence = true; - - /** - * Sequence number for this client, or null if not yet known - */ - protected Long sequence = null; - - /** - * Map of results awaiting completion. May be pending missing data. - */ - private HashMap> awaiting = new HashMap<>(); - - private final Consumer internalHandler = new ResultConsumer() { - @Override - protected synchronized void handleResult(long id, Result v) { - - if ((v!=null)&&(ErrorCodes.SEQUENCE.equals(v.getErrorCode()))) { - // We probably got a wrong sequence number. Kill the stored value. - sequence=null; - } - - // TODO: maybe extract method? - synchronized (awaiting) { - CompletableFuture cf = awaiting.get(id); - if (cf != null) { - awaiting.remove(id); - cf.complete(v); - log.debug( - "Completed Result received for message ID: {}", id); - } else { - log.warn( - "Ignored Result received for unexpected message ID: {}", id); - } - } - } - - @Override - public void accept(Message m) { - super.accept(m); - - if (delegatedHandler != null) { - try { - delegatedHandler.accept(m); - } catch (Throwable t) { - log.warn("Exception thrown in user-supplied handler function: {}", t); - } - } - } - }; - - private Consumer delegatedHandler = null; - - private Convex(Address address, AKeyPair keyPair) { - this.keyPair = keyPair; - this.address = address; - } - - /** - * Creates an anonymous connection to a Peer, suitable for queries - * @param hostAddress Address of Peer - * @return New Convex client instance - * @throws IOException If IO Error occurs - * @throws TimeoutException If connection attempt times out - */ - public static Convex connect(InetSocketAddress hostAddress) throws IOException, TimeoutException { - return connect(hostAddress,(Address)null, (AKeyPair)null); - } - - /** - * Create a Convex client by connecting to the specified Peer using the given - * key pair - * - * @param peerAddress Address of Peer - * @param address Address of Account to use for Client - * @param keyPair Key pair to use for client transactions - * @return New Convex client instance - * @throws IOException If connection fails - * @throws TimeoutException If connection attempt times out - */ - public static Convex connect(InetSocketAddress peerAddress, Address address, AKeyPair keyPair) throws IOException, TimeoutException { - return Convex.connect(peerAddress, address, keyPair, Stores.current()); - } - - /** - * Create a Convex client by connecting to the specified Peer using the given - * key pair and using a given store - * - * @param peerAddress Address of Peer - * @param address Address of Account to use for Client - * @param keyPair Key pair to use for client transactions - * @param store Store to use for this connection - * @return New Convex client instance - * @throws IOException If connection fails - * @throws TimeoutException If connection attempt times out - */ - public static Convex connect(InetSocketAddress peerAddress, Address address, AKeyPair keyPair, AStore store) throws IOException, TimeoutException { - Convex convex = new Convex(address, keyPair); - convex.connectToPeer(peerAddress, store); - return convex; - } - - /** - * Sets the Address for this connection. This will be used by default for - * subsequent transactions and queries - * - * @param address Address to use - */ - public synchronized void setAddress(Address address) { - if (this.address == address) return; - this.address = address; - // clear sequence, since we don't know the new account sequence number yet - sequence = null; - } - - public synchronized void setAddress(Address addr, AKeyPair kp) { - setAddress(addr); - setKeyPair(kp); - } - - public synchronized void setKeyPair(AKeyPair kp) { - this.keyPair = kp; - } - - /** - * Gets the next sequence number for this Client, which should be used for - * building new signed transactions - * - * @return Sequence number as a Long value greater than zero - */ - private long getIncrementedSequence() { - long next = getSequence() + 1L; - sequence = next; - return next; - } - - public void setNextSequence(long nextSequence) { - this.sequence = nextSequence - 1L; - } - - public void setHandler(Consumer handler) { - this.delegatedHandler = handler; - } - - /** - * Gets the current sequence number for this Client, which is the sequence - * number of the last transaction observed for the current client's Account. - * - * @return Sequence number as a Long value, zero or positive - */ - public long getSequence() { - if (sequence == null) { - try { - Future f = query(Special.forSymbol(Symbols.STAR_SEQUENCE)); - Result r = f.get(); - if (r.isError()) throw new Error("Error querying *sequence*: " + r.getErrorCode() + " " + r.getValue()); - ACell result=r.getValue(); - if (!(result instanceof CVMLong)) throw new Error("*sequence* query did not return Long, got: "+result); - sequence = RT.jvm(result); - } catch (IOException | InterruptedException | ExecutionException e) { - throw new Error("Error trying to get sequence number", e); - } - } - return sequence; - } - - private void connectToPeer(InetSocketAddress peerAddress, AStore store) throws IOException, TimeoutException { - setConnection(Connection.connect(peerAddress, internalHandler, store)); - } - - /** - * Signs a value on behalf of this client. - * - * @param Type of value to sign - * @param value Value to sign - * @return SignedData instance - */ - public SignedData signData(T value) { - return keyPair.signData(value); - } - - /** - * Gets the Internet address of the currently connected remote - * - * @return Remote socket address - */ - public InetSocketAddress getRemoteAddress() { - return connection.getRemoteAddress(); - } - - /** - * Creates a new account with the given public key - * - * @param publicKey Public key to set for the new account - * @return Address of account created - * @throws TimeoutException If attempt times out - * @throws IOException If IO error occurs - */ - public Address createAccountSync(AccountKey publicKey) throws TimeoutException, IOException { - Invoke trans = Invoke.create(address, 0, "(create-account 0x" + publicKey.toHexString() + ")"); - Result r = transactSync(trans); - if (r.isError()) throw new Error("Error creating account: " + r.getErrorCode()+ " "+r.getValue()); - return (Address) r.getValue(); - } - - /** - * Creates a new account with the given public key - * - * @param publicKey Public key to set for the new account - * @return Address of account created - * @throws TimeoutException If attempt times out - * @throws IOException If IO error occurs - */ - public CompletableFuture
createAccount(AccountKey publicKey) throws TimeoutException, IOException { - Invoke trans = Invoke.create(address, 0, "(create-account 0x" + publicKey.toHexString() + ")"); - CompletableFuture fr = transact(trans); - return fr.thenApply(r->r.getValue()); - } - - /** - * Checks if this Convex client instance has an open connection. - * - * @return true if connected, false otherwise - */ - public boolean isConnected() { - Connection c = this.connection; - return (c != null) && (!c.isClosed()); - } - - /** - * Gets the underlying Connection instance for this Client. May be null if not - * connected. - * - * @return Connection instance or null - */ - public Connection getConnection() { - return connection; - } - - /** - * Updates the given transaction to have the next sequence number. - * - * @param t Any transaction, for which the correct next sequence number is - * desired - * @return The updated transaction - */ - private synchronized ATransaction applyNextSequence(ATransaction t) { - if (sequence != null) { - // if already we know the next sequence number to be applied, set it - return t.withSequence(++sequence); - } else { - return t.withSequence(getIncrementedSequence()); - } - } - - /** - * Submits a transaction to the Convex network, returning a future once the - * transaction has been successfully queued. Signs the transaction with the - * currently set key pair - * - * Should be thread safe as long as multiple clients do not attempt to submit - * transactions for the same account concurrently. - * - * @param transaction Transaction to execute - * @return A Future for the result of the transaction - * @throws IOException If the connection is broken, or the send buffer is full - */ - public synchronized CompletableFuture transact(ATransaction transaction) throws IOException { - if (transaction.getAddress() == null) { - transaction = transaction.withAddress(address); - } - if (autoSequence || (transaction.getSequence() <= 0)) { - // apply sequence if using expected address - if (Utils.equals(transaction.getAddress(), address)) { - transaction = applyNextSequence(transaction); - } else { - // ignore?? - } - } - SignedData signed = keyPair.signData(transaction); - return transact(signed); - } - - /** - * Submits a signed transaction to the Convex network, returning a future once - * the transaction has been successfully queued. - * - * @param signed Signed transaction to execute - * @return A Future for the result of the transaction - * @throws IOException If the connection is broken - */ - public synchronized CompletableFuture transact(SignedData signed) throws IOException { - CompletableFuture cf; - long id = -1; - - synchronized (awaiting) { - // loop until request is queued - while (id < 0) { - id = connection.sendTransaction(signed); - if (id<0) { - try { - Thread.sleep(10); - } catch (InterruptedException e) { - // Ignore - } - } - } - - // Store future for completion by result message - cf = awaitResult(id); - } - - log.debug("Sent transaction with message ID: {} awaiting count = {}",id,awaiting.size()); - return cf; - } - - /** - * Submits a transfer transaction to the Convex network, returning a future once - * the transaction has been successfully queued. - * - * @param target Destination address for transfer - * @param amount Amount of Convex Coins to transfer - * @return A Future for the result of the transaction - * @throws IOException If the connection is broken, or the send buffer is full - */ - public CompletableFuture transfer(Address target, long amount) throws IOException { - ATransaction trans = Transfer.create(getAddress(), 0, target, amount); - return transact(trans); - } - - /** - * Submits a transfer transaction to the Convex network peer, and waits for - * confirmation of the result - * - * @param target Destination address for transfer - * @param amount Amount of Convex Coins to transfer - * @return Result of the transaction - * @throws IOException If the connection is broken, or the send buffer is - * full - * @throws TimeoutException If the transaction times out - */ - public Result transferSync(Address target, long amount) throws IOException, TimeoutException { - ATransaction trans = Transfer.create(getAddress(), 0, target, amount); - return transactSync(trans); - } - - /** - * Submits a transaction synchronously to the Convex network, returning a Result - * - * @param transaction Transaction to execute - * @return The result of the transaction - * @throws IOException If the connection is broken - * @throws TimeoutException If the attempt to transact with the network is not - * confirmed within a reasonable time - */ - public Result transactSync(SignedData transaction) throws TimeoutException, IOException { - return transactSync(transaction, timeout); - } - - /** - * Submits a transaction synchronously to the Convex network, returning a Result - * - * @param transaction Transaction to execute - * @return The result of the transaction - * @throws IOException If the connection is broken - * @throws TimeoutException If the attempt to transact with the network is not - * confirmed within a reasonable time - */ - public Result transactSync(ATransaction transaction) throws TimeoutException, IOException { - return transactSync(transaction, timeout); - } - - /** - * Submits a signed transaction synchronously to the Convex network, returning a - * Result - * - * @param transaction Transaction to execute - * @param timeout Number of milliseconds for timeout - * @return The result of the transaction - * @throws IOException If the connection is broken - * @throws TimeoutException If the attempt to transact with the network is not - * confirmed by the specified timeout - */ - public Result transactSync(ATransaction transaction, long timeout) throws TimeoutException, IOException { - // sample time at start of transaction attempt - long start = Utils.getTimeMillis(); - - Future cf = transact(transaction); - - // adjust timeout if time elapsed to submit transaction - long now = Utils.getTimeMillis(); - timeout = Math.max(0L, timeout - (now - start)); - try { - Result r = cf.get(timeout, TimeUnit.MILLISECONDS); - return r; - } catch (InterruptedException | ExecutionException e) { - throw new Error("Not possible? Since there is no Thread for the future....", e); - } - } - - /** - * Submits a signed transaction synchronously to the Convex network, returning a - * Result - * - * @param transaction Transaction to execute - * @param timeout Number of milliseconds for timeout - * @return The result of the transaction - * @throws IOException If the connection is broken - * @throws TimeoutException If the attempt to transact with the network is not - * confirmed by the specified timeout - */ - public Result transactSync(SignedData transaction, long timeout) - throws TimeoutException, IOException { - // sample time at start of transaction attempt - long start = Utils.getTimeMillis(); - - Future cf = transact(transaction); - - // adjust timeout if time elapsed to submit transaction - long now = Utils.getTimeMillis(); - timeout = Math.max(0L, timeout - (now - start)); - try { - return cf.get(timeout, TimeUnit.MILLISECONDS); - } catch (InterruptedException | ExecutionException e) { - throw new Error("Not possible? Since there is no Thread for the future....", e); - } - } - - /** - * Submits a query to the Convex network, returning a Future once the query has - * been successfully queued. - * - * @param query Query to execute, as a Form or Op - * @return A Future for the result of the query - * @throws IOException If the connection is broken, or the send buffer is full - */ - public Future query(ACell query) throws IOException { - return query(query, getAddress()); - } - - /** - * Attempts to acquire a complete persistent data structure for the given hash - * from the remote peer. Uses the store configured for the calling thread. - * - * @param hash Hash of value to acquire. - * - * @return Future for the cell being acquired - */ - public Future acquire(Hash hash) { - return acquire(hash, Stores.current()); - } - - /** - * Attempts to acquire a complete persistent data structure for the given hash - * from the remote peer. Uses the store provided as a destination. - * - * @param hash Hash of value to acquire. - * @param store Store to acquire the persistent data to. - * - * @return Future for the Cell being acquired - */ - public Future acquire(Hash hash, AStore store) { - CompletableFuture f = new CompletableFuture(); - new Thread(new Runnable() { - @Override - public void run() { - Stores.setCurrent(store); // use store for calling thread - try { - Ref ref = store.refForHash(hash); - HashSet missingSet = new HashSet<>(); - while (!f.isDone()) { - missingSet.clear(); - - if (ref == null) { - missingSet.add(hash); - } else { - if (ref.getStatus() >= Ref.PERSISTED) { - // we have everything! - f.complete(ref.getValue()); - return; - } - ref.findMissing(missingSet); - } - for (Hash h : missingSet) { - // send missing data requests until we fill pipeline - log.debug( "Request missing data: {}" , h); - boolean sent = connection.sendMissingData(h); - if (!sent) { - log.debug("Send Queue full!"); - break; - } - } - // if too low, can send multiple requests, and then block the peer - Thread.sleep(100); - ref = store.refForHash(hash); - if (ref != null) { - if (ref.getStatus() >= Ref.PERSISTED) { - // we have everything! - f.complete(ref.getValue()); - return; - } - // maybe complete, but not sure - try { - ref = ref.persist(); - f.complete(ref.getValue()); - } catch (MissingDataException e) { - Hash missing = e.getMissingHash(); - log.debug("Still missing: {}", missing); - connection.sendMissingData(missing); - } - } - } - } catch (Throwable t) { - // catch any errors, probably IO? - f.completeExceptionally(t); - } - } - }).start(); - return f; - } - - /** - * Request status using a sync operation. This request will automatically get any missing data with the status request - * - * @param timeoutMillis Milliseconds to wait for request timeout - * @return Status Vector from target Peer - * - * @throws IOException If an IO Error occurs - * @throws InterruptedException If execution is interrupted - * @throws ExecutionException If a concurrent execution failure occurs - * @throws TimeoutException If operation times out - * - */ - @SuppressWarnings("unchecked") - public AVector requestStatusSync(long timeoutMillis) throws IOException, InterruptedException, ExecutionException, TimeoutException { - AVector status = null; - int retryCount = 10; - Future statusFuture=requestStatus(); - while (status == null && retryCount > 0 ) { - try { - status=statusFuture.get(timeoutMillis,TimeUnit.MILLISECONDS).getValue(); - } catch (MissingDataException e) { - status = (AVector) acquire(e.getMissingHash()).get(timeoutMillis,TimeUnit.MILLISECONDS); - } - retryCount -= 1; - } - return status; - } - - /** - * Submits a status request to the Convex network peer, returning a Future once the - * request has been successfully queued. - * - * @return A Future for the result of the requestStatus - * @throws IOException If the connection is broken, or the send buffer is full - */ - public Future requestStatus() throws IOException { - synchronized (awaiting) { - long id = connection.sendStatusRequest(); - if (id < 0) { - throw new IOException("Failed to send status request due to full buffer"); - } - - // TODO: ensure status is fully loaded - // Store future for completion by result message - CompletableFuture cf = awaitResult(id); - - return cf; - } - } - - /** - * Method to await a complete result. Should be called with lock on `awaiting` map - * @param - * @param id - * @return - */ - protected CompletableFuture awaitResult(long id) { - CompletableFuture cf = new CompletableFuture(); - awaiting.put(id,cf); - return cf; - } - - /** - * Request a challenge. This is request is made by any peer that needs to find out - * if another peer can be trusted. - * - * @param data Signed data to send to the peer for the challenge. - * - * @return A Future for the result of the requestChallenge - * - * @throws IOException if the connection fails. - * - */ - public Future requestChallenge(SignedData data) throws IOException { - synchronized (awaiting) { - long id = connection.sendChallenge(data); - if (id < 0) { - // TODO: too fragile? - throw new IOException("Failed to send challenge due to full buffer"); - } - - // Store future for completion by result message - return awaitResult(id); - } - } - - /** - * Submits a query to the Convex network, returning a Future once the query has - * been successfully queued. - * - * @param query Query to execute, as a Form or Op - * @param address Address to use for the query - * @return A Future for the result of the query - * @throws IOException If the connection is broken, or the send buffer is full - */ - public Future query(ACell query, Address address) throws IOException { - synchronized (awaiting) { - long id = connection.sendQuery(query, address); - if (id < 0) { - throw new IOException("Failed to send query due to full buffer"); - } - - return awaitResult(id); - } - } - - /** - * Executes a query synchronously and waits for the Result - * @param query Query to execute. Map be a form or Op - * @return Result of synchronous query - * @throws TimeoutException If the synchronous request timed out - * @throws IOException In case of network error - */ - public Result querySync(ACell query) throws TimeoutException, IOException { - return querySync(query, getAddress()); - } - - /** - * Executes a query synchronously and waits for the Result - * - * @param timeoutMillis Timeout to wait for query result. Will throw - * TimeoutException if not received in this time - * @param query Query to execute, as a Form or Op - * @return Result of query - * @throws TimeoutException If the synchronous request timed out - * @throws IOException In case of network error - */ - public Result querySync(ACell query, long timeoutMillis) throws IOException, TimeoutException { - return querySync(query, getAddress(), timeoutMillis); - } - - /** - * Executes a query synchronously and waits for the Result - * - * @param address Address to use for the query - * @param query Query to execute, as a Form or Op - * @return Result of query - * @throws TimeoutException If the synchronous request timed out - * @throws IOException In case of network error - */ - public Result querySync(ACell query, Address address) throws IOException, TimeoutException { - return querySync(query, address, timeout); - } - - /** - * Executes a query synchronously and waits for the Result - * - * @param timeoutMillis Timeout to wait for query result. Will throw - * TimeoutException if not received in this time - * @param address Address to use for the query - * @param query Query to execute, as a Form or Op - * @return Result of query - * @throws TimeoutException If the synchronous request timed out - * @throws IOException In case of network error - */ - public Result querySync(ACell query, Address address, long timeoutMillis) throws TimeoutException, IOException { - Future cf = query(query, address); - - try { - return cf.get(timeoutMillis, TimeUnit.MILLISECONDS); - } catch (InterruptedException | ExecutionException e) { - throw new Error("Not possible? Since there is no Thread for the future....", e); - } - } - - /** - * Returns the current AcountKey for the client using the API. - * - * @return AcountKey instance - */ - public AccountKey getAccountKey() { - return keyPair.getAccountKey(); - } - - /** - * Returns the current Address for the client using the API. - * - * @return Address instance - */ - public Address getAddress() { - return address; - } - - /** - * Sets the current Connection for this Client - * - * @param conn Connection value to use - */ - private void setConnection(Connection conn) { - if (this.connection == conn) return; - close(); - this.connection = conn; - } - - /** - * Disconnects the client from the network, closing the underlying connection. - */ - public synchronized void close() { - Connection c = this.connection; - if (c != null) { - c.close(); - } - connection = null; - awaiting.clear(); - } - - @Override - public void finalize() { - close(); - } - - /** - * Determines if this Client is configured to automatically generate sequence - * numbers - * - * @return - */ - protected boolean isAutoSequence() { - return autoSequence; - } - - /** - * Configures auto-generation of sequence numbers - * - * @param autoSequence true to enable auto-sequencing, false otherwise - */ - protected void setAutoSequence(boolean autoSequence) { - this.autoSequence = autoSequence; - } - - public Long getBalance(Address address) throws IOException { - try { - Future future = query(Reader.read("(balance " + address.toString() + ")")); - Result result = future.get(timeout, TimeUnit.MILLISECONDS); - if (result.isError()) throw new Error(result.toString()); - CVMLong bal = (CVMLong) result.getValue(); - return bal.longValue(); - } catch (ExecutionException | InterruptedException | TimeoutException ex) { - throw new IOException("Unable to query balance", ex); - } - } - - /** - * Connect to a local Server, using the Peer's address and keypair - * @param server Server to connect to - * @return New Client Connection - * @throws TimeoutException If connection attempt times out - * @throws IOException If IO error occurs - */ - public static Convex connect(Server server) throws IOException, TimeoutException { - return connect(server.getHostAddress(),server.getPeerController(),server.getKeyPair()); - } - - /** - * Wraps a connection as a Convex client instance - * @param c Connection to wrap - * @return New Convex client instance using underlying connection - */ - public static Convex wrap(Connection c) { - Convex convex=new Convex(null,null); - convex.setConnection(c); - return convex; - } - - /** - * Gets the consensus state from the remote Peer - * @return Future for consensus state - * @throws TimeoutException If initial status request times out - */ - public Future acquireState() throws TimeoutException { - try { - Future sF=requestStatus(); - AVector status=sF.get(timeout, TimeUnit.MILLISECONDS).getValue(); - Hash stateHash=RT.ensureHash(status.get(4)); - - if (stateHash==null) throw new Error("Bad status response from Peer"); - return acquire(stateHash); - } catch (InterruptedException|ExecutionException|IOException e) { - throw Utils.sneakyThrow(e); - } - } - - /** - * Close without affecting the connection - */ - public void closeButMaintainConnection() { - this.connection=null; - close(); - } - - - - -} diff --git a/convex-peer/src/main/java/convex/net/Connection.java b/convex-peer/src/main/java/convex/net/Connection.java deleted file mode 100644 index 263c0f1a1..000000000 --- a/convex-peer/src/main/java/convex/net/Connection.java +++ /dev/null @@ -1,770 +0,0 @@ -package convex.net; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.net.StandardSocketOptions; -import java.nio.ByteBuffer; -import java.nio.channels.ByteChannel; -import java.nio.channels.CancelledKeyException; -import java.nio.channels.Channel; -import java.nio.channels.ClosedChannelException; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.SocketChannel; -import java.util.Iterator; -import java.util.Random; -import java.util.Set; -import java.util.concurrent.TimeoutException; -import java.util.function.Consumer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import convex.core.Constants; -import convex.core.Result; -import convex.core.data.ACell; -import convex.core.data.AccountKey; -import convex.core.data.AVector; -import convex.core.data.Address; -import convex.core.data.Format; -import convex.core.data.Hash; -import convex.core.data.IRefFunction; -import convex.core.data.SignedData; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.store.AStore; -import convex.core.store.Stores; -import convex.core.transactions.ATransaction; -import convex.core.util.Counters; -import convex.core.util.Utils; - -/** - *

- * Class representing a low-level Connection between network participants. - *

- * - *

- * Sent messages are sent asynchronously via the shared client selector. - *

- * - *

- * Received messages are read by the shared client selector, converted into - * Message instances, and passed to a Consumer for handling. - *

- * - *

- * A Connection "owns" the ByteChannel associated with this Peer connection - *

- */ -@SuppressWarnings("unused") -public class Connection { - - final ByteChannel channel; - - /** - * Counter for IDs of all messages sent from this JVM - */ - private static long idCounter = 0; - - /** - * Store to use for this connection. Required for responding to incoming - * messages. - */ - private final AStore store; - - /** - * If trusted, the Account Key of the remote peer. - */ - private AccountKey trustedPeerKey; - - private static final Logger log = LoggerFactory.getLogger(Connection.class.getName()); - - /** - * Pre-allocated direct buffer for message sending TODO: is one per connection - * OK? Users should synchronise on this briefly while building message. - */ - private final ByteBuffer frameBuf = ByteBuffer.allocateDirect(Format.LIMIT_ENCODING_LENGTH + 20); - - private final MessageReceiver receiver; - private final MessageSender sender; - - private Connection(ByteChannel clientChannel, Consumer receiveAction, AStore store, - AccountKey trustedPeerKey) { - this.channel = clientChannel; - receiver = new MessageReceiver(receiveAction, this); - sender = new MessageSender(clientChannel); - this.store = store; - this.trustedPeerKey = trustedPeerKey; - } - - /** - * Create a PeerConnection using an existing channel. Does not perform any - * connection initialisation: channel should already be connected. - * - * @param channel Byte channel to wrap - * @param receiveAction Consumer to be called when a Message is received - * @param store Store to use when receiving messages. - * @param trustedPeerKey Trusted peer account key if this is a trusted - * connection, if not then null* - * @return New Connection instance - * @throws IOException If IO error occurs - */ - public static Connection create(ByteChannel channel, Consumer receiveAction, AStore store, - AccountKey trustedPeerKey) throws IOException { - return new Connection(channel, receiveAction, store, trustedPeerKey); - } - - /** - * Gets the global message ID counter - * @return Message ID counter for last message sent - */ - public static long getCounter() { - return idCounter; - } - - /** - * Create a PeerConnection by connecting to a remote address - * - * @param hostAddress Internet Address to connect to - * @param receiveAction A callback Consumer to be called for any received - * messages on this connection - * @param store Store to use for this Connection - * @return New Connection instance - * @throws IOException If connection fails because of any IO problem - * @throws TimeoutException If connection cannot be established within an - * acceptable time (~5s) - */ - public static Connection connect(InetSocketAddress hostAddress, Consumer receiveAction, AStore store) - throws IOException, TimeoutException { - return connect(hostAddress, receiveAction, store, null); - } - - /** - * Create a Connection by connecting to a remote address - * - * @param hostAddress Internet Address to connect to - * @param receiveAction A callback Consumer to be called for any received - * messages on this connection - * @param store Store to use for this Connection - * @param trustedPeerKey Trusted peer account key if this is a trusted - * connection, if not then null - * @return New Connection instance - * @throws IOException If connection fails because of any IO problem - * @throws TimeoutException If the connection cannot be established within the - * timeout period - */ - public static Connection connect(InetSocketAddress hostAddress, Consumer receiveAction, AStore store, - AccountKey trustedPeerKey) throws IOException, TimeoutException { - return connect(hostAddress,receiveAction,store,trustedPeerKey,Constants.SOCKET_SEND_BUFFER_SIZE,Constants.SOCKET_RECEIVE_BUFFER_SIZE); - } - - /** - * Create a Connection by connecting to a remote address - * - * @param hostAddress Internet Address to connect to - * @param receiveAction A callback Consumer to be called for any received - * messages on this connection - * @param store Store to use for this Connection - * @param trustedPeerKey Trusted peer account key if this is a trusted - * connection, if not then null - * @param sendBufferSize Size of connection send buffer in bytes - * @param receiveBufferSize Size of connection receive buffer in bytes - * @return New Connection instance - * @throws IOException If connection fails because of any IO problem - * @throws TimeoutException If the connection cannot be established within the - * timeout period - */ - public static Connection connect(InetSocketAddress hostAddress, Consumer receiveAction, AStore store, - AccountKey trustedPeerKey, int sendBufferSize, int receiveBufferSize) throws IOException, TimeoutException { - if (store == null) - throw new Error("Connection requires a store"); - SocketChannel clientChannel = SocketChannel.open(); - clientChannel.configureBlocking(false); - clientChannel.socket().setReceiveBufferSize(receiveBufferSize); - clientChannel.socket().setSendBufferSize(sendBufferSize); - - // TODO: reconsider this - clientChannel.socket().setTcpNoDelay(true); - clientChannel.connect(hostAddress); - - long start = Utils.getCurrentTimestamp(); - while (!clientChannel.finishConnect()) { - long now = Utils.getCurrentTimestamp(); - long elapsed=now-start; - if (elapsed > Constants.DEFAULT_CLIENT_TIMEOUT) - throw new TimeoutException("Couldn't connect"); - try { - Thread.sleep(10+elapsed/5); - } catch (InterruptedException e) { - throw new IOException("Connect interrupted", e); - } - } - - Connection pc = create(clientChannel, receiveAction, store, trustedPeerKey); - pc.startClientListening(); - log.debug("Connect succeeded for host: {}", hostAddress); - return pc; - } - - public long getReceivedCount() { - return receiver.getReceivedCount(); - } - - /** - * Returns the remote SocketAddress associated with this connection, or null if - * not available - * - * @return An InetSocketAddress if associated, otherwise null - */ - public InetSocketAddress getRemoteAddress() { - if (!(channel instanceof SocketChannel)) - return null; - try { - return (InetSocketAddress) ((SocketChannel) channel).getRemoteAddress(); - } catch (Exception e) { - // anything fails, we have no address - return null; - } - } - - /** - * Gets the store associated with this Connection - * @return Store instance - */ - public AStore getStore() { - return store; - } - - /** - * Returns the local SocketAddress associated with this connection, or null if - * not available - * - * @return A SocketAddress if associated, otherwise null - */ - public InetSocketAddress getLocalAddress() { - if (!(channel instanceof SocketChannel)) - return null; - try { - return (InetSocketAddress) ((SocketChannel) channel).getLocalAddress(); - } catch (Exception e) { - // anything fails, we have no address - return null; - } - - } - - /** - * Sends a DATA Message on this connection. - * - * Does not send embedded values. - * - * @param value Any data object, which will be encoded and sent as a single cell - * @return true if buffered successfully, false otherwise (not sent) - * @throws IOException If IO error occurs - */ - public boolean sendData(ACell value) throws IOException { - log.trace("Sending data: {}", value); - ByteBuffer buf = Format.encodedBuffer(value); - return sendBuffer(MessageType.DATA, buf); - } - - /** - * Sends a DATA Message on this connection. - * - * @param value Any data object - * @return true if buffered successfully, false otherwise (not sent) - * @throws IOException If IO error occurs - */ - public boolean sendMissingData(Hash value) throws IOException { - log.trace("Requested missing data for hash {} with store {}", value.toHexString(), Stores.current()); - return sendObject(MessageType.MISSING_DATA, value); - } - - /** - * Sends a QUERY Message on this connection with a null Address - * - * @param form A data object representing the query form - * @return The ID of the message sent, or -1 if send buffer is full. - * @throws IOException If IO error occurs - */ - public long sendQuery(ACell form) throws IOException { - return sendQuery(form, null); - } - - /** - * Sends a QUERY Message on this connection. - * - * @param form A data object representing the query form - * @param address The address with which to run the query, which may be null - * @return The ID of the message sent, or -1 if send buffer is full. - * @throws IOException If IO error occurs - */ - public long sendQuery(ACell form, Address address) throws IOException { - AStore temp = Stores.current(); - try { - long id = ++idCounter; - AVector v = Vectors.of(id, form, address); - boolean sent = sendObject(MessageType.QUERY, v); - return sent ? id : -1; - } finally { - Stores.setCurrent(temp); - } - - } - - /** - * Sends a STATUS Request Message on this connection. - * - * @return The ID of the message sent, or -1 if send buffer is full. - * @throws IOException If IO error occurs - */ - public long sendStatusRequest() throws IOException { - AStore temp = Stores.current(); - try { - long id = ++idCounter; - CVMLong idPayload = CVMLong.create(id); - sendObject(MessageType.STATUS, idPayload); - return id; - } finally { - Stores.setCurrent(temp); - } - } - - /** - * Sends a CHALLENGE Request Message on this connection. - * - * @param challenge Challenge a Vector that has been signed by the sending peer. - * - * @return The ID of the message sent, or -1 if the message cannot be sent. - * - * @throws IOException If IO error occurs - * - */ - public long sendChallenge(SignedData challenge) throws IOException { - AStore temp = Stores.current(); - try { - long id = ++idCounter; - boolean sent = sendObject(MessageType.CHALLENGE, challenge); - return (sent) ? id : -1; - } finally { - Stores.setCurrent(temp); - } - } - - /** - * Sends a RESPONSE Request Message on this connection. - * - * @param response Signed response for the remote peer - * @return The ID of the message sent, or -1 if the message cannot be sent. - * - * @throws IOException If IO error occurs - * - */ - public long sendResponse(SignedData response) throws IOException { - AStore temp = Stores.current(); - try { - long id = ++idCounter; - boolean sent = sendObject(MessageType.RESPONSE, response); - return (sent) ? id : -1; - } finally { - Stores.setCurrent(temp); - } - } - - /** - * Sends a transaction if possible, returning the message ID (greater than zero) - * if successful. - * - * Uses the configured CLIENT_STORE to store the transaction, so that any - * missing data requests from the server can be honoured. - * - * Returns -1 if the message could not be sent because of a full buffer. - * - * @param signed Signed transaction - * @return Message ID of the transaction request, or -1 if send buffer is full. - * @throws IOException In the event of an IO error, e.g. closed connection - */ - public long sendTransaction(SignedData signed) throws IOException { - AStore temp = Stores.current(); - try { - Stores.setCurrent(store); - long id = ++idCounter; - AVector v = Vectors.of(id, signed); - boolean sent = sendObject(MessageType.TRANSACT, v); - return (sent) ? id : -1; - } finally { - Stores.setCurrent(temp); - } - } - - /** - * Sends a RESULT Message on this connection with no error code (i.e. a success) - * - * @param id ID for result message - * @param value Any data object - * @return True if buffered for sending successfully, false otherwise - * @throws IOException If IO error occurs - */ - public boolean sendResult(CVMLong id, ACell value) throws IOException { - return sendResult(id, value, null); - } - - /** - * Sends a RESULT Message on this connection. - * - * @param id ID for result message - * @param value Any data object - * @param errorCode Error code for this result. May be null to indicate success - * @return True if buffered for sending successfully, false otherwise - * @throws IOException In case of IO Error - */ - public boolean sendResult(CVMLong id, ACell value, ACell errorCode) throws IOException { - Result result = Result.create(id, value, errorCode); - return sendObject(MessageType.RESULT, result); - } - - /** - * Sends a RESULT Message on this connection. - * - * @param result Result data structure - * @return true if message queued successfully, false otherwise - * @throws IOException If IO error occurs - */ - public boolean sendResult(Result result) throws IOException { - return sendObject(MessageType.RESULT, result); - } - - private IRefFunction sender() { - return sendAll; - } - - private final IRefFunction sendAll = (r -> { - // TODO: halt conditions to prevent sending the whole universe - ACell o = r.getValue(); - if (o == null) - return r; - - // send children first - o.updateRefs(sender()); - - // only send this value if not embedded - if (!o.isEmbedded()) { - try { - sendData(o); - } catch (IOException e) { - throw Utils.sneakyThrow(e); - } - } - - return r; - }); - - /** - * Sends a message over this connection - * - * @param msg Message to send - * @return true if message buffered successfully, false if failed - * @throws IOException If IO error occurs - */ - public boolean sendMessage(Message msg) throws IOException { - return sendObject(msg.getType(), msg.getPayload()); - } - - /** - * Sends a payload for the given message type. Should be called on the thread - * that responds to missing data messages from the destination. - * - * @param type Type of message - * @param payload Payload value for message - * @return true if message queued successfully, false otherwise - * @throws IOException If IO error occurs - */ - public boolean sendObject(MessageType type, ACell payload) throws IOException { - Counters.sendCount++; - - // Need to ensure message is persisted at least, so we can respond to missing - // data messages using the current thread store - // We pre-send any novelty to the destination - ACell sendVal = payload; - ACell.createPersisted(sendVal, r -> { - try { - ACell data = r.getValue(); - if (data==sendVal) return; // skip sending top payload - if (!Format.isEmbedded(data)) sendData(data); - } catch (IOException e) { - throw Utils.sneakyThrow(e); - } - }); - - ByteBuffer buf = Format.encodedBuffer(sendVal); - if (log.isTraceEnabled()) { - log.trace("Sending message: " + type + " :: " + payload + " to " + getRemoteAddress() + " format: " - + Format.encodedBlob(payload).toHexString()); - } - boolean sent = sendBuffer(type, buf); - return sent; - } - - /** - * Sends a message with the given message type and data buffer. - * - * @param type MessageType value - * @param buf Buffer containing raw wire data for the message - * @return true if message sent, false otherwise - * @throws IOException - */ - private boolean sendBuffer(MessageType type, ByteBuffer buf) throws IOException { - int dataLength = buf.remaining(); - - // Total length field is message code + encoded object length - int messageLength = dataLength + 1; - boolean sent; - int headerLength; - - // synchronize in case we are sending messages from different threads - // This is OK but need to avoid corrupted messages. - synchronized (frameBuf) { - // ensure frameBuf is clear and ready for writing - frameBuf.clear(); - - // write message header - Format.writeMessageLength(frameBuf, messageLength); - frameBuf.put(type.getMessageCode()); - headerLength = frameBuf.position(); - - // now write message - frameBuf.put(buf); - frameBuf.flip(); // ensure frameBuf is ready to write to channel - - sent = sender.bufferMessage(frameBuf); - } - - if (sent) { - if (channel instanceof SocketChannel) { - SocketChannel chan = (SocketChannel) channel; - // register interest in both reads and writes - try { - chan.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, this); - } catch (CancelledKeyException e) { - // ignore. Must have got cancelled elsewhere? - } - // wake up selector - selector.wakeup(); - } - - if (log.isTraceEnabled()) { - log.trace("Sent message " + type + " of length: " + dataLength + " Connection ID: " - + System.identityHashCode(this)); - } - } else { - log.debug("sendBuffer failed with message {} of length: {} Connection ID: {}" - , type, dataLength, System.identityHashCode(this)); - } - return sent; - } - - public synchronized void close() { - if (channel != null) { - try { - channel.close(); - } catch (IOException e) { - // TODO OK to ignore? - } - } - } - - /** - * Checks if this connection is closed (i.e. the underlying channel is closed) - * - * @return true if the channel is closed, false otherwise. - */ - public boolean isClosed() { - return !channel.isOpen(); - } - - /** - * Starts listening for received events with this given peer connection. - * PeerConnection must have a selectable SocketChannel associated - * - * @throws IOException If IO error occurs - */ - private void startClientListening() throws IOException { - SocketChannel chan = (SocketChannel) channel; - chan.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, this); - - // start running selector loop after we register for reading! - ensureSelectorLoop(); - - // seems to be needed to ensure selector sees new connection? - selector.wakeup(); - } - - private static final Selector selector; - - static { - try { - selector = Selector.open(); - } catch (IOException e) { - throw new Error(e); - } - } - - public void wakeUp() { - selector.wakeup(); - } - - private static Thread loopThread; - - private static void ensureSelectorLoop() { - // double checked initialisation - if (loopThread == null) { - synchronized (Connection.class) { - if (loopThread == null) { - loopThread = new Thread(selectorLoop, "PeerConnection NIO client selector loop"); - // make this a daemon thread so it shuts down if everything else exits - loopThread.setDaemon(true); - loopThread.start(); - } - } - } - } - - private static Runnable selectorLoop = new Runnable() { - @Override - public void run() { - - log.debug("Client selector loop started"); - while (true) { - try { - selector.select(1000); - Set keys = selector.selectedKeys(); - Iterator it = keys.iterator(); - while (it.hasNext()) { - final SelectionKey key = it.next(); - it.remove(); // always remove key from selection set - - // log.finest("PeerConnection key received: "+key); - if (!key.isValid()) { - continue; - } - - try { - if (key.isReadable()) { - selectRead(key); - } else if (key.isWritable()) { - selectWrite(key); - } - } catch (ClosedChannelException e) { - // channel was closed, just lose the key? - log.debug("Unexpected ChannelClosedException, cancelling key: {}", e); - key.cancel(); - } catch (IOException e) { - log.debug("Unexpected IOException, cancelling key: {}", e); - key.cancel(); - } catch (CancelledKeyException e) { - log.debug("Cancelled key"); - } - } - } catch (Throwable t) { - log.error("Uncaught error in PeerConnection client selector loop: {}", t); - t.printStackTrace(); - } - } - } - }; - - /** - * Handles channel reads from a SelectionKey for the client listener - * - * SECURITY: Called on Connection Selector Thread - * - * @param key - * @throws IOException - */ - protected static void selectRead(SelectionKey key) throws IOException { - Connection conn = (Connection) key.attachment(); - if (conn == null) - throw new Error("No PeerConnection specified"); - - try { - int n = conn.handleChannelRecieve(); - // log.finest("Received bytes: " + n); - } catch (ClosedChannelException e) { - log.debug("Channel closed from: {}", conn.getRemoteAddress()); - key.cancel(); - } catch (BadFormatException e) { - log.warn("Cancelled connection to Peer: Bad data format from: " + conn.getRemoteAddress() + " " - + e.getMessage()); - key.cancel(); - } - } - - /** - * Handles receipt of bytes from the channel on this Connection. - * - * Will switch the current store to the Connection-specific store if required. - * - * SECURITY: Called on NIO Thread (Server or client Connection) - * - * @return The number of bytes read from channel - * @throws IOException If IO error occurs - * @throws BadFormatException If there is an encoding error - */ - public int handleChannelRecieve() throws IOException, BadFormatException { - AStore tempStore = Stores.current(); - try { - // set the current store for handling incoming messages - Stores.setCurrent(store); - return receiver.receiveFromChannel(channel); - } finally { - Stores.setCurrent(tempStore); - } - } - - /** - * Handles writes to the channel. - * - * SECURITY: Called on Selector Thread - * - * @param key Selection Key - * @throws IOException - */ - static void selectWrite(SelectionKey key) throws IOException { - Connection pc = (Connection) key.attachment(); - boolean allSent = pc.sender.maybeSendBytes(); - - if (allSent) { - // deregister interest in writing - key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE); - } else { - // we want to continue writing - } - } - - /** - * Sends bytes buffered into the underlying channel. - * @return True if all bytes are sent, false otherwise - * @throws IOException If an IO Exception occurs - */ - public boolean flushBytes() throws IOException { - return sender.maybeSendBytes(); - } - - @Override - public String toString() { - return "PeerConnection: " + channel; - } - - public AccountKey getTrustedPeerKey() { - return trustedPeerKey; - } - - public void setTrustedPeerKey(AccountKey value) { - trustedPeerKey = value; - } - - public boolean isTrusted() { - return trustedPeerKey != null; - } -} diff --git a/convex-peer/src/main/java/convex/net/MemoryByteChannel.java b/convex-peer/src/main/java/convex/net/MemoryByteChannel.java deleted file mode 100644 index 9176faee8..000000000 --- a/convex-peer/src/main/java/convex/net/MemoryByteChannel.java +++ /dev/null @@ -1,70 +0,0 @@ -package convex.net; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.ByteChannel; -import java.nio.channels.ClosedChannelException; - -/** - * ByteChannel implementation wrapping a fixed size in-memory buffer - * - * - */ -public class MemoryByteChannel implements ByteChannel { - /** - * ByteBuffer for channel contents. - * Maintained ready for writing - */ - private final ByteBuffer memory; - boolean open=true; - - private MemoryByteChannel(ByteBuffer buf) { - this.memory=buf; - } - - public static MemoryByteChannel create(int length) { - ByteBuffer bb=ByteBuffer.allocate(length); - return new MemoryByteChannel(bb); - } - - @Override - public int read(ByteBuffer dst) throws ClosedChannelException { - if (!open) throw new ClosedChannelException(); - synchronized (memory) { - memory.flip(); // position will be 0, limit is available bytes - int available=memory.remaining(); - int numRead=Math.min(available, dst.remaining()); - memory.limit(numRead); - dst.put(memory); - memory.limit(available); - memory.compact(); - return numRead; - } - } - - @Override - public boolean isOpen() { - return open; - } - - @Override - public void close() throws IOException { - open=false; - } - - @Override - public int write(ByteBuffer src) throws IOException { - if (!open) throw new ClosedChannelException(); - synchronized(memory) { - synchronized(src) { - int numPut=Math.min(memory.remaining(), src.remaining()); - int savedLimit=src.limit(); - src.limit(src.position()+numPut); - memory.put(src); - src.limit(savedLimit); - return numPut; - } - } - } - -} diff --git a/convex-peer/src/main/java/convex/net/Message.java b/convex-peer/src/main/java/convex/net/Message.java deleted file mode 100644 index 879602832..000000000 --- a/convex-peer/src/main/java/convex/net/Message.java +++ /dev/null @@ -1,111 +0,0 @@ -package convex.net; - -import convex.core.Belief; -import convex.core.Result; -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.SignedData; -import convex.core.data.prim.CVMLong; -import convex.core.util.Utils; - -/** - *

Class representing a message to / from a specific PeerConnection

- * - *

This class is an immutable data structure, but NOT a representable on-chain - * data structure, as it is part of the peer protocol layer.

- * - *

Messages may contain a Payload, which can be any Data Object.

- */ -public class Message { - - private final Connection connection; - private final ACell payload; - private final MessageType type; - - private Message(Connection peerConnection, MessageType type, ACell payload) { - this.connection = peerConnection; - this.type = type; - this.payload = payload; - } - - public static Message create(Connection peerConnection, MessageType type, ACell payload) { - return new Message(peerConnection, type, payload); - } - - public static Message create(Connection peerConnection, ACell o) { - return create(peerConnection, MessageType.DATA, o); - } - - public static Message createData(ACell o) { - return create(null,MessageType.DATA,o); - } - - public static Message createBelief(SignedData sb) { - return create(null,MessageType.BELIEF,sb); - } - - public static Message createChallenge(SignedData challenge) { - return create(null,MessageType.CHALLENGE, challenge); - } - - public static Message createResponse(SignedData response) { - return create(null,MessageType.RESPONSE, response); - } - - public static Message createGoodBye(SignedData peerKey) { - return create(null,MessageType.GOODBYE, peerKey); - } - - public Connection getConnection() { - return connection; - } - - public Message withConnection(Connection peerConnection) { - return new Message(peerConnection, type, payload); - } - - @SuppressWarnings("unchecked") - public T getPayload() { - return (T) payload; - } - - public MessageType getType() { - return type; - } - - public ACell getErrorCode() { - ACell et=((AVector)payload).get(2); - return et; - } - - @Override - public String toString() { - // TODO. Are tags really needed in `.toString`? - return "#message {:type " + getType() + " :payload " + Utils.print(payload) + "}"; - } - - /** - * Gets the message ID for correlation, assuming this message type supports IDs. - * - * @return Message ID, or null if the message type does not use message IDs - */ - public CVMLong getID() { - switch (type) { - // Query and transact use a vector [ID ...] - case QUERY: - case TRANSACT: return (CVMLong) ((AVector)payload).get(0); - - // Result is a special record type - case RESULT: return (CVMLong)((Result)payload).getID(); - - // Status ID is the single value - case STATUS: return (CVMLong)(payload); - - default: return null; - } - } - - - - -} diff --git a/convex-peer/src/main/java/convex/net/MessageReceiver.java b/convex-peer/src/main/java/convex/net/MessageReceiver.java deleted file mode 100644 index ceb907ab1..000000000 --- a/convex-peer/src/main/java/convex/net/MessageReceiver.java +++ /dev/null @@ -1,174 +0,0 @@ -package convex.net; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.ClosedChannelException; -import java.nio.channels.ReadableByteChannel; -import java.util.function.Consumer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import convex.core.Constants; -import convex.core.data.ABlob; -import convex.core.data.ACell; -import convex.core.data.Blob; -import convex.core.data.Format; -import convex.core.exceptions.BadFormatException; - -/** - * Class responsible for buffered accumulation of messages received over a connection. - * - * ByteBuffers received must be passed in via @receiveFromChannel - * - * Passes any successfully received objects to a specified Consumer, using the same thread on which the - * MessageReceiver was called. - * - *
- *

"There are only two hard problems in distributed systems: 2. Exactly-once - * delivery 1. Guaranteed order of messages 2. Exactly-once delivery" - *

- *
- attributed to Mathias Verraes
- *
- * - * - */ -public class MessageReceiver { - // Receive buffer must be big enough at least for one max sized message plus message header - public static final int RECEIVE_BUFFER_SIZE = Constants.RECEIVE_BUFFER_SIZE; - - /** - * Buffer for receiving partial messages. Maintained ready for writing. - * - * Maybe use a direct buffer since we are copying from the socket channel? But probably doesn't make any difference. - */ - private ByteBuffer buffer = ByteBuffer.allocate(RECEIVE_BUFFER_SIZE); - - private final Consumer action; - private final Connection connection; - - private long receivedMessageCount = 0; - - private static final Logger log = LoggerFactory.getLogger(MessageReceiver.class.getName()); - - public MessageReceiver(Consumer receiveAction, Connection pc) { - this.action = receiveAction; - this.connection = pc; - } - - public Consumer getAction() { - return action; - } - - /** - * Get the number of messages received in total by this Receiver - * @return Count of messages received - */ - public long getReceivedCount() { - return receivedMessageCount; - } - - /** - * Handles receipt of bytes from a channel. Should be called with a - * ReadableByteChannel containing bytes received. - * - * May be called multiple times during receipt of a single message, i.e. can - * handle partial message receipt. - * - * Will consume enough bytes from channel to handle exactly one message. Bytes - * will be left unconsumed on the channel if more are available. - * - * This hopefully - * creates sufficient backpressure on clients sending a lot of messages. - * - * @param chan Byte channel - * @throws IOException If IO error occurs - * @return The number of bytes read from the channel - * @throws BadFormatException If a bad encoding is received - */ - public synchronized int receiveFromChannel(ReadableByteChannel chan) throws IOException, BadFormatException { - int numRead=0; - - // first read a message length - if (buffer.position()<2) { - buffer.limit(2); - numRead = chan.read(buffer); - - if (numRead < 0) throw new ClosedChannelException(); - - // exit if we don't have at least 2 bytes for message length (may also be a message code) - if (buffer.position()<2) return numRead; - } - - // peek message length at start of buffer. May throw BFE. - int len = Format.peekMessageLength(buffer); - int lengthLength = (len < 64) ? 1 : 2; - - // limit buffer to total message frame size including length - int totalFrameSize=lengthLength + len; - buffer.limit(totalFrameSize); - - // try to read more bytes up to limit of total message size - { - int n=chan.read(buffer); - if (n < 0) throw new ClosedChannelException(); - numRead+=n; - } - - // exit if we are still waiting for more bytes - if (buffer.hasRemaining()) return numRead; - - // Log.debug("Message received with length: "+len); - buffer.flip(); // prepare for read - - // position buffer ready to receive message content (i.e. skip length - // field). We still want to include the message code. - buffer.position(lengthLength); - byte mType=buffer.get(); - MessageType type=MessageType.decode(mType); - - byte[] bs=new byte[len-1]; // message length after type byte - buffer.get(bs); - assert(!buffer.hasRemaining()); // should consume entire buffer! - Blob encoding=Blob.wrap(bs); - - receiveMessage(type, encoding); - - // clear buffer - buffer.clear(); - return numRead; - } - - /** - * Reads exactly one message from the ByteBuffer, checking that the position is - * advanced as expected. Buffer must contain sufficient bytes for given message length. - * - * Expects a message code at the buffer's current position. - * - * Calls the receive action with the message if successfully received. Should be called with - * the correct store for this Connection. - * - * SECURITY: Gets called on NIO server thread - * - * @throws BadFormatException if the message is incorrectly formatted` - */ - private void receiveMessage(MessageType type, ABlob encoding) throws BadFormatException { - - ACell payload = connection.getStore().decode(encoding); - - Message message = Message.create(connection, type, payload); - receivedMessageCount++; - if (action != null) { - try { - log.trace("Message received: {}", message.getType()); - action.accept(message); - } catch (Throwable e) { - log.warn("Exception not handled from: " + connection.getRemoteAddress()); - e.printStackTrace(); - } - } else { - log.warn("Ignored message because no receive action set: " + message); - } - } - -} diff --git a/convex-peer/src/main/java/convex/net/MessageSender.java b/convex-peer/src/main/java/convex/net/MessageSender.java deleted file mode 100644 index d4853f476..000000000 --- a/convex-peer/src/main/java/convex/net/MessageSender.java +++ /dev/null @@ -1,81 +0,0 @@ -package convex.net; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.ByteChannel; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import convex.core.Constants; - -/** - * Message sender responsible for moving bytes from a ByteBuffer to a ByteChannel - * - * Must call maybeSendBytes to attempt to flush buffer to channel. - */ -public class MessageSender { - public static final int SEND_BUFFER_SIZE = Constants.SEND_BUFFER_SIZE; - - private final ByteChannel channel; - - /** - * Buffer for send bytes. Retained in a state ready for reading, so we flip on - * initialisation. Must be accessed holding lock on buffer. - */ - private final ByteBuffer buffer = ByteBuffer.allocate(SEND_BUFFER_SIZE).flip(); - - protected static final Logger log = LoggerFactory.getLogger(MessageSender.class.getName()); - - public MessageSender(ByteChannel channel) { - this.channel = channel; - } - - /** - * Buffers a message for sending. - * - * @param messageFrame Source ByteBuffer containing complete message bytes (including length) - * @return True if successfully buffered, false otherwise (insufficient send buffer - * size) - */ - public boolean bufferMessage(ByteBuffer messageFrame) { - synchronized (buffer) { - // compact buffer, ready for writing - buffer.compact(); - - // return false if insufficient space to send - if (buffer.remaining() < messageFrame.remaining()) { - // flip to maintain readiness for writing - buffer.flip(); - return false; - } - buffer.put(messageFrame); - // flip so ready for reading once again - buffer.flip(); - } - return true; - } - - /** - * Try to send bytes on the outbound channel. - * - * @return True if all bytes have been sent, false otherwise. - * @throws IOException If IO error occurs - */ - public boolean maybeSendBytes() throws IOException { - synchronized (buffer) { - if (!buffer.hasRemaining()) return true; - - // write to channel if possible. May write zero or more bytes - channel.write(buffer); - - if (buffer.hasRemaining()) { - log.debug("Send buffer full!"); - return false; - } else { - return true; - } - } - } - -} diff --git a/convex-peer/src/main/java/convex/net/MessageType.java b/convex-peer/src/main/java/convex/net/MessageType.java deleted file mode 100644 index 94838122c..000000000 --- a/convex-peer/src/main/java/convex/net/MessageType.java +++ /dev/null @@ -1,143 +0,0 @@ -package convex.net; - -import convex.core.exceptions.BadFormatException; - -public enum MessageType { - - /** - * A message that requests the remote endpoint to respond with a signed - * response. - * - * The challenge must be signed to authenticate the challenger. - * - * The challenge is sent with a vector of [hash accountKey-of-the-challenged] - */ - CHALLENGE(1), - - /** - * A response to a challenge. The challengee must sign the response as proof of - * possession of the claimed address. - */ - RESPONSE(2), - - /** - * A message relaying data. - * - * Data is presented "as-is", and may be: - the result of a missing data request - * - data sent ahead of another message requiring composite data - */ - DATA(3), - - /** - * A control command to a peer. - * - * Should only be accepted and acted upon when originating from trusted, - * authenticated senders. - */ - COMMAND(4), - - /** - * A request to provide missing data. Peers should not send this message unless - * both: a) they are unable to locate the given data in their local store b) - * They have reason to believe the targeted peer may be able to provide it - * - * Excessive invalid missing data requests may be considered a DoS attack by - * peers. Peers under load may need to ignore missing data requests. - * - * Payload is the missing data hash. - * - * Receiver should respond with a DATA message if the specified data is - * available in their store. - */ - MISSING_DATA(5), - - /** - * A request to perform the specified query and return results. - * - * Payload is: [id form address?] - * - * Receiver may may determine policies regarding whether to accept or reject - * queries, typically receiver will want to authenticate the sender and ensure - * good standing? - */ - QUERY(6), - - /** - * A message requesting a transaction be performed by the receiving peer and - * included in the next available block. - * - * Payload is: [id signed-data] - */ - TRANSACT(7), - - /** - * Message containing the result for a corresponding COMMAND, QUERY or TRANSACT - * message. - * - * Payload is: [id result error-flag] - * - * Where: - * - Result is the result of the request, or the message if an error occurred - * - error-flag is nil if the transaction succeeded, or error code if it failed - */ - RESULT(8), - - /** - * Communication of a latest Belief by a Peer. - * - * Payload is a SignedData - */ - BELIEF(9), - - /** - * Communication of an intention to shutdown. - */ - GOODBYE(10), - - /** - * Request for a peer status update. - * - * Expected Result is a Vector: [signed-belief-hash states-hash initial-state-hash peer-key consensus-state-hash] - */ - STATUS(11); - - private final byte messageCode; - - public byte getMessageCode() { - return messageCode; - } - - public static MessageType decode(int i) throws BadFormatException { - switch (i) { - case 1: - return CHALLENGE; - case 2: - return RESPONSE; - case 3: - return DATA; - case 4: - return COMMAND; - case 5: - return MISSING_DATA; - case 6: - return QUERY; - case 7: - return TRANSACT; - case 8: - return RESULT; - case 9: - return BELIEF; - case 10: - return GOODBYE; - case 11: - return STATUS; - } - throw new BadFormatException("Invalid message code: " + i); - } - - MessageType(int i) { - this.messageCode = (byte) i; - if (i != messageCode) throw new Error("Message format byte out of range: " + i); - } - -} diff --git a/convex-peer/src/main/java/convex/net/NIOServer.java b/convex-peer/src/main/java/convex/net/NIOServer.java deleted file mode 100644 index 43e50b0cd..000000000 --- a/convex-peer/src/main/java/convex/net/NIOServer.java +++ /dev/null @@ -1,269 +0,0 @@ -package convex.net; - -import java.io.Closeable; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.ServerSocket; -import java.net.StandardSocketOptions; -import java.nio.channels.ClosedChannelException; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; -import java.util.Iterator; -import java.util.Set; -import java.util.concurrent.BlockingQueue; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import convex.core.Constants; -import convex.core.exceptions.BadFormatException; -import convex.core.store.Stores; -import convex.peer.Server; - -/** - * NIO Server implementation that handles incoming messages on a given port. - * - * Allocates a single thread for the selector. - * - * Incoming messages are associated with a Connection (which is created if required), then placed - * on the receive message queue. This will block if the receive queue is full (thereby applying - * back-pressure to clients) - * - */ -public class NIOServer implements Closeable { - public static final int DEFAULT_PORT = 18888; - - private static final Logger log=LoggerFactory.getLogger(NIOServer.class.getName()); - - private ServerSocketChannel ssc=null; - - private BlockingQueue receiveQueue; - - private Selector selector=null; - - private boolean running=false; - - private final Server server; - - private NIOServer(Server server, BlockingQueue receiveQueue) { - this.server=server; - this.receiveQueue=receiveQueue; - } - - /** - * Creates a new unlaunched NIO server - * @param server Peer Server instance for this NIOServer - * @param receiveQueue Queue for received messages - * @return New NIOServer instance - */ - public static NIOServer create(Server server, BlockingQueue receiveQueue) { - return new NIOServer(server,receiveQueue); - } - - public void launch(Integer port) { - launch(null, port); - } - - public void launch(String host, Integer port) { - if (port==null) port=0; - - try { - ssc=ServerSocketChannel.open(); - - // Set receive buffer size - ssc.socket().setReceiveBufferSize(Constants.SOCKET_SERVER_BUFFER_SIZE); - - host = (host == null)? "0.0.0.0" : host; - InetSocketAddress address=new InetSocketAddress(host, port); - ssc.bind(address); - address=(InetSocketAddress) ssc.getLocalAddress(); - ssc.configureBlocking(false); - port=ssc.socket().getLocalPort(); - - // Register for accept. Do this before selection loop starts and - // before we return from launch! - selector = Selector.open(); - ssc.register(selector, SelectionKey.OP_ACCEPT); - - // set running status now, so that loops don't terminate - running=true; - - Thread selectorThread=new Thread(selectorLoop,"NIO Server selector loop on port: "+port); - selectorThread.setDaemon(true); - selectorThread.start(); - log.info("NIO server started on port {}",port); - } catch (Exception e) { - throw new Error("Can't bind NIOServer to port: "+port,e); - } - } - - - /** - * Runnable class for accepting socket connections and incoming data, one per peer - * If this gets maxed out, rely on backpressure to throttle clients. - */ - private Runnable selectorLoop= new Runnable() { - @Override - public void run() { - // Use the store configured for the owning server. - Stores.setCurrent(server.getStore()); - try { - - while (running) { - selector.select(1000); - - Set keys = selector.selectedKeys(); - Iterator it = keys.iterator(); - while(it.hasNext()) { - SelectionKey key=it.next(); - it.remove(); - - try { - // Just do one op on each key - if (key.isAcceptable()) { - accept(selector); - } else if (key.isReadable()) { - selectRead(key); - } else if (key.isWritable()) { - selectWrite(key); - } - } catch (ClosedChannelException e) { - // channel was closed, just lose the key? - log.debug("Client closed channel"); - key.cancel(); - } catch (IOException e) { - log.warn("Unexpected IOException, canceling key: {}",e); - // e.printStackTrace(); - key.cancel(); - } - } - // keys.clear(); - } - } catch (IOException e) { - log.error("Unexpected IOException, terminating selector loop: {}",e); - // print error and terminate - e.printStackTrace(); - } finally { - try { - // close all client channels - for (SelectionKey key: selector.keys()) { - key.channel().close(); - } - selector.close(); - selector=null; - } catch (IOException e) { - log.error("IOException while closing NIO server"); - e.printStackTrace(); - } finally { - selector=null; - } - - - if (ssc!=null) { - try { - ssc.close(); - } catch (IOException e) { - log.error("IOException while closing NIO socket channel"); - e.printStackTrace(); - } finally { - ssc=null; - } - } - - log.info("Selector loop ended on port: "+getPort()); - } - } - }; - - /** - * Gets the port that this server instance is listening on. - * @return Port number, or 0 if a server socket is not bound. - */ - public int getPort() { - if (ssc==null) return 0; - ServerSocket socket = ssc.socket(); - if (socket==null) return 0; - return socket.getLocalPort(); - } - - protected void selectWrite(SelectionKey key) throws IOException { - // attach a PeerConnection if needed for this client - ensurePeerConnection(key); - - Connection.selectWrite(key); - } - - private Connection ensurePeerConnection(SelectionKey key) throws IOException { - Connection pc=(Connection) key.attachment(); - if (pc!=null) return pc; - SocketChannel sc=(SocketChannel) key.channel(); - assert(!sc.isBlocking()); - pc=createPC(sc,receiveQueue); - key.attach(pc); - return pc; - } - - private Connection createPC(SocketChannel sc, BlockingQueue queue) throws IOException { - return Connection.create(sc,server.getReceiveAction(),server.getStore(),null); - } - - protected void selectRead(SelectionKey key) throws IOException { - - // log.info("Connection read from: "+sc.getRemoteAddress()+" with key:"+key); - Connection conn=ensurePeerConnection(key); - if (conn==null) throw new Error("No PeerConnection specified"); - try { - int n=conn.handleChannelRecieve(); - if (n==0) { - log.debug("No bytes received for key: {}",key); - } - } - catch (ClosedChannelException e) { - log.debug("Channel closed from: {}",conn.getRemoteAddress()); - key.cancel(); - } - catch (BadFormatException e) { - log.warn("Cancelled connection: Bad data format from: {} message: {}",conn.getRemoteAddress(),e.getMessage()); - // TODO: blacklist peer? - key.cancel(); - } - } - - @Override public void finalize() { - close(); - } - - @Override - public void close() { - running=false; - if (selector!=null) { - selector.wakeup(); - } - - } - - private void accept(Selector selector) throws IOException, ClosedChannelException { - SocketChannel socketChannel=ssc.accept(); - if (socketChannel==null) return; // false alarm? Nobody there? - log.debug("New connection accepted: {}", socketChannel); - socketChannel.configureBlocking(false); - - // TODO: Confirm we don't want Nagle? - socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true); - socketChannel.register(selector, SelectionKey.OP_READ); - } - - /** - * Gets the host address for this server (including port), or null if closed - * @return Host address - */ - public InetSocketAddress getHostAddress() { - if (ssc == null) return null; - ServerSocket socket = ssc.socket(); - if (socket == null) return null; - return new InetSocketAddress(socket.getInetAddress(), socket.getLocalPort()); - } - -} diff --git a/convex-peer/src/main/java/convex/net/ResultConsumer.java b/convex-peer/src/main/java/convex/net/ResultConsumer.java deleted file mode 100644 index c49d2b7aa..000000000 --- a/convex-peer/src/main/java/convex/net/ResultConsumer.java +++ /dev/null @@ -1,191 +0,0 @@ -package convex.net; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.function.Consumer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import convex.core.Result; -import convex.core.data.ACell; -import convex.core.data.Hash; -import convex.core.data.Ref; -import convex.core.exceptions.MissingDataException; -import convex.core.lang.RT; -import convex.core.store.Stores; -import convex.core.util.Utils; - -/** - * Consumer abstract base class for awaiting results. - * - * Provides basic buffering of: - * - Missing data until all data is available. - */ -public abstract class ResultConsumer implements Consumer { - - private static final Logger log = LoggerFactory.getLogger(ResultConsumer.class.getName()); - - @Override - public void accept(Message m) { - try { - MessageType type = m.getType(); - switch (type) { - case DATA: { - handleDataProvided(m); - break; - } - case MISSING_DATA: { - handleMissingDataRequest(m); - break; - } - case RESULT: { - handleResultMessage(m); - break; - } - default: { - log.error("Message type ignored: ", type); - } - } - } catch (Throwable t) { - log.warn("Failed to accept message! {}",t); - t.printStackTrace(); - } - } - - private void handleDataProvided(Message m) { - // Just store the data, can't guarantee full persistence yet - try { - ACell o = m.getPayload(); - Ref r = Ref.get(o); - r.persistShallow(); - Hash h=r.getHash(); - log.trace("Recieved DATA for hash {}",h); - unbuffer(h); - } catch (MissingDataException e) { - // ignore? - } - } - - private void handleMissingDataRequest(Message m) { - // try to be helpful by returning sent data - Hash h = RT.ensureHash(m.getPayload()); - if (h==null) return; // not a valid payload so ignore - - Ref r = Stores.current().refForHash(h); - if (r != null) try { - m.getConnection().sendData(r.getValue()); - } catch (IOException e) { - log.debug("Error replying to MISSING DATA request",e); - } - } - - /** - * Map for messages delayed due to missing data - */ - private HashMap> bufferedMessages = new HashMap<>(); - - private synchronized void buffer(Hash hash, Message m) { - ArrayList msgs = bufferedMessages.get(hash); - if (msgs == null) { - msgs = new ArrayList(); - bufferedMessages.put(hash, msgs); - } - msgs.add(m); - } - - /** - * Unbuffer and replay messages for a given hash - * - * @param hash - */ - protected synchronized void unbuffer(Hash hash) { - ArrayList msgs = bufferedMessages.get(hash); - if (msgs != null) { - bufferedMessages.remove(hash); - for (Message m : msgs) { - accept(m); - } - } - } - - /** - * Method called when a result is received. - * - * By default, delegates to handleResult and handleError - */ - private final void handleResultMessage(Message m) { - Result result = m.getPayload(); - try { - ACell.createPersisted(result); - - // we now have the full result, so notify those interested - long id=m.getID().longValue(); - handleResult(id,result); - } catch (MissingDataException e) { - // If there is missing data, re-buffer the message - // And wait for it to arrive later - Hash hash = e.getMissingHash(); - try { - if (m.getConnection().sendMissingData(hash)) { - log.debug("Missing data {} requested by client for RESULT of type: {}",hash.toHexString(),Utils.getClassName(result)); - buffer(hash, m); - } else { - log.debug("Unable to request missing data"); - } - } catch (IOException e1) { - // Ignore. We probably lost this result? - log.warn("IO Exception handling result - {}",e1); - } - return; - } - } - - /** - * Handler for a fully received Result. May be overridden. - * - * @param id ID of message received - * @param result Result value - */ - protected void handleResult(long id, Result result) { - ACell rv = result.getValue(); - ACell err = result.getErrorCode(); - if (err!=null) { - handleError(id, err, rv); - } else { - handleNormalResult(id, rv); - } - } - - /** - * Method called when an error result is received. May be overriden. - * - * Default behaviour is simply to log the error. - * - * If this method throws a MissingDataException, missing data is requested and - * the result handling may be retried later. - * - * @param id The ID of the original message to which this result corresponds - * @param code The error code received. May not be null, and is usually a Keyword - * @param errorMessage The error message associated with the result (may be null) - */ - protected void handleError(long id, ACell code, ACell errorMessage) { - log.warn("UNHANDLED ERROR RECEIVED: {} : {}", code, errorMessage); - } - - /** - * Method called when a normal (non-error) result is received. - * - * If this method throws a MissingDataException, missing data is requested and - * the result handling may be retried later. - * - * @param id The ID of the original message to which this result corresponds - * @param value The result value - */ - protected void handleNormalResult(long id, ACell value) { - log.warn("UNHANDLED RESULT RECEIVED: id={}, value={}", id,value); - } - - -} diff --git a/convex-peer/src/main/java/convex/peer/API.java b/convex-peer/src/main/java/convex/peer/API.java deleted file mode 100644 index 62c313da5..000000000 --- a/convex-peer/src/main/java/convex/peer/API.java +++ /dev/null @@ -1,205 +0,0 @@ -package convex.peer; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import convex.core.State; -import convex.core.crypto.AKeyPair; -import convex.core.data.Hash; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.store.AStore; -import convex.core.store.Stores; -import convex.core.util.Utils; - - -/** - * Class providing a simple API to a peer Server. - * - * Suitable for library usage, e.g. if a usr application wants to - * instantiate a local network peer. - * - * "If you don't believe it or don't get it , I don't have time to convince you" - * - Satoshi Nakamoto - */ -public class API { - - private static final Logger log = LoggerFactory.getLogger(API.class.getName()); - - /** - *

Launches a Peer Server with a supplied configuration.

- * - *

Config keys are:

- * - *
    - *
  • :keypair (required, AKeyPair) - AKeyPair instance. - *
  • :port (optional, Integer) - Integer port number to use for incoming connections. Defaults to random allocation. - *
  • :store (optional, AStore) - AStore instance. Defaults to the configured global store - *
  • :source (optional, String) - URL for Peer to replicate initial State/Belief from. - *
  • :state (optional, State) - Genesis state. Defaults to a fresh genesis state for the Peer if neither :source nor :state is specified - *
  • :restore (optional, Boolean) - Boolean Flag to restore from existing store. Default to true - *
  • :persist (optional, Boolean) - Boolean flag to determine if peer state should be persisted in store at server close. Default true. - *
  • :url (optional, String) - public URL for server. If provided, peer will set its public on-chain address based on this. - *
  • :auto-manage (optional Boolean) - set to true for peer to auto-manage own account. Defaults to true. - *
- * - * @param peerConfig Config map for the new Peer - * - * @param event Optional event object that implements the IServerEvent interface - * - * @return New Server instance - */ - public static Server launchPeer(Map peerConfig) { - HashMap config=new HashMap<>(peerConfig); - - // State no8t strictly necessarry? Should be possible to restore a Peer from store - if (!(config.containsKey(Keywords.STATE) - ||config.containsKey(Keywords.STORE) - ||config.containsKey(Keywords.SOURCE) - )) { - throw new IllegalArgumentException("Peer launch requires a genesis :state, remote :source or existing :store in config"); - } - - if (!config.containsKey(Keywords.KEYPAIR)) throw new IllegalArgumentException("Peer launch requires a "+Keywords.KEYPAIR+" in config"); - - try { - if (!config.containsKey(Keywords.PORT)) config.put(Keywords.PORT, null); - if (!config.containsKey(Keywords.STORE)) config.put(Keywords.STORE, Stores.getGlobalStore()); - if (!config.containsKey(Keywords.RESTORE)) config.put(Keywords.RESTORE, true); - if (!config.containsKey(Keywords.PERSIST)) config.put(Keywords.PERSIST, true); - if (!config.containsKey(Keywords.AUTO_MANAGE)) config.put(Keywords.AUTO_MANAGE, true); - - Server server = Server.create(config); - server.launch(); - return server; - } catch (Throwable t) { - log.error("Error launching peer: ",t); - t.printStackTrace(); - throw Utils.sneakyThrow(t); - } - } - - /** - * Launch a local set of peers. Intended mainly for testing / development. - * - * The Peers will have a unique genesis State, i.e. an independent network - * - * @param keyPairs List of keypairs for peers - * @param genesisState genesis state for local network - * - * @return List of Servers launched - * - */ - public static List launchLocalPeers(List keyPairs, State genesisState) { - return launchLocalPeers(keyPairs, genesisState, null, null); - } - /** - * Launch a local set of peers. Intended mainly for testing / development. - * - * The Peers will have a unique genesis State, i.e. an independent network - * - * @param keyPairs List of keypairs for peers - * @param genesisState GEnesis state for local network - * @param peerPorts Array of ports to use for each peer, if == null then randomly assign port numbers - * @param event Server event handler - * - * @return List of Servers launched - * - */ - public static List launchLocalPeers(List keyPairs, State genesisState, int peerPorts[], IServerEvent event) { - int count=keyPairs.size(); - - List serverList = new ArrayList(); - - Map config = new HashMap<>(); - - // Peer should get a new allocated port - config.put(Keywords.PORT, null); - - // Peers should all have the same genesis state - config.put(Keywords.STATE, genesisState); - - // TODO maybe have this as an option in the calling parameters? - AStore store = Stores.current(); - config.put(Keywords.STORE, store); - - // Automatically manage Peer connections - config.put(Keywords.AUTO_MANAGE, true); - - if (event!=null) { - config.put(Keywords.EVENT_HOOK, event); - } - - for (int i = 0; i < count; i++) { - AKeyPair keyPair = keyPairs.get(i); - config.put(Keywords.KEYPAIR, keyPair); - if (peerPorts != null) { - config.put(Keywords.PORT, peerPorts[i]); - } - Server server = API.launchPeer(config); - serverList.add(server); - } - - Server genesisServer = serverList.get(0); - - // go through 1..count-1 peers and join them all to the genesis Peer - // do this twice to allow for all of the peers to get all of the address in the group of peers - - for (int i = 1; i < count; i++) { - Server server=serverList.get(i); - - // Join each additional Server to the Peer #0 - ConnectionManager cm=server.getConnectionManager(); - cm.connectToPeer(genesisServer.getHostAddress()); - - // Join server #0 to this server - genesisServer.getConnectionManager().connectToPeer(server.getHostAddress()); - server.setHostname("localhost:"+server.getPort()); - } - - // wait for the peers to sync upto 10 seconds - //API.waitForNetworkReady(serverList, 10); - return serverList; - } - - /** - * Returns a true value if the local network is ready and synced with the same consensus state hash. - * - * @param serverList List of local peer servers running on the local network. - * - * @param timeoutMillis Number of milliseconds to wait before exiting with a failure. - * - * @return Return true if all server peers have the same consensus hash, else false is a timeout. - * - */ - public static boolean isNetworkReady(List serverList, long timeoutMillis) { - boolean isReady = false; - long timeoutTime = Utils.getTimeMillis() + timeoutMillis; - while (timeoutTime > Utils.getTimeMillis()) { - isReady = true; - Hash consensusHash = null; - for (Server server: serverList) { - if (consensusHash == null) { - consensusHash = server.getPeer().getConsensusState().getHash(); - } - if (!consensusHash.equals(server.getPeer().getConsensusState().getHash())) { - isReady=false; - } - try { - Thread.sleep(100); - } catch ( InterruptedException e) { - return false; - } - } - if (isReady) { - break; - } - } - return isReady; - } -} diff --git a/convex-peer/src/main/java/convex/peer/ChallengeRequest.java b/convex-peer/src/main/java/convex/peer/ChallengeRequest.java deleted file mode 100644 index 72673b351..000000000 --- a/convex-peer/src/main/java/convex/peer/ChallengeRequest.java +++ /dev/null @@ -1,89 +0,0 @@ -package convex.peer; - -import java.io.IOException; -import java.security.SecureRandom; -import java.util.concurrent.TimeUnit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -import convex.core.Peer; -import convex.core.data.ACell; -import convex.core.data.AccountKey; -import convex.core.data.AVector; -import convex.core.data.Blob; -import convex.core.data.Vectors; -import convex.core.data.Hash; -import convex.core.data.SignedData; -import convex.net.Connection; - -class ChallengeRequest { - - private static final Logger log = LoggerFactory.getLogger(ChallengeRequest.class.getName()); - - private static final int TIMEOUT_SECONDS = 10; - - - protected AccountKey peerKey; - protected long timeout; - protected Hash token; - protected Hash sendHash; - - private ChallengeRequest(AccountKey peerKey, long timeout) { - this.peerKey = peerKey; - this.timeout = timeout; - } - - public static ChallengeRequest create(AccountKey peerKey) { - return ChallengeRequest.create(peerKey, TIMEOUT_SECONDS); - } - - public static ChallengeRequest create(AccountKey peerKey, int timeoutSeconds) { - long timeout = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(timeoutSeconds); - return new ChallengeRequest(peerKey, timeout); - } - - - /** - * Sends out a single challenge to the remote peer. - * @param connection Connection - * @param peer This Peer - * @return ID of message sent, or negative value if sending fails - */ - public long send(Connection connection, Peer peer) { - AVector values = null; - try { - SecureRandom random = new SecureRandom(); - - // Get 120 random bytes - byte bytes[] = new byte[120]; - random.nextBytes(bytes); - token = Blob.create(bytes).getHash(); - - values = Vectors.of(token, peer.getNetworkID(), peerKey); - SignedData challenge = peer.sign(values); - sendHash = challenge.getHash(); - return connection.sendChallenge(challenge); - } catch (IOException e) { - log.warn("Cannot send challenge to remote peer at {}", connection.getRemoteAddress()); - values = null; - } - return -1; - } - - public AccountKey getPeerKey() { - return peerKey; - } - - public Hash getToken() { - return token; - } - - public Hash getSendHash() { - return sendHash; - } - - public boolean isTimedout() { - return timeout < System.currentTimeMillis(); - } -} diff --git a/convex-peer/src/main/java/convex/peer/ConnectionManager.java b/convex-peer/src/main/java/convex/peer/ConnectionManager.java deleted file mode 100644 index f3f04c6f8..000000000 --- a/convex-peer/src/main/java/convex/peer/ConnectionManager.java +++ /dev/null @@ -1,639 +0,0 @@ -package convex.peer; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.channels.UnresolvedAddressException; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import convex.api.Convex; -import convex.core.Belief; -import convex.core.Constants; -import convex.core.Peer; -import convex.core.State; -import convex.core.data.ACell; -import convex.core.data.AString; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.Hash; -import convex.core.data.Keywords; -import convex.core.data.PeerStatus; -import convex.core.data.SignedData; -import convex.core.data.Vectors; -import convex.core.lang.RT; -import convex.core.store.Stores; -import convex.core.util.Utils; -import convex.net.Connection; -import convex.net.Message; - -/** - * Class for managing the outbound connections from a Peer Server. - * - * Outbound connections need special handling: - Should be trusted connections - * to known peers - Should be targets for broadcast of belief updates - Should - * be limited in number - */ -public class ConnectionManager { - - private static final Logger log = LoggerFactory.getLogger(ConnectionManager.class.getName()); - - /** - * Pause for each iteration of Server connection loop. - */ - static final long SERVER_CONNECTION_PAUSE = 1000; - - /** - * Pause for each iteration of Server connection loop. - */ - static final long SERVER_POLL_DELAY = 2000; - - - protected final Server server; - private final HashMap connections = new HashMap<>(); - - /** - * Planned future connections for this Peer - */ - private final HashSet plannedConnections=new HashSet<>(); - - /** - * The list of outgoing challenges that are being made to remote peers - */ - private HashMap challengeList = new HashMap<>(); - - private Thread connectionThread = null; - - private SecureRandom random=new SecureRandom(); - - /** - * Timstamp for the last execution of the Connection Manager update loop. - */ - private long lastUpdate=Utils.getCurrentTimestamp(); - - /* - * Runnable loop for managing server connections - */ - private Runnable connectionLoop = new Runnable() { - @Override - public void run() { - Stores.setCurrent(server.getStore()); // ensure the loop uses this Server's store - try { - lastUpdate=Utils.getCurrentTimestamp(); - while (server.isLive()) { - Thread.sleep(ConnectionManager.SERVER_CONNECTION_PAUSE); - makePlannedConnections(); - maintainConnections(); - pollBelief(); - lastUpdate=Utils.getCurrentTimestamp(); - } - } catch (InterruptedException e) { - /* OK? Close the thread normally */ - } catch (Throwable e) { - log.error("Unexpected exception, Terminating Server connection loop"); - e.printStackTrace(); - } finally { - connectionThread = null; - closeAllConnections(); // shut down everything gracefully if we can - } - } - }; - - /** - * Celled by the connection manager to ensure we are tracking latest Beliefs on the network - */ - private void pollBelief() { - try { - // Poll if no recent consensus updates - long lastConsensus=server.getPeer().getConsensusState().getTimeStamp().longValue(); - if (lastConsensus+SERVER_POLL_DELAY>=lastUpdate) return; - - ArrayList conns=new ArrayList<>(connections.values()); - if (conns.size()==0) { - // Nothing to do - // log.debug("No connections available to poll!"); - return; - } - Connection c=conns.get(random.nextInt(conns.size())); - if (c.isClosed()) return; - Convex convex=Convex.connect(c.getRemoteAddress()); - try { - // use requestStatusSync to auto acquire hash of the status instead of the value - AVector status=convex.requestStatusSync(1000); - Hash h=RT.ensureHash(status.get(0)); - @SuppressWarnings("unchecked") - SignedData sb=(SignedData) convex.acquire(h).get(10000,TimeUnit.MILLISECONDS); - server.queueEvent(sb); - } finally { - convex.close(); - } - } catch (Throwable t) { - if (server.isLive()) log.warn("Polling failed: {}",t); - } - } - - private void makePlannedConnections() { - synchronized(plannedConnections) { - for (InetSocketAddress a: plannedConnections) { - Connection c=connectToPeer(a); - if (c==null) { - log.warn( "Planned Connection failed to {}",a); - } else { - log.info("Planned Connection made to {}",a); - } - } - plannedConnections.clear(); - } - } - - - protected void maintainConnections() { - State s=server.getPeer().getConsensusState(); - - long millisSinceLastUpdate=Math.max(0,Utils.getCurrentTimestamp()-lastUpdate); - - int targetPeerCount=getTargetPeerCount(); - int currentPeerCount=connections.size(); - double totalStake=s.computeStakes().get(null); - - AccountKey[] peers = connections.keySet().toArray(new AccountKey[currentPeerCount]); - for (AccountKey p: peers) { - Connection conn=connections.get(p); - - // Remove closed connections. No point keeping these - if ((conn==null)||(conn.isClosed())) { - closeConnection(p); - currentPeerCount--; - continue; - } - - /* - * Always remove Peers not staked in consensus. This should eliminate Peers that have - * withdrawn or are slashed from current consideration. - */ - PeerStatus ps=s.getPeer(p); - if ((ps==null)||(ps.getTotalStake()<=Constants.MINIMUM_EFFECTIVE_STAKE)) { - closeConnection(p); - currentPeerCount--; - continue; - } - - /* Drop Peers randomly if they have a small stake - * This ensure that new peers will get picked up occasionally and - * the distribution of peers tends towards the level of stake over time - */ - if ((millisSinceLastUpdate>0)&&(currentPeerCount>=targetPeerCount)) { - double prop=ps.getTotalStake()/totalStake; // proportion of stake represented by this Peer - // Very low chance of dropping a Peer with high stake (more than - double keepChance=Math.min(1.0, prop*targetPeerCount); - - if (keepChance<1.0) { - - double dropRate=millisSinceLastUpdate/(double)Constants.PEER_CONNECTION_DROP_TIME; - if (random.nextDouble()<(dropRate*(1.0-keepChance))) { - closeConnection(p); - currentPeerCount--; - continue; - } - } - } - - // send request for a trusted peer connection if necessary - // TODO: need to find out why the response message is not being received by the peers - requestChallenge(p, conn, server.getPeer()); - } - - // refresh peers list - currentPeerCount=connections.size(); - peers = connections.keySet().toArray(new AccountKey[currentPeerCount]); - if (peers.length potentialPeers=s.getPeers().keySet(); - InetSocketAddress target=null; - double accStake=0.0; - for (ACell c:potentialPeers) { - AccountKey peerKey=RT.ensureAccountKey(c); - if (connections.containsKey(peerKey)) continue; // skip if already connected - - if (server.getPeerKey().equals(peerKey)) continue; // don't connect to self!! - - PeerStatus ps=s.getPeers().get(peerKey); - if (ps==null) continue; // skip - AString hostName=ps.getHostname(); - if (hostName==null) continue; - InetSocketAddress maybeAddress=Utils.toInetSocketAddress(hostName.toString()); - if (maybeAddress==null) continue; - long peerStake=ps.getTotalStake(); - if (peerStake>0) { - double t=random.nextDouble()*(accStake+peerStake); - if (t>=accStake) { - target=maybeAddress; - } - accStake+=peerStake; - } - } - - if (target!=null) { - // Try to connect to Peer. If it fails, no worry, will retry another peer next time - connectToPeer(target); - } - } - } - - /** - * Gets the desired number of outgoing connections - * @return - */ - private int getTargetPeerCount() { - Integer target; - try { - target = Utils.toInt(server.getConfig().get(Keywords.OUTGOING_CONNECTIONS)); - } catch (Exception ex) { - target=null; - } - if (target==null) target=Constants.DEFAULT_OUTGOING_CONNECTION_COUNT; - return target; - } - - - public ConnectionManager(Server server) { - this.server = server; - } - - public synchronized void setConnection(AccountKey peerKey, Connection peerConnection) { - if (connections.containsKey(peerKey)) { - connections.get(peerKey).close(); - connections.replace(peerKey, peerConnection); - } - else { - connections.put(peerKey, peerConnection); - } - } - - /** - * Close and remove a connection - * - * @param peerKey Peer key linked to the connection to close and remove. - * - */ - public synchronized void closeConnection(AccountKey peerKey) { - if (connections.containsKey(peerKey)) { - Connection conn=connections.get(peerKey); - if (conn!=null) { - conn.close(); - } - connections.remove(peerKey); - server.raiseServerChange("connection"); - } - } - - /** - * Close all outgoing connections from this Peer - */ - public synchronized void closeAllConnections() { - for (Connection conn:connections.values()) { - if (conn!=null) conn.close(); - } - connections.clear(); - } - - /** - * Gets the current set of outbound peer connections from this server - * - * @return Set of connections - */ - public HashMap getConnections() { - return connections; - } - - /** - * Return true if a specified Peer is connected - * @param peerKey Public Key of Peer - * @return True if connected - * - */ - public boolean isConnected(AccountKey peerKey) { - return connections.containsKey(peerKey); - } - - - /** - * Gets a connection based on the peers public key - * @param peerKey Public key of Peer - * - * @return Connection instance, or null if not found - */ - public Connection getConnection(AccountKey peerKey) { - if (!connections.containsKey(peerKey)) return null; - return connections.get(peerKey); - } - - /** - * Returns the number of active connections - * @return Number of connections - */ - public int getConnectionCount() { - return connections.size(); - } - - /** - * Returns the number of trusted connections - * @return Number of trusted connections - * - */ - public int getTrustedConnectionCount() { - int result = 0; - for (Connection connection : connections.values()) { - if (connection.isTrusted()) { - result ++; - } - } - return result; - } - - public void processChallenge(Message m, Peer thisPeer) { - try { - SignedData> signedData = m.getPayload(); - if ( signedData == null) { - log.debug( "challenge bad message data sent"); - return; - } - AVector challengeValues = signedData.getValue(); - - if (challengeValues == null || challengeValues.size() != 3) { - log.debug("challenge data incorrect number of items should be 3 not ",RT.count(challengeValues)); - return; - } - Connection pc = m.getConnection(); - if ( pc == null) { - log.warn( "No remote peer connection from challenge"); - return; - } - // log.log(LEVEL_CHALLENGE_RESPONSE, "Processing challenge request from: " + pc.getRemoteAddress()); - - // get the token to respond with - Hash token = RT.ensureHash(challengeValues.get(0)); - if (token == null) { - log.warn( "no challenge token provided"); - return; - } - - // check to see if we are both want to connect to the same network - Hash networkId = RT.ensureHash(challengeValues.get(1)); - if (networkId == null) { - log.warn( "challenge data has no networkId"); - return; - } - if ( !networkId.equals(thisPeer.getNetworkID())) { - log.warn( "challenge data has incorrect networkId"); - return; - } - // check to see if the challenge is for this peer - AccountKey toPeer = RT.ensureAccountKey(challengeValues.get(2)); - if (toPeer == null) { - log.warn( "challenge data has no toPeer address"); - return; - } - if ( !toPeer.equals(thisPeer.getPeerKey())) { - log.warn( "challenge data has incorrect addressed peer"); - return; - } - - // get who sent this challenge - AccountKey fromPeer = signedData.getAccountKey(); - - // send the signed response back - AVector responseValues = Vectors.of(token, thisPeer.getNetworkID(), fromPeer, signedData.getHash()); - - SignedData response = thisPeer.sign(responseValues); - // log.log(LEVEL_CHALLENGE_RESPONSE, "Sending response to "+ pc.getRemoteAddress()); - if (pc.sendResponse(response) == -1 ){ - log.warn("Failed sending response from challenge to ", pc.getRemoteAddress()); - } - - } catch (Throwable t) { - log.error("Challenge Error: {}" ,t); - // t.printStackTrace(); - } - } - - AccountKey processResponse(Message m, Peer thisPeer) { - try { - SignedData signedData = m.getPayload(); - - log.debug( "Processing response request from: {}",m.getConnection().getRemoteAddress()); - - @SuppressWarnings("unchecked") - AVector responseValues = (AVector) signedData.getValue(); - - if (responseValues.size() != 4) { - log.warn( "response data incorrect number of items should be 4 not {}",responseValues.size()); - return null; - } - - - // get the signed token - Hash token = RT.ensureHash(responseValues.get(0)); - if (token == null) { - log.warn( "no response token provided"); - return null; - } - - // check to see if we are both want to connect to the same network - Hash networkId = RT.ensureHash(responseValues.get(1)); - if ( networkId == null || !networkId.equals(thisPeer.getNetworkID())) { - log.warn( "response data has incorrect networkId"); - return null; - } - // check to see if the challenge is for this peer - AccountKey toPeer = RT.ensureAccountKey(responseValues.get(2)); - if ( toPeer == null || !toPeer.equals(thisPeer.getPeerKey())) { - log.warn( "response data has incorrect addressed peer"); - return null; - } - - // hash sent by the response - Hash challengeHash = RT.ensureHash(responseValues.get(3)); - - // get who sent this challenge - AccountKey fromPeer = signedData.getAccountKey(); - - - if ( !challengeList.containsKey(fromPeer)) { - log.warn( "response from an unkown challenge"); - return null; - } - synchronized(challengeList) { - - // get the challenge data we sent out for this peer - ChallengeRequest challengeRequest = challengeList.get(fromPeer); - - Hash challengeToken = challengeRequest.getToken(); - if (!challengeToken.equals(token)) { - log.warn( "invalid response token sent"); - return null; - } - - AccountKey challengeFromPeer = challengeRequest.getPeerKey(); - if (!signedData.getAccountKey().equals(challengeFromPeer)) { - log.warn("response key does not match requested key, sent from a different peer"); - return null; - } - - // hash sent by this peer for the challenge - Hash challengeSourceHash = challengeRequest.getSendHash(); - if ( !challengeHash.equals(challengeSourceHash)) { - log.warn("response hash of the challenge does not match"); - return null; - } - // remove from list incase this fails, we can generate another challenge - challengeList.remove(fromPeer); - - Connection connection = getConnection(fromPeer); - if (connection != null) { - connection.setTrustedPeerKey(fromPeer); - server.raiseServerChange("trusted connection"); - } - - // return the trusted peer key - return fromPeer; - } - - } catch (Throwable t) { - log.error("Response Error: {}",t); - } - return null; - } - - - - /** - * Sends out a challenge to a connection that is not trusted. - * @param toPeerKey Peer key that we need to send the challenge too. - * @param connection untrusted connection - * @param thisPeer Source peer that the challenge is issued from - * - */ - public void requestChallenge(AccountKey toPeerKey, Connection connection, Peer thisPeer) { - synchronized(challengeList) { - if (connection.isTrusted()) { - return; - } - // skip if a challenge is already being sent - if (challengeList.containsKey(toPeerKey)) { - if (!challengeList.get(toPeerKey).isTimedout()) { - // not timed out, then continue to wait - return; - } - // remove the old timed out request - challengeList.remove(toPeerKey); - } - ChallengeRequest request = ChallengeRequest.create(toPeerKey); - if (request.send(connection, thisPeer)>=0) { - challengeList.put(toPeerKey, request); - } else { - // TODO: check OK to do nothing and send later? - } - } - } - - /** - * - * @param msg Message to broadcast - * - * @param requireTrusted If true, only broadcast to trusted peers - * - */ - public synchronized void broadcast(Message msg, boolean requireTrusted) { - synchronized(connections) { - for (Connection pc : connections.values()) { - try { - if ( (requireTrusted && pc.isTrusted()) || !requireTrusted) { - pc.sendMessage(msg); - } - } catch (IOException e) { - log.error("Error in broadcast: ", e); - } - } - } - } - - /** - * Connects explicitly to a Peer at the given host address - * @param hostAddress Address to connect to - * @return new Connection, or null if attempt fails - */ - public Connection connectToPeer(InetSocketAddress hostAddress) { - Connection newConn = null; - try { - // Temp client connection - Convex convex=Convex.connect(hostAddress); - - AVector status = convex.requestStatusSync(Constants.DEFAULT_CLIENT_TIMEOUT); - if (status == null || status.count()!=Constants.STATUS_COUNT) { - throw new Error("Bad status message from remote Peer"); - } - - AccountKey peerKey =RT.ensureAccountKey(status.get(3)); - if (peerKey==null) return null; - - Connection existing=connections.get(peerKey); - if ((existing!=null)&&!existing.isClosed()) return existing; - // close the current connecton to Convex API - convex.close(); - synchronized(connections) { - // reopen with connection to the peer and handle server messages - newConn = Connection.connect(hostAddress, server.peerReceiveAction, server.getStore(), null,Constants.SOCKET_PEER_BUFFER_SIZE,Constants.SOCKET_PEER_BUFFER_SIZE); - connections.put(peerKey, newConn); - } - server.raiseServerChange("connection"); - } catch (IOException | InterruptedException | ExecutionException | TimeoutException e) { - // ignore any errors from the peer connections - } catch (UnresolvedAddressException e) { - log.info("Unable to resolve host address: "+hostAddress); - } - return newConn; - } - - /** - * Schedules a request to connect to a Peer at the given host address - * @param hostAddress Address to connect to - */ - public void connectToPeerAsync(InetSocketAddress hostAddress) { - synchronized (plannedConnections) { - plannedConnections.add(hostAddress); - } - } - - public void start() { - // Set timestamp for connection updates - lastUpdate=Utils.getCurrentTimestamp(); - - // start connection thread - connectionThread = new Thread(connectionLoop, "Connection Manager thread at "+server.getPort()); - connectionThread.setDaemon(true); - connectionThread.start(); - - } - - public void close() { - if (connectionThread!=null) { - connectionThread.interrupt(); - } - } - - - - -} diff --git a/convex-peer/src/main/java/convex/peer/IServerEvent.java b/convex-peer/src/main/java/convex/peer/IServerEvent.java deleted file mode 100644 index 20be10a43..000000000 --- a/convex-peer/src/main/java/convex/peer/IServerEvent.java +++ /dev/null @@ -1,12 +0,0 @@ -package convex.peer; - -/** - * Server Event Interface. The server will post events to this the callback. - * - */ -public interface IServerEvent { - - void onServerChange(ServerEvent serverEvent); // sent on server change status, connections, consensus - - -} diff --git a/convex-peer/src/main/java/convex/peer/Server.java b/convex-peer/src/main/java/convex/peer/Server.java deleted file mode 100644 index 1a67ebce7..000000000 --- a/convex-peer/src/main/java/convex/peer/Server.java +++ /dev/null @@ -1,1207 +0,0 @@ -package convex.peer; - -import java.io.Closeable; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.Consumer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import convex.api.Convex; -import convex.core.Belief; -import convex.core.Block; -import convex.core.BlockResult; -import convex.core.Constants; -import convex.core.ErrorCodes; -import convex.core.Peer; -import convex.core.Result; -import convex.core.State; -import convex.core.crypto.AKeyPair; -import convex.core.data.ACell; -import convex.core.data.AString; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.core.data.Format; -import convex.core.data.Hash; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.PeerStatus; -import convex.core.data.Ref; -import convex.core.data.SignedData; -import convex.core.data.Strings; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.exceptions.BadSignatureException; -import convex.core.exceptions.InvalidDataException; -import convex.core.exceptions.MissingDataException; -import convex.core.init.Init; -import convex.core.lang.Context; -import convex.core.lang.RT; -import convex.core.lang.Reader; -import convex.core.store.AStore; -import convex.core.store.Stores; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Invoke; -import convex.core.util.Shutdown; -import convex.core.util.Utils; -import convex.net.Connection; -import convex.net.Message; -import convex.net.MessageType; -import convex.net.NIOServer; - - -/** - * A self contained server that can be launched with a config. - * - * Server creates the following threads: - * - A ReceiverThread that processes message from the Server's receive Queue - * - An UpdateThread that handles Belief updates and transaction processing - * - A ConnectionManager thread, via the ConnectionManager - * - * "Programming is a science dressed up as art, because most of us don't - * understand the physics of software and it's rarely, if ever, taught. The - * physics of software is not algorithms, data structures, languages, and - * abstractions. These are just tools we make, use, and throw away. The real - * physics of software is the physics of people. Specifically, it's about our - * limitations when it comes to complexity and our desire to work together to - * solve large problems in pieces. This is the science of programming: make - * building blocks that people can understand and use easily, and people will - * work together to solve the very largest problems." ― Pieter Hintjens - * - */ -public class Server implements Closeable { - public static final int DEFAULT_PORT = 18888; - - private static final int RECEIVE_QUEUE_SIZE = 10000; - - private static final int EVENT_QUEUE_SIZE = 1000; - - // Maximum Pause for each iteration of Server update loop. - private static final long SERVER_UPDATE_PAUSE = 5L; - - static final Logger log = LoggerFactory.getLogger(Server.class.getName()); - - // private static final Level LEVEL_MESSAGE = Level.FINER; - - /** - * Queue for received messages to be processed by this Peer Server - */ - private BlockingQueue receiveQueue = new ArrayBlockingQueue(RECEIVE_QUEUE_SIZE); - - /** - * Queue for received events (Beliefs, Transactions) to be processed - */ - private BlockingQueue> eventQueue = new ArrayBlockingQueue<>(EVENT_QUEUE_SIZE); - - - /** - * Message consumer that simply enqueues received messages received by this Server - */ - Consumer peerReceiveAction = new Consumer() { - @Override - public void accept(Message msg) { - try { - receiveQueue.put(msg); - } catch (InterruptedException e) { - log.warn("Interrupt on peer receive queue!"); - } - } - }; - - /** - * Connection manager instance. - */ - protected ConnectionManager manager; - - /** - * Store to use for all threads associated with this server instance - */ - private final AStore store; - - private final HashMap config; - - /** - * Flag for a running server. Setting to false will terminate server threads. - */ - private volatile boolean isRunning = false; - - private NIOServer nio; - private Thread receiverThread = null; - private Thread updateThread = null; - - /** - * The Peer instance current state for this server. Will be updated based on peer events. - */ - private Peer peer; - - /** - * The Peer Controller Address - */ - private Address controller; - - /** - * The list of new transactions to be added to the next Block. Accessed only in update loop - * - * Must all have been fully persisted. - */ - private ArrayList> newTransactions = new ArrayList<>(); - - /** - * The set of queued partial messages pending missing data. - * - * Delivery will be re-attempted when missing data is provided - */ - private HashMap partialMessages = new HashMap(); - - /** - * The list of new beliefs received from remote peers the block being created - * Should only modify with the lock for this Server held. - */ - private HashMap> newBeliefs = new HashMap<>(); - - - /** - * Hostname of the peer server. - */ - String hostname; - - private IServerEvent eventHook = null; - - private Server(HashMap config) throws TimeoutException, IOException { - AStore configStore = (AStore) config.get(Keywords.STORE); - this.store = (configStore == null) ? Stores.current() : configStore; - - // assign the event hook if set - if (config.containsKey(Keywords.EVENT_HOOK)) { - Object maybeHook=config.get(Keywords.EVENT_HOOK); - if (maybeHook instanceof IServerEvent) { - this.eventHook = (IServerEvent)maybeHook; - } - } - // Switch to use the configured store for setup, saving the caller store - final AStore savedStore=Stores.current(); - try { - Stores.setCurrent(store); - this.config = config; - // now setup the connection manager - this.manager = new ConnectionManager(this); - - this.peer = establishPeer(); - - establishController(); - - nio = NIOServer.create(this, receiveQueue); - - } finally { - Stores.setCurrent(savedStore); - } - } - - /** - * Establish the controller Account for this Peer. - */ - private void establishController() { - Address controlAddress=RT.castAddress(getConfig().get(Keywords.CONTROLLER)); - if (controlAddress==null) { - controlAddress=peer.getController(); - if (controlAddress==null) { - throw new IllegalStateException("Peer Controller account does not exist for Peer Key: "+peer.getPeerKey()); - } - } - AccountStatus as=peer.getConsensusState().getAccount(controlAddress); - if (as==null) { - throw new IllegalStateException("Peer Controller Account does not exist: "+controlAddress); - } - if (!as.getAccountKey().equals(getKeyPair().getAccountKey())) { - throw new IllegalStateException("Server keypair does not match keypair for control account: "+controlAddress); - } - this.setPeerController(controlAddress); - } - - @SuppressWarnings("unchecked") - private Peer establishPeer() throws TimeoutException, IOException { - log.info("Establishing Peer with store: {}",Stores.current()); - try { - AKeyPair keyPair = (AKeyPair) getConfig().get(Keywords.KEYPAIR); - if (keyPair==null) throw new IllegalArgumentException("No Peer Key Pair provided in config"); - - Object source=getConfig().get(Keywords.SOURCE); - if (Utils.bool(source)) { - // Peer sync case - InetSocketAddress sourceAddr=Utils.toInetSocketAddress(source); - Convex convex=Convex.connect(sourceAddr); - log.info("Attempting Peer Sync with: "+sourceAddr); - long timeout = establishTimeout(); - AVector status = convex.requestStatusSync(timeout); - if (status == null || status.count()!=Constants.STATUS_COUNT) { - throw new Error("Bad status message from remote Peer"); - } - Hash beliefHash=RT.ensureHash(status.get(0)); - Hash networkID=RT.ensureHash(status.get(2)); - State genF=(State) convex.acquire(networkID).get(timeout,TimeUnit.MILLISECONDS); - log.info("Retreived Genesis State: "+networkID); - SignedData belF=(SignedData) convex.acquire(beliefHash).get(timeout,TimeUnit.MILLISECONDS); - log.info("Retreived Peer Signed Belief: "+networkID); - - Peer peer=Peer.create(keyPair, genF, belF.getValue()); - return peer; - - } else if (Utils.bool(getConfig().get(Keywords.RESTORE))) { - // Restore from storage case - try { - - Peer peer = Peer.restorePeer(store, keyPair); - if (peer != null) { - log.info("Restored Peer with root data hash: {}",store.getRootHash()); - return peer; - } - } catch (Throwable e) { - log.error("Can't restore Peer from store: {}",e); - } - } - State genesisState = (State) config.get(Keywords.STATE); - if (genesisState!=null) { - log.info("Defaulting to standard Peer startup with genesis state: "+genesisState.getHash()); - } else { - AccountKey peerKey=keyPair.getAccountKey(); - genesisState=Init.createState(List.of(peerKey)); - log.info("Created new genesis state: "+genesisState.getHash()+ " with initial peer: "+peerKey); - } - return Peer.createGenesisPeer(keyPair,genesisState); - } catch (ExecutionException|InterruptedException e) { - throw Utils.sneakyThrow(e); - } - } - - private long establishTimeout() { - Object maybeTimeout=getConfig().get(Keywords.TIMEOUT); - if (maybeTimeout==null) return Constants.PEER_SYNC_TIMEOUT; - Utils.toInt(maybeTimeout); - return 0; - } - - /** - * Creates a new (unlaunched) Server with a given config. - * - * @param config Server configuration map. Will be defensively copied. - * - * @param event Event interface where the server will send information about the peer - * @return New Server instance - * @throws IOException If an IO Error occurred establishing the Peer - * @throws TimeoutException If Peer creation timed out - */ - public static Server create(HashMap config) throws TimeoutException, IOException { - return new Server(new HashMap<>(config)); - } - - /** - * Gets the current Belief held by this PeerServer - * - * @return Current Belief - */ - public Belief getBelief() { - return peer.getBelief(); - } - - /** - * Gets the current Peer data structure for this Server. - * - * @return Current Peer - */ - public Peer getPeer() { - return peer; - } - - /** - * Gets the desired host name for this Peer - * @return Hostname String - */ - public String getHostname() { - return hostname; - } - - /** - * Launch the Peer Server, including all main server threads - */ - public void launch() { - AStore savedStore=Stores.current(); - try { - Stores.setCurrent(store); - - HashMap config = getConfig(); - - Object p = config.get(Keywords.PORT); - Integer port = (p == null) ? null : Utils.toInt(p); - - nio.launch((String)config.get(Keywords.HOST), port); - port = nio.getPort(); // Get the actual port (may be auto-allocated) - - if (getConfig().containsKey(Keywords.URL)) { - hostname = (String) config.get(Keywords.URL); - log.debug("Setting desired peer URL to: " + hostname); - } else { - hostname = null; - } - - - - // set running status now, so that loops don't terminate - isRunning = true; - - // Start connection manager loop - manager.start(); - - receiverThread = new Thread(receiverLoop, "Receive Loop on port: " + port); - receiverThread.setDaemon(true); - receiverThread.start(); - - // Start Peer update thread - updateThread = new Thread(updateLoop, "Update Loop on port: " + port); - updateThread.setDaemon(true); - updateThread.start(); - - - // Close server on shutdown, should be before Etch stores in priority - Shutdown.addHook(Shutdown.SERVER, new Runnable() { - @Override - public void run() { - close(); - } - }); - - // Connect to source peer if specified - if (getConfig().containsKey(Keywords.SOURCE)) { - Object s=getConfig().get(Keywords.SOURCE); - InetSocketAddress sa=Utils.toInetSocketAddress(s); - if (sa!=null) { - if (manager.connectToPeer(sa)!=null) { - log.debug("Automatically connected to :source peer at: {}",sa); - } else { - log.warn("Failed to connect to :source peer at: {}",sa); - } - } else { - log.warn("Failed to parse :source peer address {}",s); - } - } - - log.info( "Peer Server started with Peer Address: {}",getPeerKey()); - } catch (Throwable e) { - close(); - throw new Error("Failed to launch Server", e); - } finally { - Stores.setCurrent(savedStore); - } - } - - /** - * Process a message received from a peer or client. We know at this point that the - * message parsed successfully, not much else..... - * - * If the message is partial, will be queued pending delivery of missing data. - * - * Runs on receiver thread - * - * @param m - */ - private void processMessage(Message m) { - MessageType type = m.getType(); - log.trace("Processing message {}",type); - try { - switch (type) { - case BELIEF: - processBelief(m); - break; - case CHALLENGE: - processChallenge(m); - break; - case RESPONSE: - processResponse(m); - break; - case COMMAND: - break; - case DATA: - processData(m); - break; - case MISSING_DATA: - processMissingData(m); - break; - case QUERY: - processQuery(m); - break; - case RESULT: - break; - case TRANSACT: - processTransact(m); - break; - case GOODBYE: - processClose(m); - break; - case STATUS: - processStatus(m); - break; - } - - } catch (MissingDataException e) { - Hash missingHash = e.getMissingHash(); - log.trace("Missing data: {} in message of type {}" , missingHash,type); - try { - registerPartialMessage(missingHash, m); - m.getConnection().sendMissingData(missingHash); - log.trace("Requested missing data {} for partial message",missingHash); - } catch (IOException ex) { - log.warn( "Exception while requesting missing data: {}" + ex); - } - } catch (BadFormatException | ClassCastException | NullPointerException e) { - log.warn("Error processing client message: {}", e); - } - } - - /** - * Respond to a request for missing data, on a best-efforts basis. Requests for - * missing data we do not hold are ignored. - * - * @param m - * @throws BadFormatException - */ - private void processMissingData(Message m) throws BadFormatException { - // payload for a missing data request should be a valid Hash - Hash h = RT.ensureHash(m.getPayload()); - if (h == null) throw new BadFormatException("Hash required for missing data message"); - - Ref r = store.refForHash(h); - if (r != null) { - try { - ACell data = r.getValue(); - boolean sent = m.getConnection().sendData(data); - // log.trace( "Sent missing data for hash: {} with type {}",Utils.getClassName(data)); - if (!sent) { - log.debug("Can't send missing data for hash {} due to full buffer",h); - } - } catch (IOException e) { - log.warn("Unable to deliver missing data for {} due to exception: {}", h, e); - } - } else { - log.debug("Unable to provide missing data for {} from store: {}", h,Stores.current()); - } - } - - @SuppressWarnings("unchecked") - private void processTransact(Message m) { - // query is a vector [id , signed-object] - AVector v = m.getPayload(); - SignedData sd = (SignedData) v.get(1); - - // System.out.println("transact: "+v); - - // Persist the signed transaction. Might throw MissingDataException? - // If we already have the transaction persisted, will get signature status - ACell.createPersisted(sd); - - if (!sd.checkSignature()) { - // terminate the connection, dishonest client? - try { - // TODO: throttle? - m.getConnection().sendResult(m.getID(), Strings.create("Bad Signature!"), ErrorCodes.SIGNATURE); - } catch (IOException e) { - // Ignore?? Connection probably gone anyway - } - log.info("Bad signature from Client! {}" , sd); - return; - } - - registerInterest(sd.getHash(), m); - try { - eventQueue.put(sd); - } catch (InterruptedException e) { - log.warn("Unexpected interruption adding transaction to event queue!"); - } - } - - /** - * Called by a remote peer to close connections to the remote peer. - * - */ - private void processClose(Message m) { - SignedData signedPeerKey = m.getPayload(); - AccountKey remotePeerKey = RT.ensureAccountKey(signedPeerKey.getValue()); - manager.closeConnection(remotePeerKey); - raiseServerChange("connection"); - } - - /** - * Checks if received data fulfils the requirement for a partial message If so, - * process the message again. - * - * @param hash - * @return true if the data request resulted in a re-queued message, false - * otherwise - */ - private boolean maybeProcessPartial(Hash hash) { - Message m; - synchronized (partialMessages) { - m = partialMessages.get(hash); - - if (m != null) { - log.trace( "Attempting to re-queue partial message due to received hash: ",hash); - if (receiveQueue.offer(m)) { - partialMessages.remove(hash); - return true; - } else { - log.warn( "Queue full for message with received hash: {}", hash); - } - } - } - return false; - } - - /** - * Stores a partial message for potential later handling. - * - * @param missingHash Hash of missing data dependency - * @param m Message to re-attempt later when missing data is received. - */ - private void registerPartialMessage(Hash missingHash, Message m) { - synchronized (partialMessages) { - log.trace( "Registering partial message with missing hash: " ,missingHash); - partialMessages.put(missingHash, m); - } - } - - /** - * Register of client interests in receiving transaction responses - */ - private HashMap interests = new HashMap<>(); - - private void registerInterest(Hash signedTransactionHash, Message m) { - interests.put(signedTransactionHash, m); - } - - /** - * Handle general Belief update, taking belief registered in newBeliefs - * - * @return true if Peer Belief changed, false otherwise - * @throws InterruptedException - */ - protected boolean maybeUpdateBelief() throws InterruptedException { - long oldConsensusPoint = peer.getConsensusPoint(); - - // possibly have own transactions to publish - maybePostOwnTransactions(); - - // publish new blocks if needed. Guaranteed to change belief if this happens - boolean published = maybePublishBlock(); - - // only do belief merge if needed: either after publishing a new block or with - // incoming beliefs - if ((!published) && newBeliefs.isEmpty()) return false; - - // Update Peer timestamp first. This determines what we might accept. - peer = peer.updateTimestamp(Utils.getCurrentTimestamp()); - - boolean updated = maybeMergeBeliefs(); - // Must skip broadcast if we haven't published a new Block or updated our own Order - if (!(updated||published)) return false; - - // At this point we know our Order should have changed - final Belief belief = peer.getBelief(); - - broadcastBelief(belief); - - // Report transaction results - long newConsensusPoint = peer.getConsensusPoint(); - if (newConsensusPoint > oldConsensusPoint) { - log.debug("Consensus point update from {} to {}" ,oldConsensusPoint , newConsensusPoint); - for (long i = oldConsensusPoint; i < newConsensusPoint; i++) { - Block block = peer.getPeerOrder().getBlock(i); - BlockResult br = peer.getBlockResult(i); - reportTransactions(block, br); - } - } - - return true; - } - - /** - * Time of last belief broadcast - */ - private long lastBroadcastBelief=0; - private long broadcastCount=0L; - - private void broadcastBelief(Belief belief) { - // At this point we know something updated our belief, so we want to rebroadcast - // belief to network - Consumer> noveltyHandler = r -> { - ACell o = r.getValue(); - if (o == belief) return; // skip sending data for belief cell itself, will be BELIEF payload - Message msg = Message.createData(o); - // broadcast to all peers trusted or not - manager.broadcast(msg, false); - }; - - // persist the state of the Peer, announcing the new Belief - // (ensure we can handle missing data requests etc.) - peer=peer.persistState(noveltyHandler); - - // Broadcast latest Belief to connected Peers - SignedData sb = peer.getSignedBelief(); - - Message msg = Message.createBelief(sb); - - // at the moment broadcast to all peers trusted or not TODO: recheck this - manager.broadcast(msg, false); - lastBroadcastBelief=Utils.getCurrentTimestamp(); - broadcastCount++; - } - - /** - * Gets the number of belief broadcasts made by this Peer - * @return Count of broadcasts from this Server instance - */ - public long getBroadcastCount() { - return broadcastCount; - } - - private long lastBlockPublishedTime=0L; - - /** - * Checks for pending transactions, and if found propose them as a new Block. - * - * @return True if a new block is published, false otherwise. - */ - protected boolean maybePublishBlock() { - long timestamp=Utils.getCurrentTimestamp(); - // skip if recently published a block - if ((lastBlockPublishedTime+Constants.MIN_BLOCK_TIME)>timestamp) return false; - - Block block=null; - int n = newTransactions.size(); - if (n == 0) return false; - // TODO: smaller block if too many transactions? - block = Block.create(timestamp, (List>) newTransactions, peer.getPeerKey()); - newTransactions.clear(); - - ACell.createPersisted(block); - - Peer newPeer = peer.proposeBlock(block); - log.info("New block proposed: {} transaction(s), hash={}", block.getTransactions().count(), block.getHash()); - - peer = newPeer; - lastBlockPublishedTime=timestamp; - return true; - } - - private long lastOwnTransactionTimestamp=0L; - - private static final long OWN_TRANSACTIONS_DELAY=300; - - /** - * Gets the Peer controller Address - * @return Peer controller Address - */ - public Address getPeerController() { - return controller; - } - - /** - * Sets the Peer controller Address - * @param a Peer Controller Address to set - */ - public void setPeerController(Address a) { - controller=a; - } - - /** - * Adds an event to the inbound server event queue. May block. - * @param event Signed event to add to inbound event queue - * @throws InterruptedException - */ - public void queueEvent(SignedData event) throws InterruptedException { - eventQueue.put(event); - } - - /** - * Check if the Peer want to send any of its own transactions - * @param transactionList List of transactions to add to. - */ - private void maybePostOwnTransactions() { - if (!Utils.bool(config.get(Keywords.AUTO_MANAGE))) return; - - State s=getPeer().getConsensusState(); - long ts=Utils.getCurrentTimestamp(); - - // If we already did this recently, don't try again - if (ts<(lastOwnTransactionTimestamp+OWN_TRANSACTIONS_DELAY)) return; - - lastOwnTransactionTimestamp=ts; // mark this timestamp - - String desiredHostname=getHostname(); // Intended hostname - AccountKey peerKey=getPeerKey(); - PeerStatus ps=s.getPeer(peerKey); - AString chn=ps.getHostname(); - String currentHostname=(chn==null)?null:chn.toString(); - - // Try to set hostname if not correctly set - trySetHostname: - if (!Utils.equals(desiredHostname, currentHostname)) { - log.info("Trying to update own hostname from: {} to {}",currentHostname,desiredHostname); - Address address=ps.getController(); - if (address==null) break trySetHostname; - AccountStatus as=s.getAccount(address); - if (as==null) break trySetHostname; - if (!Utils.equals(getPeerKey(), as.getAccountKey())) break trySetHostname; - - String code; - if (desiredHostname==null) { - code = String.format("(set-peer-data %s {:url nil})", peerKey); - } else { - code = String.format("(set-peer-data %s {:url \"%s\"})", peerKey, desiredHostname); - } - ACell message = Reader.read(code); - ATransaction transaction = Invoke.create(address, as.getSequence()+1, message); - newTransactions.add(getKeyPair().signData(transaction)); - } - } - - - /** - * Checks for mergeable remote beliefs, and if found merge and update own - * belief. - * - * @return True if Peer Belief Order was changed, false otherwise. - */ - protected boolean maybeMergeBeliefs() { - try { - // First get the set of new beliefs for merging - Belief[] beliefs; - synchronized (newBeliefs) { - int n = newBeliefs.size(); - beliefs = new Belief[n]; - int i = 0; - for (AccountKey addr : newBeliefs.keySet()) { - beliefs[i++] = newBeliefs.get(addr).getValue(); - } - newBeliefs.clear(); - } - Peer newPeer = peer.mergeBeliefs(beliefs); - - // Check for substantive change (i.e. Orders updated, can ignore timestamp) - if (newPeer.getBelief().getOrders().equals(peer.getBelief().getOrders())) return false; - - log.debug( "New merged Belief update: {}" ,newPeer.getBelief().getHash()); - // we merged successfully, so clear pending beliefs and update Peer - peer = newPeer; - return true; - } catch (MissingDataException e) { - // Shouldn't happen if beliefs are persisted - // e.printStackTrace(); - throw new Error("Missing data in belief update: " + e.getMissingHash().toHexString(), e); - } catch (BadSignatureException e) { - // Shouldn't happen if Beliefs are already validated - // e.printStackTrace(); - throw new Error("Bad Signature in belief update!", e); - } catch (InvalidDataException e) { - // Shouldn't happen if Beliefs are already validated - // e.printStackTrace(); - throw new Error("Invalid data in belief update!", e); - } - } - - private void processStatus(Message m) { - try { - // We can ignore payload - - Connection pc = m.getConnection(); - log.debug( "Processing status request from: {}" ,pc.getRemoteAddress()); - // log.log(LEVEL_MESSAGE, "Processing query: " + form + " with address: " + - // address); - - Peer peer=this.getPeer(); - Hash beliefHash=peer.getSignedBelief().getHash(); - Hash stateHash=peer.getStates().getHash(); - Hash initialStateHash=peer.getStates().get(0).getHash(); - AccountKey peerKey=getPeerKey(); - Hash consensusHash=peer.getConsensusState().getHash(); - - AVector reply=Vectors.of(beliefHash,stateHash,initialStateHash,peerKey,consensusHash); - - pc.sendResult(m.getID(), reply); - } catch (Throwable t) { - log.warn("Status Request Error: {}", t); - } - } - - private void processChallenge(Message m) { - manager.processChallenge(m, peer); - } - - private void processResponse(Message m) { - manager.processResponse(m, peer); - } - - private void processQuery(Message m) { - try { - // query is a vector [id , form, address?] - AVector v = m.getPayload(); - CVMLong id = (CVMLong) v.get(0); - ACell form = v.get(1); - - // extract the Address, or use HERO if not available. - Address address = (Address) v.get(2); - - Connection pc = m.getConnection(); - log.debug( "Processing query: {} with address: {}" , form, address); - // log.log(LEVEL_MESSAGE, "Processing query: " + form + " with address: " + - // address); - Context resultContext = peer.executeQuery(form, address); - boolean resultReturned; - - if (resultContext.isExceptional()) { - resultReturned = pc.sendResult(Result.fromContext(id, resultContext)); - } else { - resultReturned = pc.sendResult(id, resultContext.getResult()); - } - - if (!resultReturned) { - log.warn("Failed to send query result back to client with ID: {}", id); - } - - } catch (Throwable t) { - log.warn("Query Error: {}", t); - } - } - - private void processData(Message m) { - ACell payload = m.getPayload(); - - // TODO: be smarter about this? hold a per-client queue for a while? - Ref r = Ref.get(payload); - r = r.persistShallow(); - Hash payloadHash = r.getHash(); - - if (log.isTraceEnabled()) { - log.trace( "Processing DATA of type: " + Utils.getClassName(payload) + " with hash: " - + payloadHash.toHexString() + " and encoding: " + Format.encodedBlob(payload).toHexString()); - } - // if our data satisfies a missing data object, need to process it - maybeProcessPartial(r.getHash()); - } - - /** - * Process an incoming message that represents a Belief - * - * @param m - */ - private void processBelief(Message m) { - Connection pc = m.getConnection(); - if (pc.isClosed()) return; // skip messages from closed peer - - ACell o = m.getPayload(); - - Ref ref = Ref.get(o); - try { - // check we can persist the new belief - // May also pick up cached signature verification if already held - ref = ref.persist(); - - @SuppressWarnings("unchecked") - SignedData receivedBelief = (SignedData) o; - receivedBelief.validateSignature(); - - // TODO: validate trusted connection? - // TODO: can drop Beliefs if under pressure? - - eventQueue.put(receivedBelief); - } catch (ClassCastException e) { - // bad message? - log.warn("Exception due to bad message from peer? {}" ,e); - } catch (BadSignatureException e) { - // we got sent a bad signature. - // TODO: Probably need to slash peer? but ignore for now - log.warn("Bad signed belief from peer: " + Utils.print(o)); - } catch (InterruptedException e) { - throw Utils.sneakyThrow(e); - } - } - - /* - * Loop to process messages from the receive queue - */ - private Runnable receiverLoop = new Runnable() { - @Override - public void run() { - Stores.setCurrent(getStore()); // ensure the loop uses this Server's store - - try { - log.debug("Reciever thread started for peer at {}", getHostAddress()); - - while (isRunning) { // loop until server terminated - Message m = receiveQueue.poll(100, TimeUnit.MILLISECONDS); - if (m != null) { - processMessage(m); - } - } - - log.debug("Reciever thread terminated normally for peer {}", this); - } catch (InterruptedException e) { - log.debug("Receiver thread interrupted "); - } catch (Throwable e) { - log.warn("Receiver thread terminated abnormally! "); - log.error("Server FAILED: " + e.getMessage()); - e.printStackTrace(); - } - } - }; - - /* - * Runnable loop for managing Server state updates - */ - private final Runnable updateLoop = new Runnable() { - @Override - public void run() { - Stores.setCurrent(getStore()); // ensure the loop uses this Server's store - try { - // loop while the server is running - while (isRunning) { - long timestamp=Utils.getCurrentTimestamp(); - - // Try belief update - if (maybeUpdateBelief() ) { - raiseServerChange("consensus"); - } - - // Maybe rebroadcast Belief if not done recently - if ((lastBroadcastBelief+Constants.REBROADCAST_DELAY) firstEvent=eventQueue.poll(SERVER_UPDATE_PAUSE, TimeUnit.MILLISECONDS); - if (firstEvent==null) return; - ArrayList> allEvents=new ArrayList<>(); - allEvents.add(firstEvent); - eventQueue.drainTo(allEvents); - for (SignedData signedEvent: allEvents) { - ACell event=signedEvent.getValue(); - if (event instanceof ATransaction) { - SignedData receivedTrans=(SignedData)signedEvent; - newTransactions.add(receivedTrans); - } else if (event instanceof Belief) { - SignedData receivedBelief=(SignedData)signedEvent; - AccountKey addr = receivedBelief.getAccountKey(); - SignedData current = newBeliefs.get(addr); - // Make sure the Belief is the latest from a Peer - if ((current == null) || (current.getValue().getTimestamp() <= receivedBelief.getValue() - .getTimestamp())) { - // Add to map of new Beliefs received for each Peer - newBeliefs.put(addr, receivedBelief); - - // Notify the update thread that there is something new to handle - log.debug("Valid belief received by peer at {}: {}" - ,getHostAddress(),receivedBelief.getValue().getHash()); - } - } else { - throw new Error("Unexpected type in event queue!"+Utils.getClassName(event)); - } - } - } - - private void reportTransactions(Block block, BlockResult br) { - // TODO: consider culling old interests after some time period - int nTrans = block.length(); - for (long j = 0; j < nTrans; j++) { - try { - SignedData t = block.getTransactions().get(j); - Hash h = t.getHash(); - Message m = interests.get(h); - if (m != null) { - log.trace("Returning transaction result to ", m.getConnection().getRemoteAddress()); - - Connection pc = m.getConnection(); - if ((pc == null) || pc.isClosed()) continue; - ACell id = m.getID(); - Result res = br.getResults().get(j).withID(id); - - pc.sendResult(res); - interests.remove(h); - } - } catch (Throwable e) { - log.warn("Exception while sending Result: ",e); - // ignore - } - } - } - - /** - * Gets the port that this Server is currently accepting connections on - * @return Port number - */ - public int getPort() { - return nio.getPort(); - } - - @Override - public void finalize() { - close(); - } - - /** - * Writes the Peer data to the configured store. - * - * This will overwrite any previously persisted peer data. - */ - public void persistPeerData() { - AStore tempStore = Stores.current(); - try { - Stores.setCurrent(store); - ACell peerData = peer.toData(); - Ref peerRef = ACell.createPersisted(peerData); - Hash peerHash = peerRef.getHash(); - store.setRootHash(peerHash); - log.info( "Stored peer data for Server with hash: {}", peerHash.toHexString()); - } catch (Throwable e) { - log.warn("Failed to persist peer state when closing server: {}" ,e); - } finally { - Stores.setCurrent(tempStore); - } - } - - @Override - public void close() { - // persist peer state if necessary - if ((peer != null) && Utils.bool(getConfig().get(Keywords.PERSIST))) { - persistPeerData(); - } - - // TODO: not much point signing this? - SignedData signedPeerKey = peer.sign(peer.getPeerKey()); - Message msg = Message.createGoodBye(signedPeerKey); - - // broadcast GOODBYE message to all outgoing remote peers - manager.broadcast(msg, false); - - isRunning = false; - if (updateThread != null) { - updateThread.interrupt(); - try { - updateThread.join(100); - } catch (InterruptedException e) { - // Ignore - } - } - if (receiverThread != null) { - receiverThread.interrupt(); - try { - receiverThread.join(100); - } catch (InterruptedException e) { - // Ignore - } - } - manager.close(); - nio.close(); - // Note we don't do store.close(); because we don't own the store. - } - - /** - * Gets the host address for this Server (including port), or null if closed - * - * @return Host Address - */ - public InetSocketAddress getHostAddress() { - return nio.getHostAddress(); - } - - /** - * Returns the Keypair for this peer server - * - * SECURITY: Be careful with this! - * @return Key pair for Peer - */ - public AKeyPair getKeyPair() { - return getPeer().getKeyPair(); - } - - /** - * Gets the public key of the peer account - * - * @return AccountKey of this Peer - */ - public AccountKey getPeerKey() { - AKeyPair kp = getKeyPair(); - if (kp == null) return null; - return kp.getAccountKey(); - } - - /** - * Gets the Store configured for this Server. A server must consistently use the - * same store instance for all Server threads. - * - * @return Store instance - */ - public AStore getStore() { - return store; - } - - /** - * Reports a server change event to the registered hook, if any - * @param reason Message for server change - */ - public void raiseServerChange(String reason) { - if (eventHook != null) { - ServerEvent serverEvent = ServerEvent.create(this, reason); - eventHook.onServerChange(serverEvent); - } - } - - public ConnectionManager getConnectionManager() { - return manager; - } - - public HashMap getConfig() { - return config; - } - - public Consumer getReceiveAction() { - return peerReceiveAction; - } - - /** - * Sets the desired host name for this Server - * @param string Desired host name String, e.g. "my-domain.com:12345" - */ - public void setHostname(String string) { - hostname=string; - } - - public boolean isLive() { - return isRunning; - } -} diff --git a/convex-peer/src/main/java/convex/peer/ServerEvent.java b/convex-peer/src/main/java/convex/peer/ServerEvent.java deleted file mode 100644 index c186816c9..000000000 --- a/convex-peer/src/main/java/convex/peer/ServerEvent.java +++ /dev/null @@ -1,30 +0,0 @@ -package convex.peer; - -/** - * Lightweight wrapper for server events - */ -public class ServerEvent { - - protected ServerInformation information=null; - protected Server server; - protected String reason; - - private ServerEvent(Server server, String reason) { - this.server = server; - this.reason = reason; - } - - public static ServerEvent create(Server server, String reason) { - return new ServerEvent(server, reason); - } - - public ServerInformation getInformation() { - if (information==null) { - information=ServerInformation.create(server); - } - return information; - } - public String getReason() { - return reason; - } -} diff --git a/convex-peer/src/main/java/convex/peer/ServerInformation.java b/convex-peer/src/main/java/convex/peer/ServerInformation.java deleted file mode 100644 index e03df6a95..000000000 --- a/convex-peer/src/main/java/convex/peer/ServerInformation.java +++ /dev/null @@ -1,88 +0,0 @@ -package convex.peer; - - -import convex.core.Order; -import convex.core.Peer; -import convex.core.data.AccountKey; -import convex.core.data.Hash; - -/** - * Utility class to extract and store server information samples - */ -public class ServerInformation { - - private AccountKey peerKey; - private String hostname; - private int connectionCount; - private int trustedConnectionCount; - private boolean isSynced; - private boolean isJoined; - private Hash networkID; - private long consensusPoint; - private Hash stateHash; - private Hash beliefHash; - private long blockCount; - - - private ServerInformation(Server server, ConnectionManager manager) { - load(server, manager); - } - - public static ServerInformation create(Server server) { - return new ServerInformation(server, server.getConnectionManager()); - } - - protected void load(Server server, ConnectionManager manager) { - Peer peer = server.getPeer(); - Order order = peer.getPeerOrder(); - - peerKey = peer.getPeerKey(); - hostname = server.getHostname(); - connectionCount = manager.getConnectionCount(); - trustedConnectionCount = manager.getTrustedConnectionCount(); - isSynced = order != null && peer.getConsensusPoint() > 0; - networkID = peer.getNetworkID(); - consensusPoint = peer.getConsensusPoint(); - isJoined = connectionCount > 0; - stateHash = peer.getConsensusState().getHash(); - beliefHash = peer.getBelief().getHash(); - blockCount = 0; - if (order != null ) { - blockCount = order.getBlockCount(); - } - } - - public AccountKey getPeerKey() { - return peerKey; - } - public String getHostname() { - return hostname; - } - public int getConnectionCount() { - return connectionCount; - } - public int getTrustedConnectionCount() { - return trustedConnectionCount; - } - public boolean isSynced() { - return isSynced; - } - public boolean isJoined() { - return isJoined; - } - public Hash getNetworkID() { - return networkID; - } - public long getConsensusPoint() { - return consensusPoint; - } - public Hash getStateHash() { - return stateHash; - } - public Hash getBeliefHash() { - return beliefHash; - } - public long getBlockCount() { - return blockCount; - } -} diff --git a/convex-peer/src/test/java/convex/api/ConvexTest.java b/convex-peer/src/test/java/convex/api/ConvexTest.java deleted file mode 100644 index b86118b7b..000000000 --- a/convex-peer/src/test/java/convex/api/ConvexTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package convex.api; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import convex.core.ErrorCodes; -import convex.core.Result; -import convex.core.crypto.AKeyPair; -import convex.core.crypto.Ed25519Signature; -import convex.core.data.Address; -import convex.core.data.Ref; -import convex.core.data.SignedData; -import convex.core.lang.Reader; -import convex.core.lang.ops.Constant; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Invoke; -import convex.core.util.Utils; -import convex.peer.TestNetwork; - -/** - * Tests for a Convex Client connection - */ -public class ConvexTest { - - static Address ADDRESS; - static final AKeyPair KEYPAIR = AKeyPair.generate(); - - private static TestNetwork network; - - @BeforeAll - public static void init() { - network = TestNetwork.getInstance(); - synchronized(network.SERVER) { - try { - ADDRESS=network.CONVEX.createAccountSync(KEYPAIR.getAccountKey()); - network.CONVEX.transfer(ADDRESS, 1000000000L).get(1000,TimeUnit.MILLISECONDS); - } catch (Throwable e) { - e.printStackTrace(); - throw Utils.sneakyThrow(e); - } - } - } - - @Test - public void testConnection() throws IOException, TimeoutException { - synchronized (network.SERVER) { - Convex convex = Convex.connect(network.SERVER); - assertTrue(convex.isConnected()); - convex.close(); - assertFalse(convex.isConnected()); - } - } - - @Test - public void testConvex() throws IOException, TimeoutException { - synchronized (network.SERVER) { - Convex convex = Convex.connect(network.SERVER.getHostAddress(), ADDRESS, KEYPAIR); - Result r = convex.transactSync(Invoke.create(ADDRESS, 0, Reader.read("*address*")), 1000); - assertNull(r.getErrorCode(), "Error:" + r.toString()); - assertEquals(ADDRESS, r.getValue()); - } - } - - @Test - public void testBadSignature() throws IOException, TimeoutException, InterruptedException, ExecutionException { - synchronized (network.SERVER) { - Convex convex = Convex.connect(network.SERVER.getHostAddress(), ADDRESS, KEYPAIR); - Ref tr = Invoke.create(ADDRESS, 0, Reader.read("*address*")).getRef(); - Result r = convex.transact(SignedData.create(KEYPAIR, Ed25519Signature.ZERO, tr)).get(); - assertEquals(ErrorCodes.SIGNATURE, r.getErrorCode()); - } - } - - @SuppressWarnings("unchecked") - @Test - public void testManyTransactions() throws IOException, TimeoutException, InterruptedException, ExecutionException { - synchronized (network.SERVER) { - Convex convex = Convex.connect(network.SERVER.getHostAddress(), ADDRESS, KEYPAIR); - int n = 100; - Future[] rs = new Future[n]; - for (int i = 0; i < n; i++) { - Future f = convex.transact(Invoke.create(ADDRESS, 0, Constant.of(i))); - rs[i] = f; - } - for (int i = 0; i < n; i++) { - Result r = rs[i].get(6000, TimeUnit.MILLISECONDS); - assertNull(r.getErrorCode(), "Error:" + r.toString()); - } - } - } - -} diff --git a/convex-peer/src/test/java/convex/api/TestApplications.java b/convex-peer/src/test/java/convex/api/TestApplications.java deleted file mode 100644 index d57366b29..000000000 --- a/convex-peer/src/test/java/convex/api/TestApplications.java +++ /dev/null @@ -1,30 +0,0 @@ -package convex.api; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -public class TestApplications { - - @Test - public void testProcess() throws Exception { - Process p=Applications.launchApp(TestApplications.class); - p.waitFor(); - assertEquals(0,p.exitValue()); - } - - @Test - public void testProcessWithArgs() throws Exception { - Process p=Applications.launchApp(TestApplications.class,"foo","bar"); - p.waitFor(); - assertEquals(2,p.exitValue()); - } - - /** - * Test main class for launch - * @param args - */ - public static void main(String[] args) { - System.exit(args.length); - } -} diff --git a/convex-peer/src/test/java/convex/examples/AcquireState.java b/convex-peer/src/test/java/convex/examples/AcquireState.java deleted file mode 100644 index 3325eb77f..000000000 --- a/convex-peer/src/test/java/convex/examples/AcquireState.java +++ /dev/null @@ -1,27 +0,0 @@ -package convex.examples; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import convex.api.Convex; -import convex.core.State; - -public class AcquireState { - - public static void main(String[] args) throws IOException, InterruptedException, ExecutionException, TimeoutException { - // Use a fresh store - //EtchStore etch=EtchStore.createTemp("acquire-testing"); - //Stores.setCurrent(etch); - - InetSocketAddress hostAddress = new InetSocketAddress("convex.world", 43579); - - Convex convex = Convex.connect(hostAddress, null,null); - - State state=convex.acquireState().get(5000, TimeUnit.MILLISECONDS); - - System.out.println(state); - } -} diff --git a/convex-peer/src/test/java/convex/examples/ClientApp.java b/convex-peer/src/test/java/convex/examples/ClientApp.java deleted file mode 100644 index 7e859549b..000000000 --- a/convex-peer/src/test/java/convex/examples/ClientApp.java +++ /dev/null @@ -1,27 +0,0 @@ -package convex.examples; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; - -import convex.api.Convex; -import convex.core.lang.RT; -import convex.peer.Server; - -public class ClientApp { - - public static void main(String... args) throws IOException, InterruptedException, TimeoutException, ExecutionException { - InetSocketAddress hostAddress = new InetSocketAddress("localhost", Server.DEFAULT_PORT + 1); - - Convex convex = Convex.connect(hostAddress, null,null); - - // send a couple of queries, wait for results - convex.querySync(RT.cvm("A beautiful life - something special - a magic moment")); - convex.querySync(RT.cvm(1L)); - convex.close(); - - System.exit(0); - } - -} diff --git a/convex-peer/src/test/java/convex/examples/Ed25519Sign.java b/convex-peer/src/test/java/convex/examples/Ed25519Sign.java deleted file mode 100644 index 3cf074814..000000000 --- a/convex-peer/src/test/java/convex/examples/Ed25519Sign.java +++ /dev/null @@ -1,54 +0,0 @@ -package convex.examples; - -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.PublicKey; -import java.security.Signature; -import java.security.SignatureException; - -import convex.core.data.Blob; -import convex.core.util.Utils; - -/** - * Test class for Ed25519 functionality - */ -public class Ed25519Sign { - - public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, SignatureException { - - KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519", "BC"); - KeyPair kp = kpg.generateKeyPair(); - - { - byte[] enc=kp.getPrivate().getEncoded(); - System.out.println(enc.length + " bytes in private key encoding:"); - System.out.println(" => "+Blob.wrap(enc).toHexString()); - } - - { - byte[] enc=kp.getPublic().getEncoded(); - System.out.println(enc.length + " bytes in public key encoding:"); - System.out.println(" => "+Blob.wrap(enc).toHexString()); - } - - Signature sig = Signature.getInstance("Ed25519"); - sig.initSign(kp.getPrivate()); - - byte[] msg=Utils.hexToBytes("cafebabe"); - - sig.update(msg); - byte[] sbs = sig.sign(); - - System.out.println("Sig: "+Utils.toHexString(sbs)+" ("+sbs.length+" bytes)"); - - PublicKey pubKey=kp.getPublic(); - sig.initVerify(pubKey); - sig.update(msg); - System.out.println("Verify: "+sig.verify(sbs)); - - } - -} diff --git a/convex-peer/src/test/java/convex/examples/JoinTestNetwork.java b/convex-peer/src/test/java/convex/examples/JoinTestNetwork.java deleted file mode 100644 index a5b985be8..000000000 --- a/convex-peer/src/test/java/convex/examples/JoinTestNetwork.java +++ /dev/null @@ -1,51 +0,0 @@ -package convex.examples; - -import java.io.File; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.util.HashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; - -import convex.core.Peer; -import convex.core.crypto.AKeyPair; -import convex.core.data.AccountKey; -import convex.core.data.Address; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.exceptions.BadSignatureException; -import convex.core.util.Utils; -import convex.peer.API; -import convex.peer.Server; -import etch.EtchStore; - -public class JoinTestNetwork { - InetSocketAddress hostAddress=Utils.toInetSocketAddress("convex.world:18888"); - AKeyPair kp=AKeyPair.createSeeded(578578); // for user - Address acct=Address.create(47); - AccountKey peerKey=kp.getAccountKey(); - - public void testJoinNetwork() throws IOException, InterruptedException, ExecutionException, TimeoutException, BadSignatureException { - - System.out.println("PublicKey: "+kp.getAccountKey()); - - HashMap config=new HashMap<>(); - config.put(Keywords.KEYPAIR,kp); - config.put(Keywords.STORE,EtchStore.create(new File("temp-join-db.etch"))); - config.put(Keywords.CONTROLLER,acct); - config.put(Keywords.SOURCE,"convex.world:18888"); - - Server newServer=API.launchPeer(config); - - // make peer connections directly - newServer.getConnectionManager().connectToPeer(hostAddress); - - Thread.sleep(10000); - Peer peer=newServer.getPeer(); - System.out.println("State count:"+peer.getStates().count()); - } - - public static void main(String[] args) throws BadSignatureException, IOException, InterruptedException, ExecutionException, TimeoutException { - new JoinTestNetwork().testJoinNetwork(); - } -} diff --git a/convex-peer/src/test/java/convex/examples/PeerCluster.java b/convex-peer/src/test/java/convex/examples/PeerCluster.java deleted file mode 100644 index 407323c9a..000000000 --- a/convex-peer/src/test/java/convex/examples/PeerCluster.java +++ /dev/null @@ -1,110 +0,0 @@ -package convex.examples; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import convex.core.Constants; -import convex.core.State; -import convex.core.crypto.AKeyPair; -import convex.core.crypto.Ed25519KeyPair; -import convex.core.data.AString; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.AccountStatus; -import convex.core.data.Address; -import convex.core.data.BlobMap; -import convex.core.data.BlobMaps; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.Maps; -import convex.core.data.PeerStatus; -import convex.core.data.Strings; -import convex.core.data.Vectors; -import convex.core.util.Utils; -import convex.peer.API; -import convex.peer.Server; - -public class PeerCluster { - - private static final Logger log = LoggerFactory.getLogger(PeerCluster.class.getName()); - - public static final int NUM_PEERS = 5; - public static final ArrayList> PEER_CONFIGS = new ArrayList<>(NUM_PEERS); - public static final ArrayList PEER_KEYPAIRS = new ArrayList<>(NUM_PEERS); - - static { - // create a key pair for each peer - for (int i = 0; i < NUM_PEERS; i++) { - PEER_KEYPAIRS.add(Ed25519KeyPair.createSeeded(1000+i)); - } - - // create configuration maps for each peer - for (int i = 0; i < NUM_PEERS; i++) { - int port = Server.DEFAULT_PORT + i; - Map config = new HashMap<>(); - config.put(Keywords.PORT, port); - config.put(Keywords.KEYPAIR, PEER_KEYPAIRS.get(i)); - PEER_CONFIGS.add(config); - } - - State initialState = createInitialState(); - - for (int i = 0; i < NUM_PEERS; i++) { - Map config = PEER_CONFIGS.get(i); - config.put(Keywords.STATE, initialState); - } - } - - private static State createInitialState() { - // setting up NUM_PEERS peers with accounts 0..NUM_PEERS-1 - AVector accts = Vectors.empty(); - BlobMap peers = BlobMaps.empty(); - for (int i = 0; i < NUM_PEERS; i++) { - AccountKey peerKey = PEER_KEYPAIRS.get(i).getAccountKey(); - Map config = PEER_CONFIGS.get(i); - int port = Utils.toInt(config.get(Keywords.PORT)); - AString urlString = Strings.create("http://localhost"+ port); - Address address = Address.create(i); - PeerStatus ps = PeerStatus.create(address, 1000000000, Maps.create(Keywords.URL,urlString)); - peers = peers.assoc(peerKey, ps); - - AccountStatus as = AccountStatus.create(1000000000,peerKey); - accts = accts.conj(as); - } - - return State.create(accts, peers, Constants.INITIAL_GLOBALS, BlobMaps.empty()); - } - - public static void main(String... args) { - ArrayList peers = new ArrayList<>(NUM_PEERS); - - log.info("Creating peer configurations"); - for (Map config : PEER_CONFIGS) { - peers.add(API.launchPeer(config)); - } - - try { - log.info("Peers launched"); - - while (true) { - try { - Thread.sleep(1000); - // Log.info("Waiting..."); - } catch (InterruptedException e) { - log.warn("Sleep interrupted?"); - return; - } - } - } finally { - log.info("Server stopping...."); - for (Server peer : peers) { - peer.close(); - } - log.info("Server stopped successfully"); - } - } - -} diff --git a/convex-peer/src/test/java/convex/examples/SigSamples.java b/convex-peer/src/test/java/convex/examples/SigSamples.java deleted file mode 100644 index 7f5c14d8c..000000000 --- a/convex-peer/src/test/java/convex/examples/SigSamples.java +++ /dev/null @@ -1,27 +0,0 @@ -package convex.examples; - -import convex.core.crypto.AKeyPair; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.SignedData; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMLong; - -/** - * Test class for Ed25519 functionality - */ -public class SigSamples { - - public static void main(String[] args) { - - AKeyPair kp=AKeyPair.generate(); - AccountKey a=kp.getAccountKey(); - - AVector v=Vectors.of(1L,2L); - SignedData> sd=kp.signData(v); - System.out.println("Address: "+a); - System.out.println("Hash: "+v.getHash()); - System.out.println("Signature: "+sd.getSignature().toString()); - } - -} diff --git a/convex-peer/src/test/java/convex/net/ConnectionTest.java b/convex-peer/src/test/java/convex/net/ConnectionTest.java deleted file mode 100644 index 33daa7eec..000000000 --- a/convex-peer/src/test/java/convex/net/ConnectionTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package convex.net; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.io.IOException; -import java.util.ArrayList; - -import org.junit.Test; - -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadFormatException; -import convex.core.store.Stores; -import convex.core.util.Utils; - -/** - * Tests for the low level Connection class - */ -public class ConnectionTest { - - @Test - public void testMessageFlood() throws IOException, BadFormatException, InterruptedException { - final ArrayList received = new ArrayList<>(); - - MemoryByteChannel chan = MemoryByteChannel.create(100); - Connection conn=Connection.create(chan, null, Stores.current(), null); - - // create a custom PeerConnection and MessageReceiver for testing - // null Queue OK, we aren't queueing with our custom receive action - MessageReceiver mr = new MessageReceiver(a -> { - synchronized (received) { - received.add(a); - } - }, conn); - - Thread receiveThread=new Thread(()-> { - while (true) { - try { - mr.receiveFromChannel(chan); - if(Thread.interrupted()) return; - } catch (BadFormatException | IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - throw Utils.sneakyThrow(e); - } - } - }); - receiveThread.start(); - - int NUM=10000; - int sentCount = 0; - int resendCount = 0; - - for (int i=0; i received = new ArrayList<>(); - - MemoryByteChannel chan = MemoryByteChannel.create(10000); - Connection pc = Connection.create(chan, null, Stores.current(), null); - - // create a custom PeerConnection and MessageReceiver for testing - // null Queue OK, we aren't queueing with our custom receive action - MessageReceiver mr = new MessageReceiver(a -> received.add(a), pc); - - ACell msg1 = RT.cvm("Hello World!"); - assertTrue(pc.sendData(msg1)); - ACell msg2 = RT.cvm(13L); - assertTrue(pc.sendData(msg2)); - - // need to call sendBytes to flush send buffer to channel - // since we aren't using a Selector / SocketChannel here - assertTrue(pc.flushBytes()); - - // receive first message - mr.receiveFromChannel(chan); - assertEquals(1, received.size()); - assertEquals(msg1, received.get(0).getPayload()); - - // receive second message - mr.receiveFromChannel(chan); - assertEquals(2, received.size()); - assertEquals(msg2, received.get(1).getPayload()); - - Message m1 = received.get(0); - assertEquals(MessageType.DATA, m1.getType()); - } -} diff --git a/convex-peer/src/test/java/convex/peer/MessageTest.java b/convex-peer/src/test/java/convex/peer/MessageTest.java deleted file mode 100644 index 27d8db469..000000000 --- a/convex-peer/src/test/java/convex/peer/MessageTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package convex.peer; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; - -import convex.core.exceptions.BadFormatException; -import convex.net.MessageType; - -public class MessageTest { - - @Test - public void testTypes() throws BadFormatException { - MessageType[] types = MessageType.values(); - assertEquals(11, types.length); - - for (MessageType t : types) { - assertSame(t, MessageType.decode(t.getMessageCode())); - } - } - - @Test - public void testBadCode() { - assertThrows(BadFormatException.class, () -> MessageType.decode(-1)); - } -} diff --git a/convex-peer/src/test/java/convex/peer/RestoreTest.java b/convex-peer/src/test/java/convex/peer/RestoreTest.java deleted file mode 100644 index bde9bc68c..000000000 --- a/convex-peer/src/test/java/convex/peer/RestoreTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package convex.peer; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; - -import org.junit.jupiter.api.Test; - -import convex.api.Convex; -import convex.core.Result; -import convex.core.State; -import convex.core.crypto.AKeyPair; -import convex.core.data.AccountKey; -import convex.core.data.Address; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.Lists; -import convex.core.data.Maps; -import convex.core.init.Init; -import convex.core.lang.Symbols; -import convex.core.store.AStore; -import convex.core.transactions.Invoke; -import etch.EtchStore; - -public class RestoreTest { - AKeyPair KP=AKeyPair.createSeeded(123456781); - List keys=Lists.of(KP.getAccountKey()); - - State GENESIS=Init.createState(keys); - Address HERO=Init.GENESIS_ADDRESS; - - @Test - public void restoreTest() throws IOException, InterruptedException, ExecutionException, TimeoutException { -// { -// System.out.println("Test store = "+Stores.current()); -// -// State s=Init.STATE; -// System.out.println("Init Ref = "+s.getRef()); -// -// Ref ref=Ref.forHash(s.getHash()); -// if (ref.isMissing()) { -// System.out.println("State not stored"); -// } else { -// State s2=ref.getValue(); -// System.out.println("Store ref: "+s2.getRef()); -// } -// } - - AStore store=EtchStore.createTemp(); - Map config = Maps.hashMapOf( - Keywords.KEYPAIR,KP, - Keywords.STATE,GENESIS, - Keywords.STORE,store, - Keywords.URL,null, - Keywords.PERSIST,true - ); - Server s1=API.launchPeer(config); - - // Connect with HERO Account - Convex cvx1=Convex.connect(s1); - - Result tx1=cvx1.transactSync(Invoke.create(HERO,1, Symbols.STAR_ADDRESS)); - assertEquals(HERO,tx1.getValue()); - Long balance1=cvx1.getBalance(HERO); - assertTrue(balance1>0); - s1.close(); - - // TODO: testing that server is definitely down. This is a bit slow.... - // assertThrows(Throwable.class,()->cvx1.getBalance(HERO)); - - // Launch peer and connect - Server s2=API.launchPeer(config); - - assertNull(s2.getHostname()); - - Convex cvx2=Convex.connect(s2.getHostAddress(), HERO,KP); - - // TODO: check this? - // Long balance2=cvx2.getBalance(HERO); - // assertEquals(balance1,balance2); - - Result tx2=cvx2.transactSync(Invoke.create(HERO,2, Symbols.BALANCE)); - assertFalse(tx2.isError()); - - State state=s2.getPeer().getConsensusState(); - assertNotNull(state); - } -} \ No newline at end of file diff --git a/convex-peer/src/test/java/convex/peer/ServerTest.java b/convex-peer/src/test/java/convex/peer/ServerTest.java deleted file mode 100644 index 6d43c55a6..000000000 --- a/convex-peer/src/test/java/convex/peer/ServerTest.java +++ /dev/null @@ -1,289 +0,0 @@ -package convex.peer; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.util.HashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.Consumer; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import convex.api.Convex; -import convex.core.Belief; -import convex.core.Coin; -import convex.core.ErrorCodes; -import convex.core.Result; -import convex.core.State; -import convex.core.crypto.AKeyPair; -import convex.core.data.ACell; -import convex.core.data.AVector; -import convex.core.data.AccountKey; -import convex.core.data.Address; -import convex.core.data.Hash; -import convex.core.data.Keyword; -import convex.core.data.Keywords; -import convex.core.data.Maps; -import convex.core.data.Ref; -import convex.core.data.SignedData; -import convex.core.data.Vectors; -import convex.core.data.prim.CVMLong; -import convex.core.exceptions.BadSignatureException; -import convex.core.init.Init; -import convex.core.lang.RT; -import convex.core.lang.Reader; -import convex.core.lang.Symbols; -import convex.core.store.AStore; -import convex.core.store.Stores; -import convex.core.transactions.ATransaction; -import convex.core.transactions.Call; -import convex.core.transactions.Invoke; -import convex.core.transactions.Transfer; -import convex.core.util.Utils; -import convex.net.Connection; -import convex.net.Message; -import convex.net.ResultConsumer; -import etch.EtchStore; - -/** - * Tests for a fresh standalone server cluster instance - */ -public class ServerTest { - - private static final Logger log = LoggerFactory.getLogger(ServerTest.class.getName()); - - private HashMap results = new HashMap<>(); - - private static TestNetwork network; - - private Consumer handler = new ResultConsumer() { - @Override - protected synchronized void handleNormalResult(long id, ACell value) { - String msg=id+ " : "+Utils.toString(value); - //System.err.println(msg); - log.debug(msg); - results.put(id, value); - } - - @Override - protected synchronized void handleError(long id, ACell code, ACell message) { - String msg=id+ " ERR: "+Utils.toString(code)+ " : "+message; - //System.err.println(msg); - log.debug(msg); - - results.put(id, code); - } - }; - - @BeforeAll - public static void init() { - network = TestNetwork.getInstance(); - } - - @Test - public void testServerConnect() throws IOException, InterruptedException, TimeoutException { - InetSocketAddress hostAddress=network.SERVER.getHostAddress(); - - // Connect to Peer Server using the current store for the client - Connection pc = Connection.connect(hostAddress, handler, Stores.current()); - AVector v = Vectors.of(1l, 2l, 3l); - long id1 = pc.sendQuery(v,network.HERO); - Utils.timeout(5000, () -> results.get(id1) != null); - assertEquals(v, results.get(id1)); - } - -// Commented out because it's slow.... -// @Test -// public void testServerFlood() throws IOException, InterruptedException { -// InetSocketAddress hostAddress=server.getHostAddress(); -// // This is a test of flooding a client connection with async messages. Should eventually throw an IOExcepion -// // from backpressure and *not* bring down the server. -// Convex convex=Convex.connect(hostAddress, VILLAIN_ADDRESS,Init.VILLAIN_KEYPAIR); -// -// Object cmd=Reader.read("(def tmp (inc tmp))"); -// assertThrows(IOException.class, ()-> { -// for (int i=0; i<1000000; i++) { -// convex.transact(Invoke.create(VILLAIN_ADDRESS, 0, cmd)); -// } -// }); -// } - - @Test - public void testBalanceQuery() throws IOException, TimeoutException { - Convex convex=Convex.connect(network.SERVER.getHostAddress(),network.VILLAIN,network.VILLAIN_KEYPAIR); - - // test the connection is still working - assertNotNull(convex.getBalance(network.VILLAIN)); - } - - @Test - public void testConvexAPI() throws IOException, InterruptedException, ExecutionException, TimeoutException { - Convex convex=Convex.connect(network.SERVER.getHostAddress(),network.VILLAIN,network.VILLAIN_KEYPAIR); - - Future f=convex.query(Symbols.STAR_BALANCE); - convex.core.Result f2=convex.querySync(Symbols.STAR_ADDRESS); - - assertEquals(network.VILLAIN,f2.getValue()); - assertTrue(f.get().getValue() instanceof CVMLong); - - - convex.core.Result r3=convex.querySync(Reader.read("(fail :foo)")); - assertTrue(r3.isError()); - assertEquals(ErrorCodes.ASSERT,r3.getErrorCode()); - assertEquals(Keywords.FOO,r3.getValue()); - assertNotNull(r3.getTrace()); - } - - @Test - public void testMissingData() throws IOException, InterruptedException, TimeoutException { - - InetSocketAddress hostAddress=network.SERVER.getHostAddress(); - - // Connect to Peer Server using the current store for the client - AStore store=Stores.current(); - Connection pc = Connection.connect(hostAddress, handler, store); - State s=network.SERVER.getPeer().getConsensusState(); - Hash h=s.getHash(); - - boolean sent=pc.sendMissingData(h); - assertTrue(sent); - - Thread.sleep(200); - Ref ref=Ref.forHash(h); - assertNotNull(ref); - } - - @Test - public void testJoinNetwork() throws IOException, InterruptedException, ExecutionException, TimeoutException, BadSignatureException { - AKeyPair kp=AKeyPair.generate(); - AccountKey peerKey=kp.getAccountKey(); - - long STAKE=1000000000; - synchronized(network.SERVER) { - Convex heroConvex=network.CONVEX; - - // Create new peer controller account - Address controller=heroConvex.createAccountSync(kp.getAccountKey()); - Result trans=heroConvex.transferSync(controller,Coin.DIAMOND); - assertFalse(trans.isError()); - - // create test user account - Address user=heroConvex.createAccountSync(kp.getAccountKey()); - trans=heroConvex.transferSync(user,STAKE); - assertFalse(trans.isError()); - - Convex convex=Convex.connect(network.SERVER.getHostAddress(), controller, kp); - trans=convex.transactSync(Invoke.create(controller, 0, "(create-peer "+peerKey+" "+STAKE+")")); - assertEquals(RT.cvm(STAKE),trans.getValue()); - //Thread.sleep(1000); // sleep a bit to allow background stuff - - HashMap config=new HashMap<>(); - config.put(Keywords.KEYPAIR,kp); - config.put(Keywords.STORE,EtchStore.createTemp()); - config.put(Keywords.SOURCE,network.SERVER.getHostAddress()); - - Server newServer=API.launchPeer(config); - - // make peer connections directly - newServer.getConnectionManager().connectToPeer(network.SERVER.getHostAddress()); - network.SERVER.getConnectionManager().connectToPeer(newServer.getHostAddress()); - - // should be in consensus at this point since just synced - // note: shouldn't matter which is the current store - assertEquals(newServer.getPeer().getConsensusState(),network.SERVER.getPeer().getConsensusState()); - - Convex client=Convex.connect(newServer.getHostAddress(), user, kp); - assertEquals(user,client.transactSync(Invoke.create(user, 0, "*address*")).getValue()); - - Result r=client.requestStatus().get(1000,TimeUnit.MILLISECONDS); - assertFalse(r.isError()); - } - } - - @Test - public void testAcquireBelief() throws IOException, InterruptedException, ExecutionException, TimeoutException, BadSignatureException { - synchronized(network.SERVER) { - - Convex convex=network.CONVEX; - - Future statusFuture=convex.requestStatus(); - Result status=statusFuture.get(10000,TimeUnit.MILLISECONDS); - assertFalse(status.isError()); - AVector v=status.getValue(); - Hash h=RT.ensureHash(v.get(0)); - - Future> acquiror=convex.acquire(h); - SignedData ab=acquiror.get(10000,TimeUnit.MILLISECONDS); - assertTrue(ab.getValue() instanceof Belief); - assertEquals(h,ab.getHash()); - } - } - - @Test - public void testAcquireState() throws IOException, InterruptedException, ExecutionException, TimeoutException, BadSignatureException { - synchronized(network.SERVER) { - - Convex convex=network.CONVEX; - - State s=convex.acquireState().get(60000,TimeUnit.MILLISECONDS); - assertTrue(s instanceof State); - } - } - - public long checkSent(Connection pc,SignedData st) throws IOException { - long x=pc.sendTransaction(st); - assertTrue(x>=0); - return x; - } - - @Test - public void testServerTransactions() throws IOException, InterruptedException, TimeoutException { - synchronized(network.SERVER) { - InetSocketAddress hostAddress=network.SERVER.getHostAddress(); - - // Connect to Peer Server using the current store for the client - Connection pc = Connection.connect(hostAddress, handler, Stores.current()); - Address addr=network.SERVER.getPeerController(); - long s=network.SERVER.getPeer().getConsensusState().getAccount(addr).getSequence(); - AKeyPair kp=network.SERVER.getKeyPair(); - long id1 = checkSent(pc,kp.signData(Invoke.create(addr, s+1, Reader.read("[1 2 3]")))); - long id2 = checkSent(pc,kp.signData(Invoke.create(addr, s+2, Reader.read("(return 2)")))); - long id2a = checkSent(pc,kp.signData(Invoke.create(addr, s+2, Reader.read("22")))); - long id3 = checkSent(pc,kp.signData(Invoke.create(addr, s+3, Reader.read("(do (def foo :bar) (rollback 3))")))); - long id4 = checkSent(pc,kp.signData(Transfer.create(addr, s+4, addr, 1000))); - long id5 = checkSent(pc,kp.signData(Call.create(addr, s+5, Init.REGISTRY_ADDRESS, Symbols.FOO, Vectors.of(Maps.empty())))); - long id6bad = checkSent(pc,kp.signData(Invoke.create(addr.offset(2), s+6, Reader.read("(def a 1)")))); - long id6 = checkSent(pc,kp.signData(Invoke.create(addr, s+6, Reader.read("foo")))); - - long last=id6; - - assertTrue(last>=0); - assertTrue(!pc.isClosed()); - - // wait for results to come back - assertFalse(Utils.timeout(10000, () -> results.containsKey(last))); - Thread.sleep(100); // bit more time in case something out of order? - - AVector v = Vectors.of(1l, 2l, 3l); - assertEquals(v, results.get(id1)); - assertEquals(RT.cvm(2L), results.get(id2)); - assertEquals(ErrorCodes.SEQUENCE, results.get(id2a)); - assertEquals(RT.cvm(3L), results.get(id3)); - assertEquals(RT.cvm(1000L), results.get(id4)); - assertTrue( results.containsKey(id5)); - assertEquals(ErrorCodes.SIGNATURE, results.get(id6bad)); - assertEquals(ErrorCodes.UNDECLARED, results.get(id6)); - } - } - -} diff --git a/convex-peer/src/test/java/convex/peer/TestNetwork.java b/convex-peer/src/test/java/convex/peer/TestNetwork.java deleted file mode 100644 index 07ebf95f8..000000000 --- a/convex-peer/src/test/java/convex/peer/TestNetwork.java +++ /dev/null @@ -1,85 +0,0 @@ -package convex.peer; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -import convex.api.Convex; -import convex.core.State; -import convex.core.crypto.AKeyPair; -import convex.core.data.AccountKey; -import convex.core.data.Address; -import convex.core.init.Init; -import convex.core.util.Utils; - -/** - * Singleton server cluster instance - */ -public class TestNetwork { - - public Server SERVER = null; - - private List SERVERS; - - public Convex CONVEX; - - // Deterministic keypairs - public AKeyPair[] KEYPAIRS = new AKeyPair[] { - AKeyPair.createSeeded(2), - AKeyPair.createSeeded(3), - AKeyPair.createSeeded(5), - AKeyPair.createSeeded(7), - AKeyPair.createSeeded(11), - AKeyPair.createSeeded(13), - AKeyPair.createSeeded(17), - AKeyPair.createSeeded(19), - }; - - public ArrayList PEER_KEYPAIRS=(ArrayList) Arrays.asList(KEYPAIRS).stream().collect(Collectors.toList()); - public ArrayList PEER_KEYS=(ArrayList) Arrays.asList(KEYPAIRS).stream().map(kp->kp.getAccountKey()).collect(Collectors.toList()); - - public AKeyPair FIRST_PEER_KEYPAIR = KEYPAIRS[0]; - public AccountKey FIRST_PEER_KEY = FIRST_PEER_KEYPAIR.getAccountKey(); - - public AKeyPair HERO_KEYPAIR = KEYPAIRS[0]; - public AKeyPair VILLAIN_KEYPAIR = KEYPAIRS[1]; - - public AccountKey HERO_KEY = HERO_KEYPAIR.getAccountKey(); - - public Address HERO; - public Address VILLAIN; - - private static TestNetwork instance = null; - - private TestNetwork() { - // Use fresh State - State s=Init.createState(PEER_KEYS); - HERO=Address.create(Init.GENESIS_ADDRESS); - VILLAIN=HERO.offset(1); - - SERVERS=API.launchLocalPeers(PEER_KEYPAIRS, s); - } - - private void waitForLaunch() { - if (SERVER == null) { - SERVER = SERVERS.get(0); - try { - // Thread.sleep(1000); - API.isNetworkReady(SERVERS, 10000); - CONVEX=Convex.connect(SERVER.getHostAddress(), HERO, HERO_KEYPAIR); - } catch (Throwable t) { - throw Utils.sneakyThrow(t); - } - } - API.isNetworkReady(SERVERS, 10000); - } - - public static TestNetwork getInstance() { - if (instance == null) { - instance = new TestNetwork(); - } - instance.waitForLaunch(); - return instance; - } -} diff --git a/convex.bat b/convex.bat deleted file mode 100644 index 22920a97c..000000000 --- a/convex.bat +++ /dev/null @@ -1,3 +0,0 @@ -@echo off -java -jar convex-cli/target/convex-cli.jar %* -exit /b %errorlevel% diff --git a/docs/coding-principles.md b/docs/coding-principles.md deleted file mode 100644 index 93fc5150b..000000000 --- a/docs/coding-principles.md +++ /dev/null @@ -1,74 +0,0 @@ -## Coding principles - -### Immutable first - -Everything is an immutable data structure, with the exception of necessary -mutable values for either: -1. Locally managed state -2. Lazy computation / caching - -### Trust the JVM - -We unashamedly exploit the JVM as an excellent runtime platform for decentralised sysytems. -It's very good at what it does, in particular the following attributes are very useful: - -- Efficient GC of short-lived objects. Much cheaper than C++ heap allocations, in fact. -- Fast JIT compiler. Close enough to C++ that we don't care. -- Rich runtime library. We don't need many external dependencies, which add complexity -and present security risks. -- Memory safety. No buffer overflows to worry about. -- Portability. This makes it easy to deploy pretty much anywhere. - -The use of a memory-managed runtime is of particular importance to this project. -We absolutely require top class garbage collection to clear unnecessary data from memory -while also ensuring that we can exploit structural sharing of persistent data structures. -We also need the exploit soft references for lazy loading of data structures -that can be evicted when no longer required. Alternative means of managing this -(reference counting etc.) were judged infeasible for performance and complexity reasons. - -### Canonical format - -Our data representations make use of a single, canonical data format. Advantages: - -- Sorting order guaranteed and stable -- Better caching / de-duplication with hashes -- Identity comparison == hash - -### Defensive coding - -Assume that you are being passed bad / malicious inputs. Check everything, -especially if there is any chance that it may have come from an external system. - -### Fail Fast - -Stop the current operation as soon as any unexpected error occurs. Throw an exception so -that a higher level operation can determine what step to take. - -### Common sense - -Any of the the above principles can be overridden by reason, evidence and common sense. - -"The three great essentials to achieve anything worthwhile are, first, hard work; second, stick-to-itiveness; third, common sense." -― Thomas Edison - - -## Some Inspirations - -- *Haskell* - for its functional purity, and attribute which is extremely valuable for -decentralised systems. - -- *Lisp* - for demonstrationg the power of homoiconicity, and the ability to bootstrap a -languge ecosystem with just a few core primatives closely linked to the Lambda Calculus. - -- *Clojure* - primarily for its syntax and functional styole, an elegant evolution of Lisp -for the modern age. - -- *Persistent data structures* - functional data structures that enable efficient operations -such as update while preserving previous copies of data in an immutable fashion. - -- *Java* - for giving us the JVM, an unusually robust and high-performance platform -for implementing systems of this nature. - -- *Ethereum* - for demonstrating a working decentralised execution engine. - -- *Bitcoin* - for demonstrating decentralised consensus, albeit in a very inefficient fashion \ No newline at end of file diff --git a/docs/wip/ethos.md b/docs/wip/ethos.md deleted file mode 100644 index 8d5c49eb8..000000000 --- a/docs/wip/ethos.md +++ /dev/null @@ -1,23 +0,0 @@ -## Convex Ethos - -### Privacy - -We support the principle that individuals should have a right to privacy, and the Convex Organisation will not disclose any personal information without the individual's consent. - -This principle is also built into the Convex protocol at a technical level, as participants may choose to operate under pseudonymous accounts. - -### Strong Opinions, Weakly Held - -Producing a complex product requires hard decisions. Making decisions well is a difficult art. Nobody has all the answers, and the best solutions come from incorporating the strongest ideas, regardless of source. - -In order to have a realistic plan, it is necessary to form an initial hypothesis on the solution. This is not necessarily the final answer, but a starting assumption on which to iterate and improve. As new information becomes available, the team should be willing to amend this hypothesis in light of new information and experience. - -### Obligation to Dissent - -It is our belief that the best ideas are those that are developed under an environment of honest, constructive criticism. It is therefore our expectation that all team members should speak up if they feel that something is being done the wrong way, and have a better solution to propose. - -### Backwards Compatibility - -We commit to maintaining backwards compatibility as a core feature of the Convex system. It is unacceptable for breaking changes to occur in underlying infrastructure once working systems are in production. - -Special exceptions may be made in extreme circumstances (e.g. fixing fundamental security flaws). It is our goal that this never occurs, but we must be prepared for such an eventuality. \ No newline at end of file diff --git a/docs/wip/governance.md b/docs/wip/governance.md deleted file mode 100644 index a7be2d442..000000000 --- a/docs/wip/governance.md +++ /dev/null @@ -1,19 +0,0 @@ - - -## Non-technical Challenges - -As well as technical challenges, the Blockchain sector has faced some significant non-technical challenges. - -* **Illegal Activity** - The anonymous nature of blockchain technology has unfortunately proved valuable for criminals wishing to transfer assets without being identified or tracked. It has facilitated a number of scams and schemes to "get rich quick" of dubious legality. It has also proved rich picking for "black hat" hackers exploiting other users of the network. -* **Over promising** - Many projects launched on a wave of "Blockchain Hype" promised the world, but in fact were unable to deliver. This tendency to over-hype technology is not new (witness the "dotcom bubble") but certainly harmed the credibility of the sector. -* **Inappropriate Application** - In many cases, applications being built with blockchain technology (particularly in the "Enterprise" space) would be better served by traditional database / server technology. While blockchains offer some significant benefits, they require significant trade-offs. In general, applications that are operated by a controlled set of trusted participants do not require decentralisation. -* **Education** - Typical users often do not have the skills to interact with decentralised applications directly, such as management of private keys. While 3rd parties may innovate in providing key management services or more pleasant user experiences, this re-introduces the problem of trusting a centralised gatekeeper, and opens up more potential security risks. -* **Forking** - major blockchain networks have seen "forks" where ecosystems have broken into two or more incompatible networks, perhaps over political differences on whether or not to make a specific technical change on the network. While forking may allow for some interesting technical differentiation, it is an unhelpful property for economic systems - where the ability for economic actors to transact freely with all others is important. - -### Fundamental Principles - -* **Decentralisation** : Economic value exchange must be a public good, open to all, and not controlled by any centralised entity. Without decentralisation, the digital economy will be unfair (not accessible to all) and inefficient (monopoly rents and barriers to entry imposed by centralised gatekeepers). -* **Governance**: The Convex network itself, should serve the interests of the whole community of users, and network governance must be neutral in all matters other than technical operation and improvements to the network. -* **Transparency**: As a public service, the state of the network and all transactions should be publicly visible and verifiable to all. -* **Privacy**: Convex may be used anonymously to protect individual identities. -* **Trust**: In economic transactions, it is often necessary for participants to trust the legal entity they are transacting with. It is important to provide safeguards against illegal activity, and recourse to real-world legal and regulatory solutions. \ No newline at end of file diff --git a/docs/wip/misc-faq.md b/docs/wip/misc-faq.md deleted file mode 100644 index baee4ed52..000000000 --- a/docs/wip/misc-faq.md +++ /dev/null @@ -1,16 +0,0 @@ -## Why should I use Convex? - -That's up to you! But we hope you'll find it compelling for these sort of reasons: - -- A fun and empowering experience as a developer -- The ability to easily build powerful decentralised applications -- Probably the best overall performance of any decentralised platform - - -## But XXX is faster! It can do YY million transactions per second! - -Maybe. We're not really in the game of competing on the basis of meaningless benchmarks. - -It's important to remember that performance isn't about a single number. It's about the being able to get what you want done quickly and efficiently, as a user of the system. - -We've seen plenty of big performance claims for decentralised platforms. The headlines may be impressive, but a lot of these don't really stack up when you examine more closely. Usually there are some significant compromises made to achieve these numbers. You can look out for: Unrealistic testing setups. Relaxed security requirements (e.g. using PoA networks). Networks that can't handle general purpose smart contracts. Issues with transactions that span across shards. Long time delays to confirm final consensus. \ No newline at end of file diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 84906e05d..000000000 --- a/pom.xml +++ /dev/null @@ -1,207 +0,0 @@ - - 4.0.0 - world.convex - convex - 0.7.0-rc3 - pom - - Convex Parent - Parent POM used to build core Convex modules. - https://convex.world - - - convex-core - convex-cli - convex-gui - convex-peer - convex-benchmarks - - - - 11 - 11 - UTF-8 - 1.32 - 5.7.1 - 1.7.31 - ${project.version} - - 2020-02-02T00:00:00Z - - --illegal-access=permit - --add-opens=java.base/java.util=ALL-UNNAMED - - - - - - Convex Public License - https://github.com/Convex-Dev/convex/blob/master/LICENSE.md - - - - - - Mike Anderson - mike@convex.world - Convex Foundation - https://convex.world - - - - - scm:git:git://github.com/Convex-Dev/convex.git - scm:git:ssh://github.com:Convex-Dev/convex.git - https://github.com/Convex-Dev/convex.git - - - - - ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots - - - - - - only-eclipse - - - m2e.version - - - - - - - - org.eclipse.m2e - lifecycle-mapping - 1.0.0 - - - - - - - - - - - - - - release - - - performRelease - true - - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 3.0.1 - - - sign-artifacts - verify - - sign - - - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.2.1 - - - attach-sources - - jar-no-fork - - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.2.0 - - syntax - - - - - attach-javadocs - - jar - - - - - - - - - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - 3.0.0-M3 - - - enforce-maven - - enforce - - - - - 3.6 - - - - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.8 - true - - ossrh - https://s01.oss.sonatype.org/ - true - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.0.0-M4 - - - - - From 85edf81c19cae405eed9c7f97f3ab033cdbe937c Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 3 Sep 2021 10:02:36 +0800 Subject: [PATCH 0003/1041] Add gitignore --- convex-cli/.gitignore | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 convex-cli/.gitignore diff --git a/convex-cli/.gitignore b/convex-cli/.gitignore new file mode 100644 index 000000000..e69de29bb From 155493703c5d1fca21d7aecc728cefe1bbd91dff Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 6 Sep 2021 09:07:46 +0000 Subject: [PATCH 0004/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8b3caed2e563d218ae5b68f48189b8ffb077a8da?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/.gitignore | 0 convex-cli/images/convex_logo.svg | 1 + convex-cli/index.html | 2071 +++++++++++++++++++++++++++++ 3 files changed, 2072 insertions(+) delete mode 100644 convex-cli/.gitignore create mode 100644 convex-cli/images/convex_logo.svg create mode 100644 convex-cli/index.html diff --git a/convex-cli/.gitignore b/convex-cli/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/convex-cli/images/convex_logo.svg b/convex-cli/images/convex_logo.svg new file mode 100644 index 000000000..84fcb7786 --- /dev/null +++ b/convex-cli/images/convex_logo.svg @@ -0,0 +1 @@ +Convex logoCreated with Sketch. diff --git a/convex-cli/index.html b/convex-cli/index.html new file mode 100644 index 000000000..d15ff66f1 --- /dev/null +++ b/convex-cli/index.html @@ -0,0 +1,2071 @@ + + + + + + + +Convex Command Line Interface + + + + + + + + + +
+
+
+
+
+
+Convex logoCreated with Sketch. +
+
+
+
+
+

Introduction

+
+
+

Convex Command Line Interface or CLI, allows you to control and setup a local Convex network, or add a peer to an already existing network. +The current test Convex network can be found at convex.world.

+
+
+

Overview

+
+

This CLI is part of the Convex code base. The CLI has been built to do the following things:

+
+
+
    +
  1. +

    Run a Local Convex network.

    +
  2. +
  3. +

    Run a Local Peer(s) connected to a local network.

    +
  4. +
  5. +

    Send and Query transactions within a local network.

    +
  6. +
  7. +

    Setup accounts within a local network.

    +
  8. +
  9. +

    Step key pairs to use for accessing local and remote networks.

    +
  10. +
  11. +

    Run a Local Peer connected to a remote Convex network.

    +
  12. +
+
+
+
+
+
+

Getting Started

+
+
+

As yet we have not packaged this in any package manager for downloading so currently the CLI can only be downloaded via github.

+
+
+

Get the source

+
+

You will need to visit convex-dev @ github and clone the repository onto your local computer.

+
+
+

The develop branch is currently the latest version.

+
+
+
terminal commands
+
+
git clone https://github.com/Convex-Dev/convex.git
+cd convex
+git checkout develop
+git pull
+
+
+
+
+

Convex Projects

+
+

The Convex code repository is made up of the following sub projects:

+
+
+
+
convex-benchmaks
+
+

Run benchmarks on the convex network.

+
+
convex-cli
+
+

This CLI project.

+
+
convex-core
+
+

The main convex core library.

+
+
convex-gui
+
+

A local convex network running as a GUI application.

+
+
convex-peer
+
+

Peer library used to run convex peers.

+
+
+
+
+
+

Compile and Setup

+
+

Once you have downloaded the latest source of Convex, you can now compile the suite of projects.

+
+
+

To do this you need to execute the Maven command:

+
+
+
terminal command
+
+
mvn install
+
+
+
+

or

+
+
+
terminal command
+
+
mvn package
+
+
+
+

If you wish to build without running the tests you can append the option -DskipTests

+
+
+

After building and installing the maven dependencies you should eventually see the following lines +generated by the Maven build process:

+
+
+
output
+
+
[INFO] ------------------------------------------------------------------------
+[INFO] Reactor Summary for convex 0.7.0-SNAPSHOT:
+[INFO]
+[INFO] convex ............................................. SUCCESS [  0.146 s]
+[INFO] convex-core ........................................ SUCCESS [  5.003 s]
+[INFO] convex-peer ........................................ SUCCESS [  0.027 s]
+[INFO] convex-gui ......................................... SUCCESS [  2.474 s]
+[INFO] convex-cli ......................................... SUCCESS [  4.665 s]
+[INFO] convex-benchmarks .................................. SUCCESS [  1.644 s]
+[INFO] ------------------------------------------------------------------------
+[INFO] BUILD SUCCESS
+[INFO] ------------------------------------------------------------------------
+[INFO] Total time:  14.463 s
+[INFO] Finished at: 0000-00-00T00:00:00+00:00
+[INFO] ------------------------------------------------------------------------
+
+
+
+
+

Files needed by CLI run a local Network or Peer

+
+

The CLI needs 3 types of files before running a local Convex network or as a Peer on any network. +The type of files are:

+
+
+
    +
  1. +

    Etch Storage database file. This contains the stored state of the Convex network. Usually, when starting up the initial cluster the first set of peers share the same Etch database. CLI Parameter: --etch

    +
  2. +
  3. +

    Keystore database file. This file contains the private/public key pairs used for the peers and any subsequent users. CLI Parameters: --keystore, --password

    +
  4. +
  5. +

    Session file. This is created by the CLI to keep track of the locally running peers, so that if you want to access the local network or add another peer to the local network, the CLI will look at the session file for a randomly available peer to connect too. CLI Parameter: --session

    +
  6. +
+
+
+ + + + + +
+ + +
+

The GUI version and the CLI run the same local network. The only difference is that the GUI does not create a session file. This means that some of the CLI features cannot be used with the GUI local network.

+
+
+
+
+
+
+
+

Running the CLI

+
+
+

Once you have successfully compiled and built Convex projects, you can now run the command line tool.

+
+
+
Mac
+
+
./convex help
+
+
+
+
Linux
+
+
./convex help
+
+
+
+
Windows
+
+
convex help
+
+
+
+

Commands

+
+

The CLI is split into command the following commands and subcommands:

+
+
+
+
Account Commands
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandSub commandDescription

account, ac

Manages convex accounts.

balance, bal, ba

Get an account balance.

create, cr

Creates an account on a local network using a public/private key from the keystore.

fund, fu

Transfers funds to an account using a public/private key from the keystore.

information, info, in

Get account information.

+
+
+
Key Commands
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandSub commandDescription

key, ke

Manage local Convex key store.

import, im

Import key pairs to the keystore.

generate, ge

Generate one or more key pairs.

list, li

List available key pairs.

export, ex

Export key pair from the keystore.

+
+
+
Local Commands
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
CommandSub commandDescription

local, lo

Operates a local convex network.

gui

Starts a local convex test network using the peer manager GUI application.

start, st

Starts a local convex test network, same as GUI but using a command line.

+
+
+
Peer Commands
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
CommandSub commandDescription

peer, pe

Operates a local peer.

create, cr

Creates a keypair, new account and a funding stake: to run a local peer.

start, st

Starts a local peer.

+
+
+
Query Command
+
+
+ ++++ + + + + + + + + + + + + +
CommandDescription

query, qu

Execute a query on the current peer.

+
+
+
Status Command
+
+
+ ++++ + + + + + + + + + + + + +
CommandDescription

status, st

Reports on the current status of the network.

+
+
+
Transaction Command
+
+
+ ++++ + + + + + + + + + + + + +
CommandDescription

transaction, transact, tr

Execute a transaction on the network via a peer.

+
+
+
Help Command
+
+
+ ++++ + + + + + + + + + + + + +
CommandDescription

help

Displays help information about the specified command

+
+
+

Shared Options

+
+

There are a few common options that can be used with any command or sub command. They are as follows:

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Short OptionLong OptionDescription

-c

--config=<configFilename>

Use the specified config file.

-e

--etch=<etchStoreFilename>

Convex state storage filename. The default is to use a temporary storage filename.

-k

--keystore=<keyStoreFilename>

keystore filename. Default: ~/.convex/keystore.pfx

-p

--password=<password>

Password to read/write to the Keystore

-s

--session=<sessionFilename>

Session filename. Defaults ~/.convex/session.conf

-v

--verbose

Show more verbose log information. You can increase verbosity by using multiple -v or -vvv

-h

--help

Show this help message and exit.

-V

--version

Print version information and exit.

+
+
+

Requesting Help

+
+

The CLI supports help using the -h or --help options or the command help. For each sub command there are more help options.

+
+
+

So for example

+
+
+
terminal command
+
+
./convex --help
+
+
+
+

will show the common options for all commands, and the list of available commands.

+
+
+
terminal command
+
+
./convex local start --help
+
+
+
+

will show the common options as well as the specific options for the convex.local.start command

+
+
+
+
+
+

Starting a local network

+
+
+

The CLI is designed to start a local Convex network. This will allow for the developer/tester to try out Convex in a local environment without +effecting any other networks.

+
+
+

Simple local start

+
+

The simplest way to start up the local Convex network is to run the following command:

+
+
+
terminal command
+
+
./convex local start --password=my-password
+
+
+
+ + + + + +
+ + +
+

In this document, the password option will always be shown as --password=my-password. This is an example of a not very good password to use for storing your keys. We suggest that you use a more secure password instead of my-password.

+
+
+
+
+

You will always need to pass the password to the keystore file since the CLI will need access to the keys to create and start up the local peers.

+
+
+

The CLI will automatically create 4 keypairs and place them in the keystore. The CLI will then start up 4 peers all sharing a single +temporary local Etch Database in the /tmp folder.

+
+
+

The Simple local start consists of the following steps:

+
+
+
    +
  1. +

    Create the count number of peer keypairs.

    +
  2. +
  3. +

    Store the new keypairs in the keystore.

    +
  4. +
  5. +

    Start up the local network using the newly created keys.

    +
  6. +
+
+
+
+

Local start with peer keys

+
+

While the simple local network start will auto-generate public keys for the local peers and create the peer accounts. You have the option instead to start the local network using a predetermined set of keys from your keystore. To do this you need to provide a list of public keys that you want the CLI to use to start up the local network.

+
+
+

If you have already used the simple local start, you can get the list of keys created by running the List keys, +this will show you the list of keys that have been stored in the key store.

+
+
+
terminal session
+
+
./convex key list --password=my-password
+
+Index Public Key
+1 6e89035fce6d842b65e7831433fb3426928865a3c8de9536cfa50a1928eb0276 (1)
+2 13e691e05dee5a2c5ad90f6802f4ac5c274582ca5332516dc4740ae55d817856
+3 8291e8976e0ee0363f98f819712552924e1dd1d8ab77c4dc8577765ee3eb2d36
+4 ce55bb850cefaf87c5a16ab7c410f942e11463d0000eb71e8a22e6ce76301b5c
+5 21076aa0c88baba170e62196b5735316f6cc1c5bfe672c0c1e5f9b85d8aaf8cb
+
+
+
+ + + + + +
1First keypair stored in the keystore with the public key starting with 6e89035fce6…​ or at index position #1
+
+
+

See Managing your Keys - The Keystore for more informaton.

+
+
+

To start up the local Convex network with the first 4 public keys for the first 4 peers you can run the following command:

+
+
+
terminal command
+
+
./convex local start --public-key=6e89035 --public-key=13e691e --public-key=8291e89 --public-key=ce55bb8 --password=my-password
+
+
+
+

or you can combine the public key fields together into a single comma seperated list option such as:

+
+
+
terminal command
+
+
./convex local start --public-key=6e89035,13e691e,8291e89,ce55bb8 --password=my-password
+
+
+
+

This will now start up a local Convex network with 4 peers each using a public key from the list provided in the keystore.

+
+
+ + + + + +
+ + +
+

To start the same peers using the same public keys you can also use the index number in the keystore. So the line:

+
+
+
+
./convex local start --index-key=1,2,3,4 --password=my-password
+
+
+
+

Will start the same set of peers as above using the first 4 key pairs from the keystore.

+
+
+
+
+
+

Local start with port numbers

+
+

By default, the CLI start a local network with each peer assigned a random port number. You can specify the port numbers used for each peer, by setting the --ports option.

+
+
+

The --ports option takes a list or range of port numbers.

+
+
+

You can use multiple --ports options such as:

+
+
+
+
./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081 --ports=8082 --ports=8083 --ports=8084
+
+
+
+

or you can provide a list of ports to use for each peer:

+
+
+
+
./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081,8082,8083,8084
+
+
+
+

or a range of port numbers:

+
+
+
+
./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081-8084
+
+
+
+

or an open range for any number of peers:

+
+
+
+
./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081-
+
+
+
+

or a combination of the above, where the first peer uses port 8088, and all subsequent peers use ports from 8090:

+
+
+
+
./convex local start --index-key=1,2,3,4 --password=my-password --ports=8088 --ports=8090-
+
+
+
+
+

Local start with config file

+
+

You can create a config file and assign the command options as config items. You can then start your +local network using a config file, instead of providing a list of keys.

+
+
+
terminal command
+
+
./convex local start --config=example_convex_local_start.conf
+
+
+
+

Config Parameters for convex.local.start

+
+
file: example_convex_local_start.conf
+
+
    # etch storage database
+    convex.etch = (1)
+
+    # default keystore filename
+    convex.keystore =$HOME/.convex/keystore.pfx
+
+    # default session filename
+    convex.session = $HOME/.convex/session.conf
+
+    # number of peers to start
+    convex.local.start = 4
+
+    # comma list of index of keys or items (2)
+    convex.local.start.index-key=
+
+    # comma list of public-key hex values, or multiple items
+    convex.local.start.public-key=6e89035
+    convex.local.start.public-key=13e691e
+    convex.local.start.public-key=8291e89
+    convex.local.start.public-key=ce55bb8
+
+    convex.local.start.ports=8090- (3)
+
+    # keystore password
+    convex.local.password = (4)
+
+
+
+ + + + + + + + + + + + + + + + + +
1If no filename is provided, then the CLI will create a temporary etch storage database in the temp folder.
2You can provide a list of public keys or indexes or duplicate settings with different values. +
+
+
convex.local.index-key = 1,2,3
+# is the same as
+convex.local.index-key = 1
+convex.local.index-key = 2
+convex.local.index-key = 3
+
+
3The peers will use port 8090 onwards
4If you do not provide a password, then the CLI will request a password on starting the local network.
+
+
+
+
+
+
+

Starting a local Peer

+
+
+

How to start a local peer, and join a local Convex network.

+
+
+

To start a local peer you first need to do the following:

+
+
+
    +
  1. +

    Start a local Convex network. see Starting a local network.

    +
  2. +
  3. +

    Create a keypair, or select an unused keypair to use for the peer.

    +
  4. +
  5. +

    Create an account for the peer.

    +
  6. +
  7. +

    Assign funds to the peer account.

    +
  8. +
  9. +

    Assign the peer account funds for the peer stake.

    +
  10. +
+
+
+ + + + + +
+ + +
+

This type of blockchain technology uses the Convergent Proof of Stake (CPoS) algorithm, where each peer has a public key and a stake amount. The stake amount decides the peers voting control in the CPoS algorithm. See Convex Technology

+
+
+
+
+

The following command does all of the above except step #1:

+
+
+
+
./convex peer create --password=my-password
+
+
+
+

You will then get back from the peer create command something like this:

+
+
+
+
Public Peer Key: 0xbc1290834e1953b2952624ab8ce34e87d308ba975d655163f9fe47283f0436aa
+Address: 45
+Balance: 199945799
+Inital stake amount: 9800000000
+Peer start line: ./convex peer start --password=my-password --address=45 --public-key=bc1290
+
+
+
+

you can then copy the Peer start line: and run a peer with the local network.

+
+
+
+
./convex peer start --password=my-password --address=45 --public-key=bc1290
+
+
+
+
+
+

Starting a local Peer to a remote Convex network

+
+
+

How to start a local peer, that connects to a remote Convex network.

+
+
+

If you wish to connect to your own remote peer, you can by adding the --peer option. This tells the new peer you are starting where a remote peer is located. Once found the started peer will try and sync with the remote peer.

+
+
+

To connect to someone else’s remote network or to connect to the test network at convex you will need to obtain a peer keypair, account with sufficient funds and the peer registered with a stake amount.

+
+
+
+
+

Starting the GUI local network

+
+
+

How to start the gui local network.

+
+
+

To start the local GUI network, you can call the command:

+
+
+
+
./convex local gui
+
+
+
+

This starts a local network in GUI mode. At the moment the GUI local network does not publish the keypairs used for the network, so the CLI cannot do the following when the GUI network is running:

+
+
+
    +
  1. +

    Account Fund Request

    +
  2. +
  3. +

    Account Create

    +
  4. +
  5. +

    Peer Create

    +
  6. +
+
+
+
+
+

Peer Output

+
+
+

Describes the output fields

+
+
+
Sample output
+
+
Starting network Id: 0xefe75ea61ad52b38f4455a88911b7bd851dc080090e1b1cb4ec75d85a44eb92d
+#2: Peer:1770c3 URL: localhost:43849 Status: J NS Connections: 1/ 0 Consensus:   0 State:efe75e Belief:46bbe3 Msg: connection
+#1: Peer:fa26c5 URL: localhost:41635 Status: J NS Connections: 1/ 0 Consensus:   0 State:efe75e Belief:7c7542 Msg: connection
+#3: Peer:556deb URL: localhost:37985 Status: J NS Connections: 1/ 0 Consensus:   0 State:efe75e Belief:a43082 Msg: connection
+#4: Peer:0fce50 URL: localhost:46559 Status: J NS Connections: 1/ 0 Consensus:   0 State:efe75e Belief:a98ea8 Msg: connection
+
+
+
+

then later

+
+
+
Sample output
+
+
#2: Peer:1770c3 URL: localhost:43849 Status: J  S Connections: 3/ 3 Consensus:  20 State:cfa8fe Belief:2c6f2a Msg: trusted connection
+#4: Peer:0fce50 URL: localhost:46559 Status: J  S Connections: 3/ 2 Consensus:  20 State:cfa8fe Belief:2c6f2a Msg: connection
+#3: Peer:556deb URL: localhost:37985 Status: J  S Connections: 3/ 3 Consensus:  20 State:cfa8fe Belief:2c6f2a Msg: trusted connection
+#4: Peer:0fce50 URL: localhost:46559 Status: J  S Connections: 3/ 3 Consensus:  20 State:cfa8fe Belief:2c6f2a Msg: trusted connection
+
+
+
+

On every event that occurs for a peer in the cluster, on its own an event is shown as a line.

+
+
+

The event data can be split up into the following fields:

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescriptionExample

Index

+

Peer index starting at 1 within the cluster of peers

+

#4

Peer

+

First 6 characters of the public key of the peer

+

Peer:0fce50

URL

+

URL of the peer

+

URL: localhost:46559

Status

+ + + + + + + + + + + + + + + + + +
+NJ + +

Not Joined

+
+J + +

Joined

+
+NS + +

Not Synced

+
+S + +

Synced

+
+

Status: J S

Connections

+

Peer connection count / Peer trusted connection count

+

Connections: 3/ 2

Consensus

+

Consensus level

+

Consensus: 20

State

+

First 6 characters of the State hash

+

State:cfa8fe

Belief

+

First 6 characters of the Belief hash

+

Belief:2c6f2a

Msg

+

Short message of the event that occured on this peer

+

Msg: trusted connection

+
+
+
+

Managing your Keys - The Keystore

+
+
+

How to manage the local public/private key pairs.

+
+
+

When using any of the key sub commands, you do not need to be connected to any network.

+
+
+

The option --keystore can be used with any sub command to specify which keystore to use.

+
+
+

Generating keypairs

+
+

How to generate a new set of public/private keys.

+
+
+

You need to generate keypairs when:

+
+
+
    +
  1. +

    Creating an account

    +
  2. +
  3. +

    Creating a new peer

    +
  4. +
+
+
+

This command allows you to create 1+ keypairs in the keystore.

+
+
+

So for example this will create 10 keypairs:

+
+
+
+
./convex key generate 10 --password=my-password
+
+
+
+
+

List keys

+
+

How to list the keys store in the keystore.

+
+
+

To list out your keystore and view the public keys of each keypair.

+
+
+
+
./convex key list --password=my-password
+
+
+
+
+

Exporting keys

+
+

How to export the keys from your keystore to encrypted text.

+
+
+

You can export a keypair from the keystore to an encrypted PEM formated text. This is useful if you need +to give another user, or application access to your network.

+
+
+

You need to provide an --export-password option with the password of the encrypted PEM formated text.

+
+
+

You also need to provide the location of the keypair you wish to export, this can be done using the --index-key or --public-key option.

+
+
+

In this example first list out the keys from the keystore.

+
+
+
+
./convex key list --password=my-password
+
+
+
+
+
1 e7fdcb0bfdfb786b51eedf33b575....
+2 373d2a583695ff367dd986e12785....
+..
+
+
+
+

If we now want to export the key #2, then we can use the following command:

+
+
+
+
./convex key export --index-key=2 --export-password=my-password --password=my-password
+
+
+
+

or a more more reliable way is to use the first hex of the public key

+
+
+
+
./convex key export --public-key=373d2a583695ff --export-password=my-password --password=my-password
+
+
+
+ + + + + +
+ + +
+

In this example, we have used an insecure password of my-password to encrypt the exported key. We suggest that you use a better password when exporting your keys, and keep the exported PEM formated text secure.

+
+
+
+
+
+

Importing keys

+
+

How to import keys into the keystore.

+
+
+

You would need to import keys, when you want to run a peer or send a transaction for an account on another network.

+
+
+

To import a keypair you need to set the options --import-file or --import-file and --import-password.

+
+
+

So for example:

+
+
+
+
./convex key import --import-file=my_key.pem --import-password=my-password --password=my-password
+
+
+
+

If the import password is successful, this will import the keypair into the keystore, and show the public key of the imported keypair.

+
+
+
+
+
+

Managing Accounts

+
+
+

Information on how to create, fund and get information about the local accounts.

+
+
+

This set of sub commands manage accounts on the local network. You need to have a local network running on the same computer for these commands to work.

+
+
+

The reason is that the keystore needs to contain the keys for the first genesis accounts in the network. With access to the genesis keypair, the account commands can create an account, and transfer sufficient funds to the new account.

+
+
+

Create an account

+
+

How to create a local account.

+
+
+

To create a new account and new keypair, you can just run:

+
+
+
+
./convex account create --password=my-password
+
+
+
+

If you wish to use an already defined keypair in your keystore, you can set the --index-key or --public-key options.

+
+
+
+
./convex account create --public-key=eb1234 --password=my-password
+
+
+
+

The command returns the account address and public key used to create the account.

+
+
+
+

Get an accounts balance

+
+

How to get an account’s balance.

+
+
+

To obtain the balance of an account, you just need to provide the address of the account.

+
+
+

So to run:

+
+
+
+
./convex account balance 45
+
+
+
+

Returns the balance for account #45

+
+
+
+

Request funds for an account

+
+

How to request funds for an account.

+
+
+
+

Get information about an account

+
+

How to get information about an account.

+
+
+
+
+
+

Status

+
+
+

How to get the local network status.

+
+
+
+
+

Queries

+
+
+

How to execute queries on a local Convex network.

+
+
+
+
+

Transactions

+
+
+

How to execute transactions on a local Convex network.

+
+
+
+
+ + + \ No newline at end of file From 2a857f7ffaad28aa27a94cee0b59b79ee2c5df68 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 6 Sep 2021 09:38:16 +0000 Subject: [PATCH 0005/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d0cf2c28e978b47b982f5509814e7c0386601d5c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d15ff66f1..d59662e9b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2064,7 +2064,7 @@

Transactions

From 3eecf5c2bff672eaef001725d16e65ce6a41658b Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Sep 2021 00:05:52 +0000 Subject: [PATCH 0006/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bdc53d9ab62ed6ada0c83ea0659ccd004700123d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 175 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 155 insertions(+), 20 deletions(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d59662e9b..bf09c712f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -895,7 +895,7 @@

Files needed by CLI ru
  1. -

    Etch Storage database file. This contains the stored state of the Convex network. Usually, when starting up the initial cluster the first set of peers share the same Etch database. CLI Parameter: --etch

    +

    Etch Storage database file. This contains the stored state of the Convex network. Usually when starting up the initial cluster the first set of peers share the same Etch database. CLI Parameter: --etch

  2. Keystore database file. This file contains the private/public key pairs used for the peers and any subsequent users. CLI Parameters: --keystore, --password

    @@ -913,7 +913,7 @@

    Files needed by CLI ru
    -

    The GUI version and the CLI run the same local network. The only difference is that the GUI does not create a session file. This means that some of the CLI features cannot be used with the GUI local network.

    +

    The GUI version and the CLI run the same local network. The only difference is that the GUI stores the gensis keypairs in memory and does not create a session file. This means that some of the CLI features cannot be used with the GUI local network, such as transfering funds, creating accounts.

    @@ -1325,14 +1325,14 @@

    Simple local start

    -

    In this document, the password option will always be shown as --password=my-password. This is an example of a not very good password to use for storing your keys. We suggest that you use a more secure password instead of my-password.

    +

    In this document the password option will always be shown as --password=my-password. This is an example of a not very good password to use for storing your keys. We suggest that you use a more secure password instead of my-password.

-

You will always need to pass the password to the keystore file since the CLI will need access to the keys to create and start up the local peers.

+

You wil always need to pass the password to the keystore file since the CLI will need access the keys to create and start up the local peers.

The CLI will automatically create 4 keypairs and place them in the keystore. The CLI will then start up 4 peers all sharing a single @@ -1350,7 +1350,7 @@

Simple local start

Store the new keypairs in the keystore.

  • -

    Start up the local network using the newly created keys.

    +

    Start up the local network using the new created keys.

  • @@ -1358,7 +1358,7 @@

    Simple local start

    Local start with peer keys

    -

    While the simple local network start will auto-generate public keys for the local peers and create the peer accounts. You have the option instead to start the local network using a predetermined set of keys from your keystore. To do this you need to provide a list of public keys that you want the CLI to use to start up the local network.

    +

    While the simple local network start will auto generate public keys for the local peers and create the peer accounts. You have the option instead to start the local network using a predetermined set of keys from your keystore. To do this you need to provide a list of public keys that you want the CLI to use to start up the local network.

    If you have already used the simple local start, you can get the list of keys created by running the List keys, @@ -1435,7 +1435,7 @@

    Local start with peer keys

    Local start with port numbers

    -

    By default, the CLI start a local network with each peer assigned a random port number. You can specify the port numbers used for each peer, by setting the --ports option.

    +

    By default the CLI start a local network with each peer assigned a random port number. You can specify the port numbers used for each peer, by setting the --ports option.

    The --ports option takes a list or range of port numbers.

    @@ -1594,7 +1594,7 @@

    Starting a local Peer

    -

    This type of blockchain technology uses the Convergent Proof of Stake (CPoS) algorithm, where each peer has a public key and a stake amount. The stake amount decides the peers voting control in the CPoS algorithm. See Convex Technology

    +

    This type of block chain technology uses Convergent Proof of Stake (CPoS) algorithm, where each peer has a public key and a stake amount. The stake amount decides the peers voting control in the CPoS algorithm. See Convex Technology

    @@ -1637,10 +1637,10 @@

    Starting a local Peer to a remote Convex netw

    How to start a local peer, that connects to a remote Convex network.

    -

    If you wish to connect to your own remote peer, you can by adding the --peer option. This tells the new peer you are starting where a remote peer is located. Once found the started peer will try and sync with the remote peer.

    +

    If you whish to connect to your own remote peer, you can by adding the --peer option. This tells the new peer you are starting where a remote peer is located. Once found the started peer will try and sync with the remote peer.

    -

    To connect to someone else’s remote network or to connect to the test network at convex you will need to obtain a peer keypair, account with sufficient funds and the peer registered with a stake amount.

    +

    To connect to someone elses remote network or to connect to the test network at convex you will need to obtain a peer keypair, account with sufficient funds and the peer registered with a stake amount.

    @@ -1659,7 +1659,7 @@

    Starting the GUI local network

    -

    This starts a local network in GUI mode. At the moment the GUI local network does not publish the keypairs used for the network, so the CLI cannot do the following when the GUI network is running:

    +

    This starts the a local network in GUI mode. At the moment the GUI local network does not publish the keypairs used for the network, so the CLI cannot do the following when the GUI network is running:

      @@ -1705,7 +1705,7 @@

      Peer Output

    -

    On every event that occurs for a peer in the cluster, on its own an event is shown as a line.

    +

    On every event that occurs for a peer in the cluster, on it’s own an event is shown as a line.

    The event data can be split up into the following fields:

    @@ -1886,7 +1886,7 @@

    Exporting keys

    How to export the keys from your keystore to encrypted text.

    -

    You can export a keypair from the keystore to an encrypted PEM formated text. This is useful if you need +

    You can export a keypair from the keystore to an encrypted PEM formated text. This is usefull if you need to give another user, or application access to your network.

    @@ -1919,11 +1919,11 @@

    Exporting keys

    -

    or a more more reliable way is to use the first hex of the public key

    +

    or a more safer way is to use the first hex of the public key

    -
    ./convex key export --public-key=373d2a583695ff --export-password=my-password --password=my-password
    +
    ./convex key export --publi-key=373d2a583695ff --export-password=my-password --password=my-password
    @@ -1934,7 +1934,7 @@

    Exporting keys

    -

    In this example, we have used an insecure password of my-password to encrypt the exported key. We suggest that you use a better password when exporting your keys, and keep the exported PEM formated text secure.

    +

    In this example we have used a insecure password of my-password to encrypt the exported key. We suggest that you use a better password when exporting your keys, and keep the exported PEM formated text secure.

    @@ -1961,7 +1961,7 @@

    Importing keys

    -

    If the import password is successful, this will import the keypair into the keystore, and show the public key of the imported keypair.

    +

    If the import password is successfull, this will import the keypair into the keystore, and show the public key of the imported keypair.

    @@ -1976,7 +1976,7 @@

    Managing Accounts

    This set of sub commands manage accounts on the local network. You need to have a local network running on the same computer for these commands to work.

    -

    The reason is that the keystore needs to contain the keys for the first genesis accounts in the network. With access to the genesis keypair, the account commands can create an account, and transfer sufficient funds to the new account.

    +

    The reason is that the keystore needs to contain the keys for the first genesis accounts in the network. With the access to the genesis keypair, the account commands can create an account, and transfer sufficient funds to the new account.

    Create an account

    @@ -2009,7 +2009,7 @@

    Get an accounts balance

    How to get an account’s balance.

    -

    To obtain the balance of an account, you just need to provide the address of the account.

    +

    To obtain the balance of an account, you just need to provide an address of the account.

    So to run:

    @@ -2028,12 +2028,53 @@

    Request funds for an account

    How to request funds for an account.

    +
    +

    With this command you can request funds for an account. This command transfers the funds from the first peer account to the named account. If the network has been started by the GUI or another user, you will not be able to run this command since the transfer of funds will need to know the private key of the first peer.

    +
    +
    +

    For example, first create an account:

    +
    +
    +
    +
    ./convex account create --password=my-password
    +
    +
    +
    +
    +
    Public Key: 1a47522ec91db1209839cf96c99948e77c489310799b29e6bf02038bc67a111c
    +Address: 46
    +Account usage: to use this key can use the options --address=46 --public-key=1a4752
    +
    +
    +
    +

    You can then request funds for this account by providing the --address and --public-key

    +
    +
    +
    +
    ./convex account fund --address=46 --public-key=1a4752 --password=my-password
    +Balance: 100000000
    +
    +

    Get information about an account

    How to get information about an account.

    +
    +

    This command returns the information about the account.

    +
    +
    +
    +
    ./convex account info 46
    +Result: {:sequence 0,:balance 100000000,:allowance 0,:environment nil,:metadata nil,:holdings {},:controller nil,:key 0x1a47522ec91db1209839cf96c99948e77c489310799b29e6bf02038bc67a111c}
    +
    +
    +
    +
    +
    Data type: Record
    +
    +
    @@ -2043,6 +2084,61 @@

    Status

    How to get the local network status.

    +
    +

    This command gets the current status from the local network.

    +
    +
    +

    For example:

    +
    +
    +
    +
    ./convex  status --password=my-password
    +State hash: 0x68b40285d8b3d5831d829f45acaa5066a793ac1aaa3fb603a07ef8c00512f414 (1)
    +Timestamp: 1580602820020 (2)
    +Timestamp value: 2020-02-02T00:20:20.020Z (3)
    +Global Fees: 0 (4)
    +Juice Price: 2 (5)
    +Total Funds: 1,000,000,000,000,000,000 (6)
    +Number of accounts: 46 (7)
    +Number of peers: 4 (8)
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    1The current hash state for the local peer.
    2The peer’s timestamp as a long number.
    3The Peer’s timestamp
    4The amount of global fees.
    5The current juice price for each transaction.
    6Total funds available in this network.
    7Total number of accounts in this network.
    8Number of peers connected to this network.
    +
    @@ -2051,6 +2147,19 @@

    Queries

    How to execute queries on a local Convex network.

    +
    +

    When you run a query, you usually need to provide an account address --address, but this command uses a default account address.

    +
    +
    +

    So to find out the current balance using the balance command you can run the following:

    +
    +
    +
    +
    ./convex query '*balance*'
    +Result: 260397600000000000
    +Data type: Long
    +
    +
    @@ -2059,12 +2168,38 @@

    Transactions

    How to execute transactions on a local Convex network.

    +
    +

    Submiting a ransaction on the Convex network can change the network state, and so this will incure a small fee that is passed back to the peers.

    +
    +
    +

    So when sending a transaction you will need to provide a --public-key or --index-key field for the keypair stored in the keystore, and also the correct --address.

    +
    +
    +

    For example:

    +
    +
    +
    +
    ./convex transaction --address=46 --public-key=1cf32e --password=secret "(map inc [1 2 3 4 5])"
    +
    +
    +
    + + + + + +
    + + +The CLI uses the internal Peer API for queries and transactions. To use other Convex networks or remote Convex networks we suggest you use the public Convex Client API interface instead. +
    +
    From e44c5e1bb9da0c31921f87a5cf9073e3330f0a5d Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Sep 2021 03:02:20 +0000 Subject: [PATCH 0007/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@723382e56ef8aa17886226ea540f55e59d10d8bd?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bf09c712f..79fd478dd 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2199,7 +2199,7 @@

    Transactions

    From 54d4fad6e090daf605e7c3b810254cddddb484f0 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Sep 2021 03:07:24 +0000 Subject: [PATCH 0008/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9e303e4bd8fb95afd324c4b080aaa026ab43295b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 79fd478dd..f52f27302 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -895,7 +895,7 @@

    Files needed by CLI ru
    1. -

      Etch Storage database file. This contains the stored state of the Convex network. Usually when starting up the initial cluster the first set of peers share the same Etch database. CLI Parameter: --etch

      +

      Etch Storage database file. This contains the stored state of the Convex network. Usually, when starting up the initial cluster the first set of peers share the same Etch database. CLI Parameter: --etch

    2. Keystore database file. This file contains the private/public key pairs used for the peers and any subsequent users. CLI Parameters: --keystore, --password

      @@ -1325,14 +1325,14 @@

      Simple local start

      -

      In this document the password option will always be shown as --password=my-password. This is an example of a not very good password to use for storing your keys. We suggest that you use a more secure password instead of my-password.

      +

      In this document, the password option will always be shown as --password=my-password. This is an example of a not very good password to use for storing your keys. We suggest that you use a more secure password instead of my-password.

    -

    You wil always need to pass the password to the keystore file since the CLI will need access the keys to create and start up the local peers.

    +

    You will always need to pass the password to the keystore file since the CLI will need access to the keys to create and start up the local peers.

    The CLI will automatically create 4 keypairs and place them in the keystore. The CLI will then start up 4 peers all sharing a single @@ -1350,7 +1350,7 @@

    Simple local start

    Store the new keypairs in the keystore.

  • -

    Start up the local network using the new created keys.

    +

    Start up the local network using the newly created keys.

  • @@ -1358,7 +1358,7 @@

    Simple local start

    Local start with peer keys

    -

    While the simple local network start will auto generate public keys for the local peers and create the peer accounts. You have the option instead to start the local network using a predetermined set of keys from your keystore. To do this you need to provide a list of public keys that you want the CLI to use to start up the local network.

    +

    While the simple local network start will auto-generate public keys for the local peers and create the peer accounts. You have the option instead to start the local network using a predetermined set of keys from your keystore. To do this you need to provide a list of public keys that you want the CLI to use to start up the local network.

    If you have already used the simple local start, you can get the list of keys created by running the List keys, @@ -1435,7 +1435,7 @@

    Local start with peer keys

    Local start with port numbers

    -

    By default the CLI start a local network with each peer assigned a random port number. You can specify the port numbers used for each peer, by setting the --ports option.

    +

    By default, the CLI start a local network with each peer assigned a random port number. You can specify the port numbers used for each peer, by setting the --ports option.

    The --ports option takes a list or range of port numbers.

    @@ -1594,7 +1594,7 @@

    Starting a local Peer

    -

    This type of block chain technology uses Convergent Proof of Stake (CPoS) algorithm, where each peer has a public key and a stake amount. The stake amount decides the peers voting control in the CPoS algorithm. See Convex Technology

    +

    This type of blockchain technology uses the Convergent Proof of Stake (CPoS) algorithm, where each peer has a public key and a stake amount. The stake amount decides the peers voting control in the CPoS algorithm. See Convex Technology

    @@ -1637,10 +1637,10 @@

    Starting a local Peer to a remote Convex netw

    How to start a local peer, that connects to a remote Convex network.

    -

    If you whish to connect to your own remote peer, you can by adding the --peer option. This tells the new peer you are starting where a remote peer is located. Once found the started peer will try and sync with the remote peer.

    +

    If you wish to connect to your own remote peer, you can by adding the --peer option. This tells the new peer you are starting where a remote peer is located. Once found the started peer will try and sync with the remote peer.

    -

    To connect to someone elses remote network or to connect to the test network at convex you will need to obtain a peer keypair, account with sufficient funds and the peer registered with a stake amount.

    +

    To connect to someone else’s remote network or to connect to the test network at convex you will need to obtain a peer keypair, account with sufficient funds and the peer registered with a stake amount.

    @@ -1659,7 +1659,7 @@

    Starting the GUI local network

    -

    This starts the a local network in GUI mode. At the moment the GUI local network does not publish the keypairs used for the network, so the CLI cannot do the following when the GUI network is running:

    +

    This starts a local network in GUI mode. At the moment the GUI local network does not publish the keypairs used for the network, so the CLI cannot do the following when the GUI network is running:

      @@ -1705,7 +1705,7 @@

      Peer Output

    -

    On every event that occurs for a peer in the cluster, on it’s own an event is shown as a line.

    +

    On every event that occurs for a peer in the cluster, on its own an event is shown as a line.

    The event data can be split up into the following fields:

    @@ -1886,7 +1886,7 @@

    Exporting keys

    How to export the keys from your keystore to encrypted text.

    -

    You can export a keypair from the keystore to an encrypted PEM formated text. This is usefull if you need +

    You can export a keypair from the keystore to an encrypted PEM formated text. This is useful if you need to give another user, or application access to your network.

    @@ -1919,11 +1919,11 @@

    Exporting keys

    -

    or a more safer way is to use the first hex of the public key

    +

    or a more more reliable way is to use the first hex of the public key

    -
    ./convex key export --publi-key=373d2a583695ff --export-password=my-password --password=my-password
    +
    ./convex key export --public-key=373d2a583695ff --export-password=my-password --password=my-password
    @@ -1934,7 +1934,7 @@

    Exporting keys

    -

    In this example we have used a insecure password of my-password to encrypt the exported key. We suggest that you use a better password when exporting your keys, and keep the exported PEM formated text secure.

    +

    In this example, we have used an insecure password of my-password to encrypt the exported key. We suggest that you use a better password when exporting your keys, and keep the exported PEM formated text secure.

    @@ -1961,7 +1961,7 @@

    Importing keys

    -

    If the import password is successfull, this will import the keypair into the keystore, and show the public key of the imported keypair.

    +

    If the import password is successful, this will import the keypair into the keystore, and show the public key of the imported keypair.

    @@ -1976,7 +1976,7 @@

    Managing Accounts

    This set of sub commands manage accounts on the local network. You need to have a local network running on the same computer for these commands to work.

    -

    The reason is that the keystore needs to contain the keys for the first genesis accounts in the network. With the access to the genesis keypair, the account commands can create an account, and transfer sufficient funds to the new account.

    +

    The reason is that the keystore needs to contain the keys for the first genesis accounts in the network. With access to the genesis keypair, the account commands can create an account, and transfer sufficient funds to the new account.

    Create an account

    @@ -2009,7 +2009,7 @@

    Get an accounts balance

    How to get an account’s balance.

    -

    To obtain the balance of an account, you just need to provide an address of the account.

    +

    To obtain the balance of an account, you just need to provide the address of the account.

    So to run:

    @@ -2199,7 +2199,7 @@

    Transactions

    From b393207ab475e4449465399487a499e9dba844a5 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Sep 2021 03:09:15 +0000 Subject: [PATCH 0009/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b2ce2750f2492ff0b8c62c4f2087d6639a07553c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f52f27302..8f3ae4466 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2199,7 +2199,7 @@

    Transactions

    From 025b1bf344c7dd0bada92255294ca4f0ccc373e8 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Sep 2021 05:15:25 +0000 Subject: [PATCH 0010/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@39d0b22b774d5cc2808d69a72199630961f40698?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8f3ae4466..38ac205b7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -1634,13 +1634,16 @@

    Starting a local Peer

    Starting a local Peer to a remote Convex network

    -

    How to start a local peer, that connects to a remote Convex network.

    +

    Currently the convex-cli does not support access to convex.world via the client HTTP API, so to setup a peer to connect to convex.world you will need to create a new peer in convex.world using convex-api-py tools/convex_tools.py and then import the peer key into the convex-cli keystore.

    -

    If you wish to connect to your own remote peer, you can by adding the --peer option. This tells the new peer you are starting where a remote peer is located. Once found the started peer will try and sync with the remote peer.

    +

    Once the key is imported you can start a peer to connect with convex.world

    -

    To connect to someone else’s remote network or to connect to the test network at convex you will need to obtain a peer keypair, account with sufficient funds and the peer registered with a stake amount.

    +

    If you wish to connect to your own remote peer, you can by adding the --peer=<remote peer URL> option. This tells the new peer you are starting where a remote peer is located. Once found the started peer will try and sync with the remote peer.

    +
    +
    +

    You will also need to set the --url=<my peer URL> option to specify the remote URL of your running peer.

    @@ -2199,7 +2202,7 @@

    Transactions

    From c7c546aea2aaca099ad69bc9c39ff72baa5c0099 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Sep 2021 05:21:30 +0000 Subject: [PATCH 0011/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@50966e770690cfc12418442ae886d0a2ce2b9e15?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 38ac205b7..19fc8817c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2202,7 +2202,7 @@

    Transactions

    From db21b7225ba91c0354dbb6ef7e225aab79c6551d Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Sep 2021 09:26:04 +0000 Subject: [PATCH 0012/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9b3cca2194b947aeff8422c008b9ebc6ddc97959?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 19fc8817c..580f67640 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -771,7 +771,7 @@

    Overview

    Setup accounts within a local network.

  • -

    Step key pairs to use for accessing local and remote networks.

    +

    Setup key pairs to use for accessing local and remote networks.

  • Run a Local Peer connected to a remote Convex network.

    @@ -785,7 +785,7 @@

    Overview

    Getting Started

    -

    As yet we have not packaged this in any package manager for downloading so currently the CLI can only be downloaded via github.

    +

    As yet we have not packaged this in any package manager for downloading so currently the CLI can only be downloaded via Github.

    Get the source

    @@ -793,7 +793,7 @@

    Get the source

    You will need to visit convex-dev @ github and clone the repository onto your local computer.

    -

    The develop branch is currently the latest version.

    +

    The "develop" branch is currently the latest version.

    terminal commands
    @@ -808,7 +808,7 @@

    Get the source

    Convex Projects

    -

    The Convex code repository is made up of the following sub projects:

    +

    The Convex code repository is made up of the following subprojects:

    @@ -901,7 +901,7 @@

    Files needed by CLI ru

    Keystore database file. This file contains the private/public key pairs used for the peers and any subsequent users. CLI Parameters: --keystore, --password

  • -

    Session file. This is created by the CLI to keep track of the locally running peers, so that if you want to access the local network or add another peer to the local network, the CLI will look at the session file for a randomly available peer to connect too. CLI Parameter: --session

    +

    Session file. This is created by the CLI to keep track of the locally running peers, so that if you want to access the local network or add another peer to the local network, the CLI will look at the session file for a randomly available peer to connect to. CLI Parameter: --session

  • @@ -913,7 +913,7 @@

    Files needed by CLI ru
    -

    The GUI version and the CLI run the same local network. The only difference is that the GUI stores the gensis keypairs in memory and does not create a session file. This means that some of the CLI features cannot be used with the GUI local network, such as transfering funds, creating accounts.

    +

    The GUI version and the CLI run the same local network. The only difference is that the GUI stores the genesis keypairs in memory and does not create a session file. This means that some of the CLI features cannot be used with the GUI local network, such as transferring funds, creating accounts.

    @@ -1273,7 +1273,7 @@

    Shared Options

    Requesting Help

    -

    The CLI supports help using the -h or --help options or the command help. For each sub command there are more help options.

    +

    The CLI supports help using the -h or --help options or the command help. For each sub command, there are more help options.

    So for example

    @@ -1634,7 +1634,7 @@

    Starting a local Peer

    Starting a local Peer to a remote Convex network

    -

    Currently the convex-cli does not support access to convex.world via the client HTTP API, so to setup a peer to connect to convex.world you will need to create a new peer in convex.world using convex-api-py tools/convex_tools.py and then import the peer key into the convex-cli keystore.

    +

    Currently, the convex-cli does not support access to convex.world via the client HTTP API, so to setup a peer to connect to convex.world you will need to create a new peer in convex.world using convex-api-py tools/convex_tools.py and then import the peer key into the convex-cli keystore.

    Once the key is imported you can start a peer to connect with convex.world

    @@ -1890,7 +1890,7 @@

    Exporting keys

    You can export a keypair from the keystore to an encrypted PEM formated text. This is useful if you need -to give another user, or application access to your network.

    +to give another user or application access to your network.

    You need to provide an --export-password option with the password of the encrypted PEM formated text.

    @@ -1922,7 +1922,7 @@

    Exporting keys

    -

    or a more more reliable way is to use the first hex of the public key

    +

    or a more reliable way is to use the first hex of the public key

    @@ -2032,7 +2032,7 @@

    Request funds for an account

    How to request funds for an account.

    -

    With this command you can request funds for an account. This command transfers the funds from the first peer account to the named account. If the network has been started by the GUI or another user, you will not be able to run this command since the transfer of funds will need to know the private key of the first peer.

    +

    With this command, you can request funds for an account. This command transfers the funds from the first peer account to the named account. If the network has been started by the GUI or another user, you will not be able to run this command since the transfer of funds will need to know the private key of the first peer.

    For example, first create an account:

    @@ -2172,7 +2172,7 @@

    Transactions

    How to execute transactions on a local Convex network.

    -

    Submiting a ransaction on the Convex network can change the network state, and so this will incure a small fee that is passed back to the peers.

    +

    Submitting a transaction on the Convex network can change the network state, and so this will incur a small fee that is passed back to the peers.

    So when sending a transaction you will need to provide a --public-key or --index-key field for the keypair stored in the keystore, and also the correct --address.

    @@ -2202,7 +2202,7 @@

    Transactions

    From 74f7d4fc92e715b0f7b1f69867e4910b59db0402 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Sep 2021 10:27:06 +0000 Subject: [PATCH 0013/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e3424d972eb2d2d4c859fdcf9995507886cf0f3b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 195 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 189 insertions(+), 6 deletions(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 580f67640..ed74efa81 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -709,7 +709,12 @@

    Convex Command Line Interface

  • Starting a local Peer
  • -
  • Starting a local Peer to a remote Convex network
  • +
  • Setting up a local Peer with Convex.world + +
  • Starting the GUI local network
  • Peer Output
  • Managing your Keys - The Keystore @@ -1631,19 +1636,197 @@

    Starting a local Peer

  • -

    Starting a local Peer to a remote Convex network

    +

    Setting up a local Peer with Convex.world

    -

    Currently, the convex-cli does not support access to convex.world via the client HTTP API, so to setup a peer to connect to convex.world you will need to create a new peer in convex.world using convex-api-py tools/convex_tools.py and then import the peer key into the convex-cli keystore.

    +

    To start a local peer connected to the convex.world network you will need to first setup the peer

    +
    +
    +

    Peer Setup in convex.world

    +
    +
      +
    • +

      Access the convex.world/sandbox

      +
    • +
    • +

      Create a new account, you will need the account number later to start the peer.

      +
    • +
    • +

      Then go to the 'Request coins' section and request some more coins for your new account.

      +
    • +
    • +

      Using the convex-cli generate a new keypair for your new peer. See Generating keypairs

      +
      +
      +
      ./convex key generate --password=my-password
      +Index Public Key
      +0 dfb22da0afda1a123e523ded624f184719a4416e9aac6f6fdedd8518fb09fe3c
      +
      +
      +
    • +
    +
    +
    + + + + + +
    + + +
    +

    The public key address, account number and URL shown in this example will be different values for when you run and paste these commands.

    +
    +
    +
    +
    +
      +
    • +

      Using the generated public key and a set stake amount (e.g. 19999 this number must be less than your current balance), +paste the following command into the sandbox command area (remember to use your public key instead):

      +
      +
      +
      (create-peer 0xdfb22da0afda1a123e523ded624f184719a4416e9aac6f6fdedd8518fb09fe3c 199999)
      +
      +
      +
      +
        +
      • +

        The syntax for create-peer is:

        +
        +
        +
        (create-peer <public-key> <stake-amount>)
        +
        +
        +
      • +
      +
      +
    • +
    +
    +
    + + + + + +
    + + +
    +

    You must use your generated public key to set-key command, as this will lock you out of the sandbox, since the sandbox +no longer has the keypair for this account.

    +
    +
    +
    +
    +
      +
    • +

      Setup the new account to use your new peer key, by running the command in the sandbox:

      +
      +
      +
      (set-key 0xdfb22da0afda1a123e523ded624f184719a4416e9aac6f6fdedd8518fb09fe3c)
      +
      +
      +
      +
        +
      • +

        The syntax for set-key is:

        +
        +
        +
        (set-key <public-key>)
        +
        +
        +
      • +
      +
      +
    • +
    +
    +
    + + + + + +
    + + +
    +

    Remember to add the 0x at the front of the key value, when using a public key in the sandbox.

    +
    +
    +
    +
    +
    +

    Starting the peer with convex.world

    +
    +

    If the peer has been setup correctly on convex.world, so you can now start your peer, using the following example command:

    +
    +
    +
    +
    ./convex peer start --public-key=dfbb22 --password=my-password --address=<address> --port=80888 --url=<my-ip>:8088 --peer=convex.world:18888
    +
    +
    +
    +

    Where:

    +
    +
    +
      +
    • +

      <address> is the account number of your peer.

      +
    • +
    • +

      <my-ip> is the public ip address of the running peer.

      +
    • +
    -

    Once the key is imported you can start a peer to connect with convex.world

    +

    or for example you can create a config file with the following options set:

    +
    +
    +
    +
    convex.peer.start.peer=convex.world:18888
    +convex.peer.start.public-key=dfb22da0afda1a123e523ded624f184719a4416e9aac6f6fdedd8518fb09fe3c
    +convex.peer.start.address=47
    +convex.peer.start.url=206.1.1.1:8181
    +convex.peer.start.port=8181
    +convex.password=my-password
    +
    +
    +
    +
      +
    • +

      Run convex only using the config file

      +
      +
      +
      ./convex peer start --config=my-peer.conf
      +
      +
      +
    • +
    +
    +
    + + + + + +
    + + +
    +

    You will need allow the peer port remote access. In our example the peer port is set to 8181, so we have allowed TCP traffic to be forwarded and passed to port 8181.

    +
    +

    If you wish to connect to your own remote peer, you can by adding the --peer=<remote peer URL> option. This tells the new peer you are starting where a remote peer is located. Once found the started peer will try and sync with the remote peer.

    -

    You will also need to set the --url=<my peer URL> option to specify the remote URL of your running peer.

    +

    You will also need to set the --url=<my peer URL> option to specify the remote URL of your running peer, so that other peers can communicate to your peer.

    +
    @@ -2202,7 +2385,7 @@

    Transactions

    From 47b037291f8fbd47179b15e3aa53e355ff75f27d Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 8 Sep 2021 07:29:50 +0000 Subject: [PATCH 0014/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@daa729e71ac59760265ef89fca17a170b9773316?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ed74efa81..1c5d12d44 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2375,7 +2375,7 @@

    Transactions

    -The CLI uses the internal Peer API for queries and transactions. To use other Convex networks or remote Convex networks we suggest you use the public Convex Client API interface instead. +The CLI uses the internal Peer API for queries and transactions. To use other Convex networks or remote Convex networks we suggest you use the public Convex Clients instead. @@ -2385,7 +2385,7 @@

    Transactions

    From 485f170e54400a9194294a8ed9b078ca33a11b92 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 8 Sep 2021 09:33:27 +0000 Subject: [PATCH 0015/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4c486423d2a396df6fe0f81960587b809c134a0c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1c5d12d44..16f372b9d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 47b9ae07a6b260c5c933dd995c9e3c43e42693e8 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 8 Sep 2021 11:13:00 +0000 Subject: [PATCH 0016/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@018f4a3d657067dc81102bfd6c40ebaa4d0a1020?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 16f372b9d..d32cc0109 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 18a2c320fcfeaf1dd7d08f8e11f7528918bccf10 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 8 Sep 2021 11:16:22 +0000 Subject: [PATCH 0017/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6fb9430a80d158f1ce9329597c2549bd5a4985fa?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d32cc0109..447523b39 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 1a8c8fefd8e95046b442b414702b0680c440d338 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 8 Sep 2021 18:45:01 +0000 Subject: [PATCH 0018/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@26cb9d3f8de9241565987b46bb63a1509ddd193d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 447523b39..aff1457f2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 387f7b6d6fe1d29f7ff1c7a58669e0d3a5a7ba40 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 8 Sep 2021 18:54:00 +0000 Subject: [PATCH 0019/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@95d8c76d7752dd8951e68dceeb1fc3dda4f6a193?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index aff1457f2..931354f75 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From d2acf7eb8b1d5c81a297970505a04c1e9a742731 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 8 Sep 2021 18:55:52 +0000 Subject: [PATCH 0020/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@de391accd8acd1012a1896ab633d9b000b333b74?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 931354f75..8ad6a44af 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 905ebd41400dd5c1075369b5cb5551ccb0a8f07c Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 8 Sep 2021 18:59:40 +0000 Subject: [PATCH 0021/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@91d3bec278b950ee0a4fddba1b3e3ec43d76b906?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8ad6a44af..19820c7c8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 4d5fc49932fe664984bd129f8bb6477f182778f4 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 8 Sep 2021 19:04:48 +0000 Subject: [PATCH 0022/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@960fc60f92652663aa4cdae86862d4a9f52b156d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 19820c7c8..8a48daa76 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 12ecb94e8d60ec7efb0d9d902afb77fad9a67e00 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 9 Sep 2021 10:52:28 +0000 Subject: [PATCH 0023/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2b25412e129be057ef98d42515709c7642e89908?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8a48daa76..d8ed4300b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 2332b6acdf932357c4d3d0d21dba652cdaca1952 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 13 Sep 2021 03:01:56 +0000 Subject: [PATCH 0024/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8b34817100797db746d6309a7daa0721242f0cb8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d8ed4300b..60cac4e8e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From a0edea0b4130b77c89714c661248b75168d96fa1 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 13 Sep 2021 07:05:05 +0000 Subject: [PATCH 0025/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@df50b888ab04b69047ef2fbcfa4520f6e3eaf3fc?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 60cac4e8e..19dc8df62 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 0e04aa08118adf91b716ecfd7f68af38dbbe3c85 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 13 Sep 2021 08:05:07 +0000 Subject: [PATCH 0026/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d9ad23ee147940298ae27d4271208e04516060ab?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 19dc8df62..a7e498df6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From ef804ff83a16c89a12ebab21f4a7d56c13733142 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 15 Sep 2021 03:15:22 +0000 Subject: [PATCH 0027/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@54be1c0e2d580e50490fca7d687ba7202711c5bf?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a7e498df6..275fe9f17 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 827cf26cea304a7fd2e763a88270b8cd709b6e13 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 17 Sep 2021 01:24:22 +0000 Subject: [PATCH 0028/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@69cd5050d5527dfa96cc901844f504a46a8db6ee?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 275fe9f17..5cbf15f85 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From cd7c49ed8ca262a28920818cdb883c07b49df7ad Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 20 Sep 2021 08:41:15 +0000 Subject: [PATCH 0029/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f7e7d0735950cbfa777f20ec566f3be0d715e3ad?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5cbf15f85..55dda87f2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 89fd7f0b8561b6984a0b55cef291f56b20166b76 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 21 Sep 2021 01:05:07 +0000 Subject: [PATCH 0030/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d732c7baffac14e579f1730832b31b106ddcb01c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 55dda87f2..6fda7b6ac 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 9c599b009c073fa4c5bc26dcd46d004e896bf7ee Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 21 Sep 2021 01:08:12 +0000 Subject: [PATCH 0031/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ece7009a3f75139318cb0963b3b38284f0c61589?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6fda7b6ac..e0dfaacfe 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 5f2db41f8bd34fb68602326219993c664ce43b45 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 24 Sep 2021 00:38:18 +0000 Subject: [PATCH 0032/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@15848396788683995e847ceeb5f6633382384ab8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e0dfaacfe..67eb7191d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From f4d5cfbedf00f5bdd9cccc046065c2e380d59c71 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 24 Sep 2021 06:13:13 +0000 Subject: [PATCH 0033/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8e40a5c2baf6e64d2de3d24dafe020a6a9c5c02b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 67eb7191d..5102170a5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 44987e6f75bde99a039476b0dab55aa9fe6749e7 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 27 Sep 2021 23:37:40 +0000 Subject: [PATCH 0034/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@126b638ac345de78875a569ff892bfee0314f84c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5102170a5..2b9184d05 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From aff82cacdd3e982d7bd9c450a72eb97a69a25844 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 28 Sep 2021 02:07:57 +0000 Subject: [PATCH 0035/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5faf14f20ec541e47897d377daf5f21627871d17?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2b9184d05..4f18d2517 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From caac2a1bc44dfbf2cc5c7496ef4aedc6a9b565e0 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 28 Sep 2021 09:47:00 +0000 Subject: [PATCH 0036/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f741fe886707993f0d6ec06378e0959a00f4dd7d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4f18d2517..94f498bbd 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 0dca226f0426c85b70b30d751a82b485f16fc9d0 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 28 Sep 2021 09:47:20 +0000 Subject: [PATCH 0037/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@341332bdd734ba824c40d8ffa278b25029c512c2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 737 +++--------------------------------------- 1 file changed, 43 insertions(+), 694 deletions(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 94f498bbd..92b5e5783 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -678,7 +678,7 @@ @@ -753,33 +747,27 @@

    Convex Command Line Interface

    Introduction

    -

    Convex Command Line Interface or CLI, allows you to control and setup a local Convex network, or add a peer to an already existing network. -The current test Convex network can be found at convex.world.

    +

    Convex Command Line Interface or CLI, allows you to control and setup a local Convex network and peers. +More information can be found at convex.world.

    Overview

    -

    This CLI is part of the Convex code base. The CLI has been built to do the following things:

    +

    The CLI is part of the Convex library. The CLI has been built to do the following things:

    1. -

      Run a Local Convex network.

      -
    2. -
    3. -

      Run a Local Peer(s) connected to a local network.

      -
    4. -
    5. -

      Send and Query transactions within a local network.

      +

      Local Convex network.

    6. -

      Setup accounts within a local network.

      +

      Local Peer(s) connected to a local network.

    7. -

      Setup key pairs to use for accessing local and remote networks.

      +

      Send and Query transactions with a local network.

    8. -

      Run a Local Peer connected to a remote Convex network.

      +

      Local Peer connected to a remote Convex network.

    @@ -790,7 +778,7 @@

    Overview

    Getting Started

    -

    As yet we have not packaged this in any package manager for downloading so currently the CLI can only be downloaded via Github.

    +

    Currently the CLI can only be downloaded via github.

    Get the source

    @@ -798,7 +786,7 @@

    Get the source

    You will need to visit convex-dev @ github and clone the repository onto your local computer.

    -

    The "develop" branch is currently the latest version.

    +

    At the moment the develop branch is currently the latest version.

    terminal commands
    @@ -813,7 +801,7 @@

    Get the source

    Convex Projects

    -

    The Convex code repository is made up of the following subprojects:

    +

    The Convex repository is made up of the following sub projects:

    @@ -835,10 +823,13 @@

    Convex Projects

    convex-peer
    -

    Peer library used to run convex peers.

    +

    Peer library used to run convex peer.

    +
    +

    The CLI project is currently part of the main Convex repository.

    +

    Compile and Setup

    @@ -846,7 +837,7 @@

    Compile and Setup

    Once you have downloaded the latest source of Convex, you can now compile the suite of projects.

    -

    To do this you need to execute the Maven command:

    +

    To do this you need to execute Maven install or package

    terminal command
    @@ -894,36 +885,22 @@

    Compile and Setup

    Files needed by CLI run a local Network or Peer

    -

    The CLI needs 3 types of files before running a local Convex network or as a Peer on any network. +

    The CLI needs 3 types of files to read or write before running a local Convex network or as a Peer. The type of files are:

    1. -

      Etch Storage database file. This contains the stored state of the Convex network. Usually, when starting up the initial cluster the first set of peers share the same Etch database. CLI Parameter: --etch

      +

      Etch Storage database file. This contains the stored state of the Convex network. Usually when starting up the initial cluster the first set of peers share the same Etch database. CLI Parameter: --etch

    2. Keystore database file. This file contains the private/public key pairs used for the peers and any subsequent users. CLI Parameters: --keystore, --password

    3. -

      Session file. This is created by the CLI to keep track of the locally running peers, so that if you want to access the local network or add another peer to the local network, the CLI will look at the session file for a randomly available peer to connect to. CLI Parameter: --session

      +

      Session file. This is created by the CLI to keep track of the locally running peers, so that if you want to access the local network or add another peer to the local network, the CLI will look at the session file for a randomly available peer to connect too. CLI Parameter: --session

    -
    - - - - - -
    - - -
    -

    The GUI version and the CLI run the same local network. The only difference is that the GUI stores the genesis keypairs in memory and does not create a session file. This means that some of the CLI features cannot be used with the GUI local network, such as transferring funds, creating accounts.

    -
    -
    -
    @@ -954,7 +931,7 @@

    Running the CLI

    Commands

    -

    The CLI is split into command the following commands and subcommands:

    +

    The CLI is split into commands and some sub commands they are:

    @@ -983,17 +960,17 @@

    Commands

    balance, bal, ba

    -

    Get an account balance.

    +

    Get account balance.

    create, cr

    -

    Creates an account on a local network using a public/private key from the keystore.

    +

    Creates an account using a public/private key from the keystore.

    fund, fu

    -

    Transfers funds to an account using a public/private key from the keystore.

    +

    Transfers funds to account using a public/private key from the keystore.

    @@ -1034,7 +1011,7 @@

    Commands

    generate, ge

    -

    Generate one or more key pairs.

    +

    Generate 1 or more private key pairs.

    @@ -1044,7 +1021,7 @@

    Commands

    export, ex

    -

    Export key pair from the keystore.

    +

    Export 1 or more key pairs from the keystore.

    @@ -1080,7 +1057,7 @@

    Commands

    start, st

    -

    Starts a local convex test network, same as GUI but using a command line.

    +

    Starts a local convex test network.

    @@ -1278,7 +1255,7 @@

    Shared Options

    Requesting Help

    -

    The CLI supports help using the -h or --help options or the command help. For each sub command, there are more help options.

    +

    The CLI supports help using the -h or --help options or the command help. For each sub command there are more help options.

    So for example

    @@ -1322,22 +1299,8 @@

    Simple local start

    ./convex local start --password=my-password
    -
    - - - - - -
    - - -
    -

    In this document, the password option will always be shown as --password=my-password. This is an example of a not very good password to use for storing your keys. We suggest that you use a more secure password instead of my-password.

    -
    -
    -
    -

    You will always need to pass the password to the keystore file since the CLI will need access to the keys to create and start up the local peers.

    +

    You wil always need to pass the password to the keystore file since the CLI will need access to create and start up the local peers.

    The CLI will automatically create 4 keypairs and place them in the keystore. The CLI will then start up 4 peers all sharing a single @@ -1355,7 +1318,7 @@

    Simple local start

    Store the new keypairs in the keystore.

  • -

    Start up the local network using the newly created keys.

    +

    Start up the local network using the new created keys.

  • @@ -1363,7 +1326,7 @@

    Simple local start

    Local start with peer keys

    -

    While the simple local network start will auto-generate public keys for the local peers and create the peer accounts. You have the option instead to start the local network using a predetermined set of keys from your keystore. To do this you need to provide a list of public keys that you want the CLI to use to start up the local network.

    +

    While the simple local start will auto generate public keys for the local peers, create the peer accounts. You have the option to start any number of peers using the private keys from your keystore. To do this you need to provide a list of public keys that you want the CLI to use to start up the local network.

    If you have already used the simple local start, you can get the list of keys created by running the List keys, @@ -1438,55 +1401,6 @@

    Local start with peer keys

    -

    Local start with port numbers

    -
    -

    By default, the CLI start a local network with each peer assigned a random port number. You can specify the port numbers used for each peer, by setting the --ports option.

    -
    -
    -

    The --ports option takes a list or range of port numbers.

    -
    -
    -

    You can use multiple --ports options such as:

    -
    -
    -
    -
    ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081 --ports=8082 --ports=8083 --ports=8084
    -
    -
    -
    -

    or you can provide a list of ports to use for each peer:

    -
    -
    -
    -
    ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081,8082,8083,8084
    -
    -
    -
    -

    or a range of port numbers:

    -
    -
    -
    -
    ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081-8084
    -
    -
    -
    -

    or an open range for any number of peers:

    -
    -
    -
    -
    ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081-
    -
    -
    -
    -

    or a combination of the above, where the first peer uses port 8088, and all subsequent peers use ports from 8090:

    -
    -
    -
    -
    ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8088 --ports=8090-
    -
    -
    -
    -

    Local start with config file

    You can create a config file and assign the command options as config items. You can then start your @@ -1524,10 +1438,8 @@

    Config Parameters for convex. convex.local.start.public-key=8291e89 convex.local.start.public-key=ce55bb8 - convex.local.start.ports=8090- (3) - # keystore password - convex.local.password = (4) + convex.local.password = (3)

    @@ -1551,10 +1463,6 @@

    Config Parameters for convex. 3 -The peers will use port 8090 onwards - - -4 If you do not provide a password, then the CLI will request a password on starting the local network. @@ -1569,264 +1477,13 @@

    Starting a local Peer

    How to start a local peer, and join a local Convex network.

    -
    -

    To start a local peer you first need to do the following:

    -
    -
    -
      -
    1. -

      Start a local Convex network. see Starting a local network.

      -
    2. -
    3. -

      Create a keypair, or select an unused keypair to use for the peer.

      -
    4. -
    5. -

      Create an account for the peer.

      -
    6. -
    7. -

      Assign funds to the peer account.

      -
    8. -
    9. -

      Assign the peer account funds for the peer stake.

      -
    10. -
    -
    -
    - - - - - -
    - - -
    -

    This type of blockchain technology uses the Convergent Proof of Stake (CPoS) algorithm, where each peer has a public key and a stake amount. The stake amount decides the peers voting control in the CPoS algorithm. See Convex Technology

    -
    -
    -
    -
    -

    The following command does all of the above except step #1:

    -
    -
    -
    -
    ./convex peer create --password=my-password
    -
    -
    -
    -

    You will then get back from the peer create command something like this:

    -
    -
    -
    -
    Public Peer Key: 0xbc1290834e1953b2952624ab8ce34e87d308ba975d655163f9fe47283f0436aa
    -Address: 45
    -Balance: 199945799
    -Inital stake amount: 9800000000
    -Peer start line: ./convex peer start --password=my-password --address=45 --public-key=bc1290
    -
    -
    -
    -

    you can then copy the Peer start line: and run a peer with the local network.

    -
    -
    -
    -
    ./convex peer start --password=my-password --address=45 --public-key=bc1290
    -
    -
    -

    Setting up a local Peer with Convex.world

    +

    Starting a local Peer to a remote Convex network

    -

    To start a local peer connected to the convex.world network you will need to first setup the peer

    -
    -
    -

    Peer Setup in convex.world

    -
    -
      -
    • -

      Access the convex.world/sandbox

      -
    • -
    • -

      Create a new account, you will need the account number later to start the peer.

      -
    • -
    • -

      Then go to the 'Request coins' section and request some more coins for your new account.

      -
    • -
    • -

      Using the convex-cli generate a new keypair for your new peer. See Generating keypairs

      -
      -
      -
      ./convex key generate --password=my-password
      -Index Public Key
      -0 dfb22da0afda1a123e523ded624f184719a4416e9aac6f6fdedd8518fb09fe3c
      -
      -
      -
    • -
    -
    -
    - - - - - -
    - - -
    -

    The public key address, account number and URL shown in this example will be different values for when you run and paste these commands.

    -
    -
    -
    -
    -
      -
    • -

      Using the generated public key and a set stake amount (e.g. 19999 this number must be less than your current balance), -paste the following command into the sandbox command area (remember to use your public key instead):

      -
      -
      -
      (create-peer 0xdfb22da0afda1a123e523ded624f184719a4416e9aac6f6fdedd8518fb09fe3c 199999)
      -
      -
      -
      -
        -
      • -

        The syntax for create-peer is:

        -
        -
        -
        (create-peer <public-key> <stake-amount>)
        -
        -
        -
      • -
      -
      -
    • -
    -
    -
    - - - - - -
    - - -
    -

    You must use your generated public key to set-key command, as this will lock you out of the sandbox, since the sandbox -no longer has the keypair for this account.

    -
    -
    -
    -
    -
      -
    • -

      Setup the new account to use your new peer key, by running the command in the sandbox:

      -
      -
      -
      (set-key 0xdfb22da0afda1a123e523ded624f184719a4416e9aac6f6fdedd8518fb09fe3c)
      -
      -
      -
      -
        -
      • -

        The syntax for set-key is:

        -
        -
        -
        (set-key <public-key>)
        -
        -
        -
      • -
      -
      -
    • -
    -
    -
    - - - - - -
    - - -
    -

    Remember to add the 0x at the front of the key value, when using a public key in the sandbox.

    -
    -
    -
    -
    -
    -

    Starting the peer with convex.world

    -
    -

    If the peer has been setup correctly on convex.world, so you can now start your peer, using the following example command:

    -
    -
    -
    -
    ./convex peer start --public-key=dfbb22 --password=my-password --address=<address> --port=80888 --url=<my-ip>:8088 --peer=convex.world:18888
    -
    -
    -
    -

    Where:

    -
    -
    -
      -
    • -

      <address> is the account number of your peer.

      -
    • -
    • -

      <my-ip> is the public ip address of the running peer.

      -
    • -
    -
    -
    -

    or for example you can create a config file with the following options set:

    -
    -
    -
    -
    convex.peer.start.peer=convex.world:18888
    -convex.peer.start.public-key=dfb22da0afda1a123e523ded624f184719a4416e9aac6f6fdedd8518fb09fe3c
    -convex.peer.start.address=47
    -convex.peer.start.url=206.1.1.1:8181
    -convex.peer.start.port=8181
    -convex.password=my-password
    -
    -
    -
    -
      -
    • -

      Run convex only using the config file

      -
      -
      -
      ./convex peer start --config=my-peer.conf
      -
      -
      -
    • -
    -
    -
    - - - - - -
    - - -
    -

    You will need allow the peer port remote access. In our example the peer port is set to 8181, so we have allowed TCP traffic to be forwarded and passed to port 8181.

    -
    -
    -
    -
    -

    If you wish to connect to your own remote peer, you can by adding the --peer=<remote peer URL> option. This tells the new peer you are starting where a remote peer is located. Once found the started peer will try and sync with the remote peer.

    -
    -
    -

    You will also need to set the --url=<my peer URL> option to specify the remote URL of your running peer, so that other peers can communicate to your peer.

    -
    +

    How to start a local peer, that connects to a remote Convex network.

    @@ -1836,30 +1493,6 @@

    Starting the GUI local network

    How to start the gui local network.

    -
    -

    To start the local GUI network, you can call the command:

    -
    -
    -
    -
    ./convex local gui
    -
    -
    -
    -

    This starts a local network in GUI mode. At the moment the GUI local network does not publish the keypairs used for the network, so the CLI cannot do the following when the GUI network is running:

    -
    -
    -
      -
    1. -

      Account Fund Request

      -
    2. -
    3. -

      Account Create

      -
    4. -
    5. -

      Peer Create

      -
    6. -
    -
    @@ -1891,7 +1524,7 @@

    Peer Output

    -

    On every event that occurs for a peer in the cluster, on its own an event is shown as a line.

    +

    On every event that occurs for a peer in the cluster, on it’s own an event is shown as a line.

    The event data can be split up into the following fields:

    @@ -2014,141 +1647,31 @@

    Peer Output

    Managing your Keys - The Keystore

    -

    How to manage the local public/private key pairs.

    -
    -
    -

    When using any of the key sub commands, you do not need to be connected to any network.

    -
    -
    -

    The option --keystore can be used with any sub command to specify which keystore to use.

    +

    To run any peer you need to manage the local public/private key pairs.

    Generating keypairs

    How to generate a new set of public/private keys.

    -
    -

    You need to generate keypairs when:

    -
    -
    -
      -
    1. -

      Creating an account

      -
    2. -
    3. -

      Creating a new peer

      -
    4. -
    -
    -
    -

    This command allows you to create 1+ keypairs in the keystore.

    -
    -
    -

    So for example this will create 10 keypairs:

    -
    -
    -
    -
    ./convex key generate 10 --password=my-password
    -
    -

    List keys

    How to list the keys store in the keystore.

    -
    -

    To list out your keystore and view the public keys of each keypair.

    -
    -
    -
    -
    ./convex key list --password=my-password
    -
    -
    -

    Exporting keys

    +

    Export keys

    How to export the keys from your keystore to encrypted text.

    -
    -

    You can export a keypair from the keystore to an encrypted PEM formated text. This is useful if you need -to give another user or application access to your network.

    -
    -
    -

    You need to provide an --export-password option with the password of the encrypted PEM formated text.

    -
    -
    -

    You also need to provide the location of the keypair you wish to export, this can be done using the --index-key or --public-key option.

    -
    -
    -

    In this example first list out the keys from the keystore.

    -
    -
    -
    -
    ./convex key list --password=my-password
    -
    -
    -
    -
    -
    1 e7fdcb0bfdfb786b51eedf33b575....
    -2 373d2a583695ff367dd986e12785....
    -..
    -
    -
    -
    -

    If we now want to export the key #2, then we can use the following command:

    -
    -
    -
    -
    ./convex key export --index-key=2 --export-password=my-password --password=my-password
    -
    -
    -
    -

    or a more reliable way is to use the first hex of the public key

    -
    -
    -
    -
    ./convex key export --public-key=373d2a583695ff --export-password=my-password --password=my-password
    -
    -
    -
    - - - - - -
    - - -
    -

    In this example, we have used an insecure password of my-password to encrypt the exported key. We suggest that you use a better password when exporting your keys, and keep the exported PEM formated text secure.

    -
    -
    -
    -

    Importing keys

    +

    Import keys

    How to import keys into the keystore.

    -
    -

    You would need to import keys, when you want to run a peer or send a transaction for an account on another network.

    -
    -
    -

    To import a keypair you need to set the options --import-file or --import-file and --import-password.

    -
    -
    -

    So for example:

    -
    -
    -
    -
    ./convex key import --import-file=my_key.pem --import-password=my-password --password=my-password
    -
    -
    -
    -

    If the import password is successful, this will import the keypair into the keystore, and show the public key of the imported keypair.

    -
    @@ -2158,109 +1681,29 @@

    Managing Accounts

    Information on how to create, fund and get information about the local accounts.

    -
    -

    This set of sub commands manage accounts on the local network. You need to have a local network running on the same computer for these commands to work.

    -
    -
    -

    The reason is that the keystore needs to contain the keys for the first genesis accounts in the network. With access to the genesis keypair, the account commands can create an account, and transfer sufficient funds to the new account.

    -

    Create an account

    How to create a local account.

    -
    -

    To create a new account and new keypair, you can just run:

    -
    -
    -
    -
    ./convex account create --password=my-password
    -
    -
    -
    -

    If you wish to use an already defined keypair in your keystore, you can set the --index-key or --public-key options.

    -
    -
    -
    -
    ./convex account create --public-key=eb1234 --password=my-password
    -
    -
    -
    -

    The command returns the account address and public key used to create the account.

    -

    Get an accounts balance

    How to get an account’s balance.

    -
    -

    To obtain the balance of an account, you just need to provide the address of the account.

    -
    -
    -

    So to run:

    -
    -
    -
    -
    ./convex account balance 45
    -
    -
    -
    -

    Returns the balance for account #45

    -

    Request funds for an account

    How to request funds for an account.

    -
    -

    With this command, you can request funds for an account. This command transfers the funds from the first peer account to the named account. If the network has been started by the GUI or another user, you will not be able to run this command since the transfer of funds will need to know the private key of the first peer.

    -
    -
    -

    For example, first create an account:

    -
    -
    -
    -
    ./convex account create --password=my-password
    -
    -
    -
    -
    -
    Public Key: 1a47522ec91db1209839cf96c99948e77c489310799b29e6bf02038bc67a111c
    -Address: 46
    -Account usage: to use this key can use the options --address=46 --public-key=1a4752
    -
    -
    -
    -

    You can then request funds for this account by providing the --address and --public-key

    -
    -
    -
    -
    ./convex account fund --address=46 --public-key=1a4752 --password=my-password
    -Balance: 100000000
    -
    -

    Get information about an account

    How to get information about an account.

    -
    -

    This command returns the information about the account.

    -
    -
    -
    -
    ./convex account info 46
    -Result: {:sequence 0,:balance 100000000,:allowance 0,:environment nil,:metadata nil,:holdings {},:controller nil,:key 0x1a47522ec91db1209839cf96c99948e77c489310799b29e6bf02038bc67a111c}
    -
    -
    -
    -
    -
    Data type: Record
    -
    -
    @@ -2270,61 +1713,6 @@

    Status

    How to get the local network status.

    -
    -

    This command gets the current status from the local network.

    -
    -
    -

    For example:

    -
    -
    -
    -
    ./convex  status --password=my-password
    -State hash: 0x68b40285d8b3d5831d829f45acaa5066a793ac1aaa3fb603a07ef8c00512f414 (1)
    -Timestamp: 1580602820020 (2)
    -Timestamp value: 2020-02-02T00:20:20.020Z (3)
    -Global Fees: 0 (4)
    -Juice Price: 2 (5)
    -Total Funds: 1,000,000,000,000,000,000 (6)
    -Number of accounts: 46 (7)
    -Number of peers: 4 (8)
    -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    1The current hash state for the local peer.
    2The peer’s timestamp as a long number.
    3The Peer’s timestamp
    4The amount of global fees.
    5The current juice price for each transaction.
    6Total funds available in this network.
    7Total number of accounts in this network.
    8Number of peers connected to this network.
    -
    @@ -2333,59 +1721,20 @@

    Queries

    How to execute queries on a local Convex network.

    -
    -

    When you run a query, you usually need to provide an account address --address, but this command uses a default account address.

    -
    -
    -

    So to find out the current balance using the balance command you can run the following:

    -
    -
    -
    -
    ./convex query '*balance*'
    -Result: 260397600000000000
    -Data type: Long
    -
    -
    -

    Transactions

    +

    Trancsactions

    How to execute transactions on a local Convex network.

    -
    -

    Submitting a transaction on the Convex network can change the network state, and so this will incur a small fee that is passed back to the peers.

    -
    -
    -

    So when sending a transaction you will need to provide a --public-key or --index-key field for the keypair stored in the keystore, and also the correct --address.

    -
    -
    -

    For example:

    -
    -
    -
    -
    ./convex transaction --address=46 --public-key=1cf32e --password=secret "(map inc [1 2 3 4 5])"
    -
    -
    -
    - - - - - -
    - - -The CLI uses the internal Peer API for queries and transactions. To use other Convex networks or remote Convex networks we suggest you use the public Convex Clients instead. -
    -
    From 82befe1673c1271c46360e95d910c514924b5fed Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 28 Sep 2021 09:47:24 +0000 Subject: [PATCH 0038/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f741fe886707993f0d6ec06378e0959a00f4dd7d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 735 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 693 insertions(+), 42 deletions(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 92b5e5783..337a30138 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -678,7 +678,7 @@ @@ -747,27 +753,33 @@

    Convex Command Line Interface

    Introduction

    -

    Convex Command Line Interface or CLI, allows you to control and setup a local Convex network and peers. -More information can be found at convex.world.

    +

    Convex Command Line Interface or CLI, allows you to control and setup a local Convex network, or add a peer to an already existing network. +The current test Convex network can be found at convex.world.

    Overview

    -

    The CLI is part of the Convex library. The CLI has been built to do the following things:

    +

    This CLI is part of the Convex code base. The CLI has been built to do the following things:

    1. -

      Local Convex network.

      +

      Run a Local Convex network.

      +
    2. +
    3. +

      Run a Local Peer(s) connected to a local network.

      +
    4. +
    5. +

      Send and Query transactions within a local network.

    6. -

      Local Peer(s) connected to a local network.

      +

      Setup accounts within a local network.

    7. -

      Send and Query transactions with a local network.

      +

      Setup key pairs to use for accessing local and remote networks.

    8. -

      Local Peer connected to a remote Convex network.

      +

      Run a Local Peer connected to a remote Convex network.

    @@ -778,7 +790,7 @@

    Overview

    Getting Started

    -

    Currently the CLI can only be downloaded via github.

    +

    As yet we have not packaged this in any package manager for downloading so currently the CLI can only be downloaded via Github.

    Get the source

    @@ -786,7 +798,7 @@

    Get the source

    You will need to visit convex-dev @ github and clone the repository onto your local computer.

    -

    At the moment the develop branch is currently the latest version.

    +

    The "develop" branch is currently the latest version.

    terminal commands
    @@ -801,7 +813,7 @@

    Get the source

    Convex Projects

    -

    The Convex repository is made up of the following sub projects:

    +

    The Convex code repository is made up of the following subprojects:

    @@ -823,13 +835,10 @@

    Convex Projects

    convex-peer
    -

    Peer library used to run convex peer.

    +

    Peer library used to run convex peers.

    -
    -

    The CLI project is currently part of the main Convex repository.

    -

    Compile and Setup

    @@ -837,7 +846,7 @@

    Compile and Setup

    Once you have downloaded the latest source of Convex, you can now compile the suite of projects.

    -

    To do this you need to execute Maven install or package

    +

    To do this you need to execute the Maven command:

    terminal command
    @@ -885,22 +894,36 @@

    Compile and Setup

    Files needed by CLI run a local Network or Peer

    -

    The CLI needs 3 types of files to read or write before running a local Convex network or as a Peer. +

    The CLI needs 3 types of files before running a local Convex network or as a Peer on any network. The type of files are:

    1. -

      Etch Storage database file. This contains the stored state of the Convex network. Usually when starting up the initial cluster the first set of peers share the same Etch database. CLI Parameter: --etch

      +

      Etch Storage database file. This contains the stored state of the Convex network. Usually, when starting up the initial cluster the first set of peers share the same Etch database. CLI Parameter: --etch

    2. Keystore database file. This file contains the private/public key pairs used for the peers and any subsequent users. CLI Parameters: --keystore, --password

    3. -

      Session file. This is created by the CLI to keep track of the locally running peers, so that if you want to access the local network or add another peer to the local network, the CLI will look at the session file for a randomly available peer to connect too. CLI Parameter: --session

      +

      Session file. This is created by the CLI to keep track of the locally running peers, so that if you want to access the local network or add another peer to the local network, the CLI will look at the session file for a randomly available peer to connect to. CLI Parameter: --session

    +
    + + + + + +
    + + +
    +

    The GUI version and the CLI run the same local network. The only difference is that the GUI stores the genesis keypairs in memory and does not create a session file. This means that some of the CLI features cannot be used with the GUI local network, such as transferring funds, creating accounts.

    +
    +
    +
    @@ -931,7 +954,7 @@

    Running the CLI

    Commands

    -

    The CLI is split into commands and some sub commands they are:

    +

    The CLI is split into command the following commands and subcommands:

    @@ -960,17 +983,17 @@

    Commands

    balance, bal, ba

    -

    Get account balance.

    +

    Get an account balance.

    create, cr

    -

    Creates an account using a public/private key from the keystore.

    +

    Creates an account on a local network using a public/private key from the keystore.

    fund, fu

    -

    Transfers funds to account using a public/private key from the keystore.

    +

    Transfers funds to an account using a public/private key from the keystore.

    @@ -1011,7 +1034,7 @@

    Commands

    generate, ge

    -

    Generate 1 or more private key pairs.

    +

    Generate one or more key pairs.

    @@ -1021,7 +1044,7 @@

    Commands

    export, ex

    -

    Export 1 or more key pairs from the keystore.

    +

    Export key pair from the keystore.

    @@ -1057,7 +1080,7 @@

    Commands

    start, st

    -

    Starts a local convex test network.

    +

    Starts a local convex test network, same as GUI but using a command line.

    @@ -1255,7 +1278,7 @@

    Shared Options

    Requesting Help

    -

    The CLI supports help using the -h or --help options or the command help. For each sub command there are more help options.

    +

    The CLI supports help using the -h or --help options or the command help. For each sub command, there are more help options.

    So for example

    @@ -1299,8 +1322,22 @@

    Simple local start

    ./convex local start --password=my-password
    +
    + + + + + +
    + + +
    +

    In this document, the password option will always be shown as --password=my-password. This is an example of a not very good password to use for storing your keys. We suggest that you use a more secure password instead of my-password.

    +
    +
    +
    -

    You wil always need to pass the password to the keystore file since the CLI will need access to create and start up the local peers.

    +

    You will always need to pass the password to the keystore file since the CLI will need access to the keys to create and start up the local peers.

    The CLI will automatically create 4 keypairs and place them in the keystore. The CLI will then start up 4 peers all sharing a single @@ -1318,7 +1355,7 @@

    Simple local start

    Store the new keypairs in the keystore.

  • -

    Start up the local network using the new created keys.

    +

    Start up the local network using the newly created keys.

  • @@ -1326,7 +1363,7 @@

    Simple local start

    Local start with peer keys

    -

    While the simple local start will auto generate public keys for the local peers, create the peer accounts. You have the option to start any number of peers using the private keys from your keystore. To do this you need to provide a list of public keys that you want the CLI to use to start up the local network.

    +

    While the simple local network start will auto-generate public keys for the local peers and create the peer accounts. You have the option instead to start the local network using a predetermined set of keys from your keystore. To do this you need to provide a list of public keys that you want the CLI to use to start up the local network.

    If you have already used the simple local start, you can get the list of keys created by running the List keys, @@ -1401,6 +1438,55 @@

    Local start with peer keys

    +

    Local start with port numbers

    +
    +

    By default, the CLI start a local network with each peer assigned a random port number. You can specify the port numbers used for each peer, by setting the --ports option.

    +
    +
    +

    The --ports option takes a list or range of port numbers.

    +
    +
    +

    You can use multiple --ports options such as:

    +
    +
    +
    +
    ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081 --ports=8082 --ports=8083 --ports=8084
    +
    +
    +
    +

    or you can provide a list of ports to use for each peer:

    +
    +
    +
    +
    ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081,8082,8083,8084
    +
    +
    +
    +

    or a range of port numbers:

    +
    +
    +
    +
    ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081-8084
    +
    +
    +
    +

    or an open range for any number of peers:

    +
    +
    +
    +
    ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8081-
    +
    +
    +
    +

    or a combination of the above, where the first peer uses port 8088, and all subsequent peers use ports from 8090:

    +
    +
    +
    +
    ./convex local start --index-key=1,2,3,4 --password=my-password --ports=8088 --ports=8090-
    +
    +
    +
    +

    Local start with config file

    You can create a config file and assign the command options as config items. You can then start your @@ -1438,8 +1524,10 @@

    Config Parameters for convex. convex.local.start.public-key=8291e89 convex.local.start.public-key=ce55bb8 + convex.local.start.ports=8090- (3) + # keystore password - convex.local.password = (3) + convex.local.password = (4)

    @@ -1463,6 +1551,10 @@

    Config Parameters for convex. 3 +The peers will use port 8090 onwards + + +4 If you do not provide a password, then the CLI will request a password on starting the local network. @@ -1477,13 +1569,264 @@

    Starting a local Peer

    How to start a local peer, and join a local Convex network.

    +
    +

    To start a local peer you first need to do the following:

    +
    +
    +
      +
    1. +

      Start a local Convex network. see Starting a local network.

      +
    2. +
    3. +

      Create a keypair, or select an unused keypair to use for the peer.

      +
    4. +
    5. +

      Create an account for the peer.

      +
    6. +
    7. +

      Assign funds to the peer account.

      +
    8. +
    9. +

      Assign the peer account funds for the peer stake.

      +
    10. +
    +
    +
    + + + + + +
    + + +
    +

    This type of blockchain technology uses the Convergent Proof of Stake (CPoS) algorithm, where each peer has a public key and a stake amount. The stake amount decides the peers voting control in the CPoS algorithm. See Convex Technology

    +
    +
    +
    +
    +

    The following command does all of the above except step #1:

    +
    +
    +
    +
    ./convex peer create --password=my-password
    +
    +
    +
    +

    You will then get back from the peer create command something like this:

    +
    +
    +
    +
    Public Peer Key: 0xbc1290834e1953b2952624ab8ce34e87d308ba975d655163f9fe47283f0436aa
    +Address: 45
    +Balance: 199945799
    +Inital stake amount: 9800000000
    +Peer start line: ./convex peer start --password=my-password --address=45 --public-key=bc1290
    +
    +
    +
    +

    you can then copy the Peer start line: and run a peer with the local network.

    +
    +
    +
    +
    ./convex peer start --password=my-password --address=45 --public-key=bc1290
    +
    +
    -

    Starting a local Peer to a remote Convex network

    +

    Setting up a local Peer with Convex.world

    -

    How to start a local peer, that connects to a remote Convex network.

    +

    To start a local peer connected to the convex.world network you will need to first setup the peer

    +
    +
    +

    Peer Setup in convex.world

    +
    +
      +
    • +

      Access the convex.world/sandbox

      +
    • +
    • +

      Create a new account, you will need the account number later to start the peer.

      +
    • +
    • +

      Then go to the 'Request coins' section and request some more coins for your new account.

      +
    • +
    • +

      Using the convex-cli generate a new keypair for your new peer. See Generating keypairs

      +
      +
      +
      ./convex key generate --password=my-password
      +Index Public Key
      +0 dfb22da0afda1a123e523ded624f184719a4416e9aac6f6fdedd8518fb09fe3c
      +
      +
      +
    • +
    +
    +
    + + + + + +
    + + +
    +

    The public key address, account number and URL shown in this example will be different values for when you run and paste these commands.

    +
    +
    +
    +
    +
      +
    • +

      Using the generated public key and a set stake amount (e.g. 19999 this number must be less than your current balance), +paste the following command into the sandbox command area (remember to use your public key instead):

      +
      +
      +
      (create-peer 0xdfb22da0afda1a123e523ded624f184719a4416e9aac6f6fdedd8518fb09fe3c 199999)
      +
      +
      +
      +
        +
      • +

        The syntax for create-peer is:

        +
        +
        +
        (create-peer <public-key> <stake-amount>)
        +
        +
        +
      • +
      +
      +
    • +
    +
    +
    + + + + + +
    + + +
    +

    You must use your generated public key to set-key command, as this will lock you out of the sandbox, since the sandbox +no longer has the keypair for this account.

    +
    +
    +
    +
    +
      +
    • +

      Setup the new account to use your new peer key, by running the command in the sandbox:

      +
      +
      +
      (set-key 0xdfb22da0afda1a123e523ded624f184719a4416e9aac6f6fdedd8518fb09fe3c)
      +
      +
      +
      +
        +
      • +

        The syntax for set-key is:

        +
        +
        +
        (set-key <public-key>)
        +
        +
        +
      • +
      +
      +
    • +
    +
    +
    + + + + + +
    + + +
    +

    Remember to add the 0x at the front of the key value, when using a public key in the sandbox.

    +
    +
    +
    +
    +
    +

    Starting the peer with convex.world

    +
    +

    If the peer has been setup correctly on convex.world, so you can now start your peer, using the following example command:

    +
    +
    +
    +
    ./convex peer start --public-key=dfbb22 --password=my-password --address=<address> --port=80888 --url=<my-ip>:8088 --peer=convex.world:18888
    +
    +
    +
    +

    Where:

    +
    +
    +
      +
    • +

      <address> is the account number of your peer.

      +
    • +
    • +

      <my-ip> is the public ip address of the running peer.

      +
    • +
    +
    +
    +

    or for example you can create a config file with the following options set:

    +
    +
    +
    +
    convex.peer.start.peer=convex.world:18888
    +convex.peer.start.public-key=dfb22da0afda1a123e523ded624f184719a4416e9aac6f6fdedd8518fb09fe3c
    +convex.peer.start.address=47
    +convex.peer.start.url=206.1.1.1:8181
    +convex.peer.start.port=8181
    +convex.password=my-password
    +
    +
    +
    +
      +
    • +

      Run convex only using the config file

      +
      +
      +
      ./convex peer start --config=my-peer.conf
      +
      +
      +
    • +
    +
    +
    + + + + + +
    + + +
    +

    You will need allow the peer port remote access. In our example the peer port is set to 8181, so we have allowed TCP traffic to be forwarded and passed to port 8181.

    +
    +
    +
    +
    +

    If you wish to connect to your own remote peer, you can by adding the --peer=<remote peer URL> option. This tells the new peer you are starting where a remote peer is located. Once found the started peer will try and sync with the remote peer.

    +
    +
    +

    You will also need to set the --url=<my peer URL> option to specify the remote URL of your running peer, so that other peers can communicate to your peer.

    +
    @@ -1493,6 +1836,30 @@

    Starting the GUI local network

    How to start the gui local network.

    +
    +

    To start the local GUI network, you can call the command:

    +
    +
    +
    +
    ./convex local gui
    +
    +
    +
    +

    This starts a local network in GUI mode. At the moment the GUI local network does not publish the keypairs used for the network, so the CLI cannot do the following when the GUI network is running:

    +
    +
    +
      +
    1. +

      Account Fund Request

      +
    2. +
    3. +

      Account Create

      +
    4. +
    5. +

      Peer Create

      +
    6. +
    +
    @@ -1524,7 +1891,7 @@

    Peer Output

    -

    On every event that occurs for a peer in the cluster, on it’s own an event is shown as a line.

    +

    On every event that occurs for a peer in the cluster, on its own an event is shown as a line.

    The event data can be split up into the following fields:

    @@ -1647,31 +2014,141 @@

    Peer Output

    Managing your Keys - The Keystore

    -

    To run any peer you need to manage the local public/private key pairs.

    +

    How to manage the local public/private key pairs.

    +
    +
    +

    When using any of the key sub commands, you do not need to be connected to any network.

    +
    +
    +

    The option --keystore can be used with any sub command to specify which keystore to use.

    Generating keypairs

    How to generate a new set of public/private keys.

    +
    +

    You need to generate keypairs when:

    +
    +
    +
      +
    1. +

      Creating an account

      +
    2. +
    3. +

      Creating a new peer

      +
    4. +
    +
    +
    +

    This command allows you to create 1+ keypairs in the keystore.

    +
    +
    +

    So for example this will create 10 keypairs:

    +
    +
    +
    +
    ./convex key generate 10 --password=my-password
    +
    +

    List keys

    How to list the keys store in the keystore.

    +
    +

    To list out your keystore and view the public keys of each keypair.

    +
    +
    +
    +
    ./convex key list --password=my-password
    +
    +
    -

    Export keys

    +

    Exporting keys

    How to export the keys from your keystore to encrypted text.

    +
    +

    You can export a keypair from the keystore to an encrypted PEM formated text. This is useful if you need +to give another user or application access to your network.

    +
    +
    +

    You need to provide an --export-password option with the password of the encrypted PEM formated text.

    +
    +
    +

    You also need to provide the location of the keypair you wish to export, this can be done using the --index-key or --public-key option.

    +
    +
    +

    In this example first list out the keys from the keystore.

    +
    +
    +
    +
    ./convex key list --password=my-password
    +
    +
    +
    +
    +
    1 e7fdcb0bfdfb786b51eedf33b575....
    +2 373d2a583695ff367dd986e12785....
    +..
    +
    +
    +
    +

    If we now want to export the key #2, then we can use the following command:

    +
    +
    +
    +
    ./convex key export --index-key=2 --export-password=my-password --password=my-password
    +
    +
    +
    +

    or a more reliable way is to use the first hex of the public key

    +
    +
    +
    +
    ./convex key export --public-key=373d2a583695ff --export-password=my-password --password=my-password
    +
    +
    +
    + + + + + +
    + + +
    +

    In this example, we have used an insecure password of my-password to encrypt the exported key. We suggest that you use a better password when exporting your keys, and keep the exported PEM formated text secure.

    +
    +
    +
    -

    Import keys

    +

    Importing keys

    How to import keys into the keystore.

    +
    +

    You would need to import keys, when you want to run a peer or send a transaction for an account on another network.

    +
    +
    +

    To import a keypair you need to set the options --import-file or --import-file and --import-password.

    +
    +
    +

    So for example:

    +
    +
    +
    +
    ./convex key import --import-file=my_key.pem --import-password=my-password --password=my-password
    +
    +
    +
    +

    If the import password is successful, this will import the keypair into the keystore, and show the public key of the imported keypair.

    +
    @@ -1681,29 +2158,109 @@

    Managing Accounts

    Information on how to create, fund and get information about the local accounts.

    +
    +

    This set of sub commands manage accounts on the local network. You need to have a local network running on the same computer for these commands to work.

    +
    +
    +

    The reason is that the keystore needs to contain the keys for the first genesis accounts in the network. With access to the genesis keypair, the account commands can create an account, and transfer sufficient funds to the new account.

    +

    Create an account

    How to create a local account.

    +
    +

    To create a new account and new keypair, you can just run:

    +
    +
    +
    +
    ./convex account create --password=my-password
    +
    +
    +
    +

    If you wish to use an already defined keypair in your keystore, you can set the --index-key or --public-key options.

    +
    +
    +
    +
    ./convex account create --public-key=eb1234 --password=my-password
    +
    +
    +
    +

    The command returns the account address and public key used to create the account.

    +

    Get an accounts balance

    How to get an account’s balance.

    +
    +

    To obtain the balance of an account, you just need to provide the address of the account.

    +
    +
    +

    So to run:

    +
    +
    +
    +
    ./convex account balance 45
    +
    +
    +
    +

    Returns the balance for account #45

    +

    Request funds for an account

    How to request funds for an account.

    +
    +

    With this command, you can request funds for an account. This command transfers the funds from the first peer account to the named account. If the network has been started by the GUI or another user, you will not be able to run this command since the transfer of funds will need to know the private key of the first peer.

    +
    +
    +

    For example, first create an account:

    +
    +
    +
    +
    ./convex account create --password=my-password
    +
    +
    +
    +
    +
    Public Key: 1a47522ec91db1209839cf96c99948e77c489310799b29e6bf02038bc67a111c
    +Address: 46
    +Account usage: to use this key can use the options --address=46 --public-key=1a4752
    +
    +
    +
    +

    You can then request funds for this account by providing the --address and --public-key

    +
    +
    +
    +
    ./convex account fund --address=46 --public-key=1a4752 --password=my-password
    +Balance: 100000000
    +
    +

    Get information about an account

    How to get information about an account.

    +
    +

    This command returns the information about the account.

    +
    +
    +
    +
    ./convex account info 46
    +Result: {:sequence 0,:balance 100000000,:allowance 0,:environment nil,:metadata nil,:holdings {},:controller nil,:key 0x1a47522ec91db1209839cf96c99948e77c489310799b29e6bf02038bc67a111c}
    +
    +
    +
    +
    +
    Data type: Record
    +
    +
    @@ -1713,6 +2270,61 @@

    Status

    How to get the local network status.

    +
    +

    This command gets the current status from the local network.

    +
    +
    +

    For example:

    +
    +
    +
    +
    ./convex  status --password=my-password
    +State hash: 0x68b40285d8b3d5831d829f45acaa5066a793ac1aaa3fb603a07ef8c00512f414 (1)
    +Timestamp: 1580602820020 (2)
    +Timestamp value: 2020-02-02T00:20:20.020Z (3)
    +Global Fees: 0 (4)
    +Juice Price: 2 (5)
    +Total Funds: 1,000,000,000,000,000,000 (6)
    +Number of accounts: 46 (7)
    +Number of peers: 4 (8)
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    1The current hash state for the local peer.
    2The peer’s timestamp as a long number.
    3The Peer’s timestamp
    4The amount of global fees.
    5The current juice price for each transaction.
    6Total funds available in this network.
    7Total number of accounts in this network.
    8Number of peers connected to this network.
    +
    @@ -1721,14 +2333,53 @@

    Queries

    How to execute queries on a local Convex network.

    +
    +

    When you run a query, you usually need to provide an account address --address, but this command uses a default account address.

    +
    +
    +

    So to find out the current balance using the balance command you can run the following:

    +
    +
    +
    +
    ./convex query '*balance*'
    +Result: 260397600000000000
    +Data type: Long
    +
    +
    -

    Trancsactions

    +

    Transactions

    How to execute transactions on a local Convex network.

    +
    +

    Submitting a transaction on the Convex network can change the network state, and so this will incur a small fee that is passed back to the peers.

    +
    +
    +

    So when sending a transaction you will need to provide a --public-key or --index-key field for the keypair stored in the keystore, and also the correct --address.

    +
    +
    +

    For example:

    +
    +
    +
    +
    ./convex transaction --address=46 --public-key=1cf32e --password=secret "(map inc [1 2 3 4 5])"
    +
    +
    +
    + + + + + +
    + + +The CLI uses the internal Peer API for queries and transactions. To use other Convex networks or remote Convex networks we suggest you use the public Convex Clients instead. +
    +
    From f99b080d6fab11f7a8ee8ee9aa94c6a695cc6326 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 28 Sep 2021 09:51:04 +0000 Subject: [PATCH 0039/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c59314210ba29c69ab8ab2300240c23884624d9c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 337a30138..942a7e292 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From cf4cd989ddf9a42e4e854895e7fc94a90871ce7e Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 28 Sep 2021 10:24:47 +0000 Subject: [PATCH 0040/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@486d3c302e7138288c6ed9dce2ba1cbf2dc037c8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 942a7e292..78f0dc5c7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From cde67a3ea9a86aacee2e7aac6ed120727d82bbf1 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 28 Sep 2021 10:45:53 +0000 Subject: [PATCH 0041/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6c70c35a8b5058b9d5c976f87a49b7cd7c6394f8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 78f0dc5c7..c97805170 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 2d9de57db4ba12c45b0337f684b01a1e87816313 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 28 Sep 2021 11:01:19 +0000 Subject: [PATCH 0042/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@063d00ae8608376aa03fd309b67635ab9c53244a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c97805170..1e7c8ae09 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 16e24a38fdacb68f491944b97ee9725e5634ede3 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 28 Sep 2021 11:02:47 +0000 Subject: [PATCH 0043/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@88c0bed70f13896eb4beaf857161d1818f401975?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1e7c8ae09..ef0409788 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 0128a448f0301f4a375317461a32c5225712490e Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 28 Sep 2021 13:06:28 +0000 Subject: [PATCH 0044/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e6db05a611cd4a1fb51f959e20d246637bb7744a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ef0409788..083340ce8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From ef659cefc813262fcb4fc707a3a62aafa0b44fa7 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 28 Sep 2021 13:15:01 +0000 Subject: [PATCH 0045/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1b2715719f2cf92d3f4f1e6ed58a7ab5dd86b531?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 083340ce8..02d6441d5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 36269b3a49ded453f4005ee6805da07207fa67e6 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 28 Sep 2021 13:16:09 +0000 Subject: [PATCH 0046/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1b2715719f2cf92d3f4f1e6ed58a7ab5dd86b531?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 02d6441d5..9f43af17c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 132867b2c5ee821373be35f7056bc9a7eb24018e Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 3 Oct 2021 05:10:54 +0000 Subject: [PATCH 0047/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cb73f69fded08135ffa833d9eaf95eed64f6d046?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9f43af17c..334ca687e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 3d548e66b012c88f50cd311848cefcb99e0055ea Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 14 Oct 2021 01:26:12 +0000 Subject: [PATCH 0048/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8b7cc768915737eea050d7613b6a9cea32bb3fa8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 334ca687e..110d0a1bb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From fbed097a75867fb6a24843a9921136d42592e286 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 15 Oct 2021 10:12:45 +0000 Subject: [PATCH 0049/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8b7cc768915737eea050d7613b6a9cea32bb3fa8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 110d0a1bb..b743b758c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 430ac5df337f4751b23c54fcc7808b4542492a25 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 23 Oct 2021 02:15:07 +0000 Subject: [PATCH 0050/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c89a313b6b733a014712c4067a1944034c999a21?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b743b758c..5fba65779 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 59168dadda71f1c21fe4ea28d396a82a277ee159 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 24 Oct 2021 06:17:27 +0000 Subject: [PATCH 0051/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0822c02e7caa9062bf856a4eaca98ffd003fbd1a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5fba65779..f90371318 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 52d1bfa6ce111b279baa924e5408fd3214d3ca20 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 25 Oct 2021 03:05:11 +0000 Subject: [PATCH 0052/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ee48769b14d027764d3fd75bc8687c909cdd206c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f90371318..123a13054 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From f0623f4296b12a53ecc4bd29be4f4c3ed854212c Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 25 Oct 2021 03:19:32 +0000 Subject: [PATCH 0053/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e59864729cc70b2d73df240cfa9e5b566b49c885?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 123a13054..ae737f611 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From d76bafca6ae18effa82d0ba74820b5139861d67e Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 29 Oct 2021 01:33:28 +0000 Subject: [PATCH 0054/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@114a606e3e8a209b9ae08bef53c48148ac356ab8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ae737f611..d0960c0c4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 422a942403cf2e716200b948206927aaf9609536 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 30 Oct 2021 00:24:12 +0000 Subject: [PATCH 0055/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7dca97977c3154c0ad676159a64e9cd4f2355cc5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d0960c0c4..ee1687962 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 5548f45c4652f32e1dc1e6a2b8e7b00dfaa46e3b Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 30 Oct 2021 00:52:03 +0000 Subject: [PATCH 0056/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@49109973be1f2ed3a8356a54330a6f4cd00df549?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ee1687962..13aef8e9c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From fb4b96836422f77b1a6f7d6213048d6b0222f1f6 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 30 Oct 2021 23:10:43 +0000 Subject: [PATCH 0057/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@10fc4aee2acc00b27f8c78a9c2bb190f229d1912?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 13aef8e9c..2c7673129 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 7746bf3dbf09b6a0bd680eeddf71e351b38c88af Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 31 Oct 2021 08:53:11 +0000 Subject: [PATCH 0058/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2906457be9b974c7ec9555808c240e410606bc6c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2c7673129..d239fefc5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From a98f3c8c3ef5298c5c04e9703178a83cdfc7fd0e Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 31 Oct 2021 09:31:29 +0000 Subject: [PATCH 0059/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@eeda900f98a6ddd7b7476a624c1d35c6033adeb7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d239fefc5..a470548df 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 3da15f4997cc4bf1700f1dfaaffc7caf5d4504bd Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 31 Oct 2021 22:55:46 +0000 Subject: [PATCH 0060/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@41562d622b7ab846f3916dfc0f5b0b3860657054?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a470548df..e17f44a7e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 94ba7c78273c9a0003f7d72d032bf23fe83b6e22 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 31 Oct 2021 23:10:49 +0000 Subject: [PATCH 0061/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@870ae66bbf9617f56983542bd95fe1f1ea9e0df4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e17f44a7e..520d987b4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 6e77617a5850daf896cc194ec1d04a7249d4d988 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 1 Nov 2021 00:18:52 +0000 Subject: [PATCH 0062/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4d80e86d351a6d7ba425a8c32651a0be5f7405a5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 520d987b4..e3831b153 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 7533ca484c2491eba716cd90a3548447ab73cc7e Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 1 Nov 2021 00:25:53 +0000 Subject: [PATCH 0063/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@22bb34e2a2c680f9eafe674b0f688c41413f746f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e3831b153..d7b8d0cf2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From f8dab7311f5b705bb14c4f5f14f447389bcc706c Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 1 Nov 2021 00:25:54 +0000 Subject: [PATCH 0064/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@22bb34e2a2c680f9eafe674b0f688c41413f746f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d7b8d0cf2..698e37d11 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 2c1c8e68e2519e544a57ac893801cce95edee14a Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 1 Nov 2021 00:52:41 +0000 Subject: [PATCH 0065/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4f694c5cc247bb31f7aec104d3bd829051e40e7b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 698e37d11..6daf34afa 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 7bee49000449d8940ab27f962a43b47db6ae3de4 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 1 Nov 2021 10:29:08 +0000 Subject: [PATCH 0066/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ca5b03dd7515f54942b09016a3a0559a46cd1e87?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6daf34afa..8299c10f5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 86b492433056727a6f57771206970bc71e88bebc Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 9 Nov 2021 00:16:20 +0000 Subject: [PATCH 0067/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f64e6dbeb52377d05d440d989ad248774091a382?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8299c10f5..75af71200 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From de2058b04254eb590b0b9c496cae5cb6640b1c3d Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 9 Nov 2021 00:22:15 +0000 Subject: [PATCH 0068/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2203b3d70008ea498c40698621fec5da69653502?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 75af71200..a0d85d407 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 13c39606df49269672f24a7c767df72e2d815b80 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 9 Nov 2021 01:02:54 +0000 Subject: [PATCH 0069/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@db7700d0d0e6917a4e0169666fe66386d10ef4fe?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a0d85d407..ec088d372 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From ebd034bf419db051d060321e6c44fe621464f21f Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 9 Nov 2021 01:40:52 +0000 Subject: [PATCH 0070/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@93d74d4220999a9aefcfdfa4af714ec741bd51de?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ec088d372..b2e92d5c3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 061325204a84a1346cb029b8b854ea88bfdcee71 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 Nov 2021 23:32:41 +0000 Subject: [PATCH 0071/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1a34e780af633d2b31621185de5c7a481bd3771c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b2e92d5c3..052127f66 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From e79df3922e8b733cc3371c7d8b0f71b8475f13e1 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 23 Nov 2021 02:09:54 +0000 Subject: [PATCH 0072/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a11944e9f91282958e51c59b5a370f03d718899a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 052127f66..ed0bba5ab 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From fe59a7d9fb0915fb254aced9a8acdf4a03e38faa Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 27 Nov 2021 00:23:56 +0000 Subject: [PATCH 0073/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1599eabeab657b52c72fb5cdefd25fd32f9ccbab?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ed0bba5ab..47bbe9abf 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 413644699cfd839545139432b0821f73c9957008 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 28 Nov 2021 07:37:10 +0000 Subject: [PATCH 0074/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@add13b19286a53f5992542c02f55027e6be1a76a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 47bbe9abf..cfd69e095 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 5fc7ae77e540c48ea925ff8253608cc484572472 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 28 Nov 2021 08:56:54 +0000 Subject: [PATCH 0075/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@559f953197c2715f5f57f7d0498961473910e003?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cfd69e095..245b56e82 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From ba400be690d5c2a607dbd6bfe24926f62a85f6da Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 28 Nov 2021 09:20:09 +0000 Subject: [PATCH 0076/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@559f953197c2715f5f57f7d0498961473910e003?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 245b56e82..c9f77be23 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From bdceea801e1cfe79f4d0ade55dde684ea1ac12f2 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 29 Nov 2021 10:56:17 +0000 Subject: [PATCH 0077/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@deaa2c9f1a3da0786b5bd6298d365ad6e0a62098?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c9f77be23..acb17ac9a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 250045e5e40c89f7a60b20c59f6859beb72343ad Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 29 Nov 2021 11:02:30 +0000 Subject: [PATCH 0078/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@46ad92f9b4181f8900bc234623b0ff2c109c57a8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index acb17ac9a..97d5bcfe8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From f4027303415777974a1077e1fb78b6d6ad62e447 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 29 Nov 2021 11:02:54 +0000 Subject: [PATCH 0079/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@46ad92f9b4181f8900bc234623b0ff2c109c57a8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 97d5bcfe8..7a5aa7f1a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From d434f5ba19222c60dc433ef3d32b8fbbb283aa6c Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 13 Dec 2021 03:47:58 +0000 Subject: [PATCH 0080/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@399b5c3dc3c850911925a892615dadf3bde457af?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7a5aa7f1a..d471787e2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 519f71b563ab30cb8067673ec1836e6798c74188 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 13 Dec 2021 20:46:54 +0000 Subject: [PATCH 0081/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@91f6c71e53311a6892587b1efebd0634eb9d98f3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d471787e2..b065a6a26 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 79045f090ce207947e48c5c5340ecab3831cf0fa Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 13 Dec 2021 21:07:52 +0000 Subject: [PATCH 0082/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cacf81fb5b6b058e4e85bf5d3d0d5b31cfc2ba1b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b065a6a26..158b07ded 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From cc58e1cce3149066cbf271587b1d94c30c75e94b Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 20 Dec 2021 02:39:08 +0000 Subject: [PATCH 0083/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@def036558688444766ae9554a8f21ee523d1403a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 158b07ded..6516fb8fd 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 3c7e51587a71e07de5332f322292e52403027116 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 18 Feb 2022 07:02:48 +0000 Subject: [PATCH 0084/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@78dd64d35892992d38bdc6e79e8c72b49f06f0dd?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6516fb8fd..b6a6fed50 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 4c2dfde758e2c02ac2b41893f5606c710d198a21 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 18 Feb 2022 07:03:39 +0000 Subject: [PATCH 0085/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2561c0658688b5311950bd34895b393b130f6dfe?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b6a6fed50..83f1f2828 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 63f3fe95e85e093b152ba8f51334af4cffeeaa20 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 18 Feb 2022 07:15:57 +0000 Subject: [PATCH 0086/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c226c45855cf82c158b2b94091da64395ba5fb19?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 83f1f2828..38adce5d7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 8254ff2101f8f81587c6e3a8947ccf5a6270b983 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 18 Feb 2022 07:32:06 +0000 Subject: [PATCH 0087/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@10393fb9e587198bcd71a94287d2c2edbc86dcb5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 38adce5d7..8dd712c31 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From bc671ea13d341c7c737e2c19a54f572d7864f8cb Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 18 Feb 2022 07:39:58 +0000 Subject: [PATCH 0088/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9601bb7069b309a55536b7ac56e44b9cd34cefc1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8dd712c31..9f3f83d56 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 50e2beb4460ef8923bfd83f9613974a4ee277909 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 18 Feb 2022 07:40:09 +0000 Subject: [PATCH 0089/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9601bb7069b309a55536b7ac56e44b9cd34cefc1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9f3f83d56..b132ab4d5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From d86bbfb4626b96cf8a55e2a0b6f78690dca50f49 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 6 Mar 2022 12:37:46 +0000 Subject: [PATCH 0090/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2010ea58c81ce293a1b0ccdaaa66d2eccbc67f86?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b132ab4d5..ceee41e42 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 09ca29b8bf5e7b829c8d7dcbbe6f37bcdc80dd68 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 6 Mar 2022 13:14:02 +0000 Subject: [PATCH 0091/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b892e3876225dccccd12f802519bdba506b54fd4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ceee41e42..9cc405ef2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 09d6b281ab413b2b4b1697cf33762c3ed726b4f7 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 6 Mar 2022 13:37:32 +0000 Subject: [PATCH 0092/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@53aeb094f63a2085e8dd03cf0a538a7273e9203a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9cc405ef2..98e5cca1c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From b53d5b1796a14ad1d6b6ffb836dad2b0443e7309 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 6 Mar 2022 13:51:10 +0000 Subject: [PATCH 0093/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9c8f99dfa99ddff17dd0cedb6e0c8c054409a46d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 98e5cca1c..886408941 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 6f5549987a678d43159d8036cc56fd7a741ac884 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 6 Mar 2022 14:00:42 +0000 Subject: [PATCH 0094/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2a36f02fe48ffc87abd6c3c556fe09f22309a00a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 886408941..c3e3fc197 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From a79d4ae3d31d0bbad33228b1db4930c87f16a8b1 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 6 Mar 2022 14:07:05 +0000 Subject: [PATCH 0095/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9454b8ab49b9b63424ee6fc5360c8f762a9d6fa3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c3e3fc197..01b4de4df 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 04f0d1e000327853704ba7e21e13c3b5fe860f5d Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 17 Mar 2022 03:33:29 +0000 Subject: [PATCH 0096/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@06ac47f8c1a73b801baa188d5a54515e316d35fa?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 01b4de4df..9931be165 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From a840a79598cc630c9e142b9dd9dc00856c6007ff Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 17 Mar 2022 04:05:45 +0000 Subject: [PATCH 0097/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4e009c029a7ee01e3ba8e98649716cd2093ea044?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9931be165..13f7fa20a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 68e9a40c95e41ec02cef1e81da4db74142fad721 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 21 Mar 2022 08:22:31 +0000 Subject: [PATCH 0098/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@74d6b5e535cb12dbcf9020f588087d555f49dba4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 13f7fa20a..84308f69d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From df5cd705a76889bfab6c2ab15f88f6df3f4ffd59 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 23 Mar 2022 10:26:10 +0000 Subject: [PATCH 0099/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4ce0121f380f73c1b6de4bb900ec93b6db9dfdf6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 84308f69d..78971d646 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From a4ed3502a48768e4879816decdc8f1fab9559656 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 24 Mar 2022 02:05:13 +0000 Subject: [PATCH 0100/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9db9d96e9ea86ea9cfeff3b2010da0d111072bde?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 78971d646..8f3c8e817 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 98e5a039d5d8337d413c58c594f7686b707a0a78 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 24 Mar 2022 02:25:43 +0000 Subject: [PATCH 0101/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@eeb0a989b96671d0ebd17bb382cd608cf2373578?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8f3c8e817..cad34379f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From da2c0b33964c4676148f56425c1054439033f835 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 24 Mar 2022 04:33:08 +0000 Subject: [PATCH 0102/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cbc98fe6f8daffbb2e77de66f442f9524349c140?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cad34379f..f27bd36b2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 018532cc2324b4d9742ce8eea7db63b868bedb74 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 24 Mar 2022 05:05:05 +0000 Subject: [PATCH 0103/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b2a44243704e0953edc9cec6f3d419d862b52980?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f27bd36b2..5a634ddfe 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 2d254fba4a1eb9c7b2de8453538c38e4612ad248 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 24 Mar 2022 06:02:30 +0000 Subject: [PATCH 0104/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@27ccd700c4e14adc080f52c42c2f5a77ee8f9d73?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5a634ddfe..de6a09b3a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 5383807915f38c0d8706b575971a18fa6492b263 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 24 Mar 2022 06:11:56 +0000 Subject: [PATCH 0105/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@44e22e4a5ae26e269457cade4c30cad741d7f1a0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index de6a09b3a..d9fecf599 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 3f071debdd3b95f4278420f34a6b3b15e57c537c Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 25 Mar 2022 03:22:14 +0000 Subject: [PATCH 0106/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2248d2441a3809629116e883197bdb759141aeba?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d9fecf599..76fe321fe 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 3681a68084f0e98d4de909caf4c45d0a72c1ffda Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 25 Mar 2022 04:30:24 +0000 Subject: [PATCH 0107/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b0030f14e36bae9bacbe8d767b5880109cfe08c9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 76fe321fe..649733fb9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 5092a31372a0c550a5a8b052f827b59534680f73 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 25 Mar 2022 04:52:27 +0000 Subject: [PATCH 0108/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6e012cc4c4534b7afaaddf7f4ef01ffa15b28ec4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 649733fb9..70460116f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 54e1eeb7b1b69a9f3429e1bdfa768f7b5afe16a0 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 25 Mar 2022 05:13:29 +0000 Subject: [PATCH 0109/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@544e3cde31c7f86a298b60ec279f01d55a077a0a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 70460116f..66178022a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 9b75c387bc410c80c44e239a3e4f9bb90a788f9c Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 25 Mar 2022 05:19:57 +0000 Subject: [PATCH 0110/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@088fb6703d90f7c35a0c9d8615cb06861c91a833?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 66178022a..abb063c9f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 83cd3783d1897145bd7d0d6b40d541e71f5d17d5 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 25 Mar 2022 06:07:39 +0000 Subject: [PATCH 0111/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1a6fa1a66cc1a6918f274d469d94081cde89ef1d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index abb063c9f..29dea37ba 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From e3c990e3b8e3a50e198c5412928e7965c8d456a2 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 25 Mar 2022 06:34:53 +0000 Subject: [PATCH 0112/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f950ddf136071156bdd9115329d402bb5ae45d10?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 29dea37ba..622188acb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 441f2e518975d52acbefb3246f1293878c8927eb Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 26 Mar 2022 05:16:51 +0000 Subject: [PATCH 0113/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b24feb6f200c59f33d5779978fe7078c082aa6d2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 622188acb..c15d797b1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 81c4a0ea2f7c297169660a9fa4742d0426a54e44 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 26 Mar 2022 07:36:53 +0000 Subject: [PATCH 0114/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2de3e38d4f8ac8a5b5c45abbedc3ca2048857bcd?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c15d797b1..6ab961472 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 5cc40c781122647c9183f194a1e7dd79305e7bac Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 26 Mar 2022 07:39:25 +0000 Subject: [PATCH 0115/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@df88e2632278a0eafab4ff8a96bd7643d8695380?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6ab961472..c538cec3c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From dbca9e1e0186803b5dfbedc655a68ed9da252270 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 29 Mar 2022 05:08:11 +0000 Subject: [PATCH 0116/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a22517b6e12b4dd51803c3871e42681f8379e223?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c538cec3c..970264c19 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From c4bf9d9847bf85507fa68a707bcd1dce04a215c6 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 30 Mar 2022 02:55:39 +0000 Subject: [PATCH 0117/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ee7a3eb79cf146abe0cff44181d1e971a5cbe278?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 970264c19..a200e98d3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From c9db93f1c7b135fd78f053a722c6d0f1bf7966f5 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 30 Mar 2022 03:01:12 +0000 Subject: [PATCH 0118/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@dbbfc97a00f27ece29fd9593dd0ddb3705b233c9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a200e98d3..9bd17f1eb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 341dac5ceb9da94d18788a97ee69ddf1b945d384 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 31 Mar 2022 11:26:56 +0000 Subject: [PATCH 0119/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2be3ed61ab9929e05f38ff78c8a22629800e9620?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9bd17f1eb..41ce7d56f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 73a27e15140089b683dd1e5cce693559868c79e7 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 31 Mar 2022 23:11:46 +0000 Subject: [PATCH 0120/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e8fa53003d55ce0e164cbafd63c8c1f0ce7fd7da?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 41ce7d56f..23eb125a4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From fd4f682658da57f464c1a38db3d68eb6174675ae Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 1 Apr 2022 00:40:42 +0000 Subject: [PATCH 0121/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2241a4dae3261e38c35e9ceafa75adc13dea565f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 23eb125a4..f124a5823 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From b32d716708135575aec0c2cc2492f6ba68289d59 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 1 Apr 2022 01:33:36 +0000 Subject: [PATCH 0122/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f0985bc9fd240f61852423f3fc410ff7cbb479c1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f124a5823..da8cf446a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 29c38f9a03e94378e0b3cdf46382224853c03a76 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 1 Apr 2022 01:48:37 +0000 Subject: [PATCH 0123/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@87b7b72b5464f8377736139e760902cdec8056ed?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index da8cf446a..34fa5964c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From ffa89a6d8d7a52b97f96f3988336a6de3328c07c Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 1 Apr 2022 02:48:48 +0000 Subject: [PATCH 0124/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4c91244020d8e0b6a35d7d5d9915dc4399e27c24?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 34fa5964c..604cc6c06 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 1acaa2a5f6dafba6262355ab365a594041f413e6 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 2 Apr 2022 05:20:52 +0000 Subject: [PATCH 0125/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@99e1c2f49409fb01abffda749a084b49ba24ce7b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 604cc6c06..6e376f779 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 737020524d49253ac9d16ddfbdd4055c432324fc Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 2 Apr 2022 05:50:30 +0000 Subject: [PATCH 0126/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f2676f1fde9f95cbc99490e8ab87b0736e45c689?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6e376f779..9bf20143a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From d6bf3dd50aab7eed6a4a46fd539eb7e21cbf33d9 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 2 Apr 2022 06:06:53 +0000 Subject: [PATCH 0127/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@29bcb297727e2aa1efa5f7b9455485ab63b5f27d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9bf20143a..d6a275cbf 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From b779f5755c9ecb2c59148fe3d59029b84af79a2a Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 4 Apr 2022 02:41:33 +0000 Subject: [PATCH 0128/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7ba339331e87ea26a46401d4159c67d62645761e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d6a275cbf..be29fa4fc 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From ab886879cff0c6d1ec7dcd44fc3fc9e69eb167b4 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 4 Apr 2022 10:29:38 +0000 Subject: [PATCH 0129/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0f5c4ca6efab8c47a4eb6d5f915bf70472952a4e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index be29fa4fc..e739c843d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From f91c7a1fb3eeb645e7e6774cd20ebc9f5af270c4 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 4 Apr 2022 11:08:45 +0000 Subject: [PATCH 0130/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7e7ff27d075e88d1a4ecdabe48102e2edb9bb7f4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e739c843d..9f51a6593 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From db0a4e1623478d277597820bc12ade87f66a6a9a Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Apr 2022 01:30:15 +0000 Subject: [PATCH 0131/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@565de4ffe5013361fe146585885c6b34cf76ad7d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9f51a6593..cad713a47 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From b1e8f4e050a5d65d9408a363be950cd99f8d6ee5 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Apr 2022 01:59:48 +0000 Subject: [PATCH 0132/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c9904767432e742dd042c97b27fc6de5f2930b7e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cad713a47..2a51a0864 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From d6dfa021ab68b4dfbb46c9d45a9a83427419319b Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Apr 2022 02:17:46 +0000 Subject: [PATCH 0133/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ae518eb7e70d32f180c9aa8c28f3351054575fce?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2a51a0864..6fcc9e8d8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 7f61199ebd9ed9a18a05fbce08721697f162c09a Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Apr 2022 02:30:57 +0000 Subject: [PATCH 0134/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f3b7e25d672f8a3882f7576c6ff8d17577e76aac?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6fcc9e8d8..0b419b1e6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 682c7bd2725c2442c4a205be4d89e75f7e2a806a Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Apr 2022 02:45:27 +0000 Subject: [PATCH 0135/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b2c9039cdecfda1d048a202ba2e84634a6d8d3e1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0b419b1e6..1809ecc55 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 7aa1868179874ad62f6593bcadc33c282fbbaac3 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Apr 2022 03:00:52 +0000 Subject: [PATCH 0136/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@10f44dede81a3313d1e071f81af0a94dd8f24e5e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1809ecc55..52f4cb743 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From df837b96fd110f87e253417c1e84d9c4448e6ebd Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Apr 2022 03:19:37 +0000 Subject: [PATCH 0137/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9ba47e7e86224a830b389d2dbcc20dc0ac43a042?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 52f4cb743..a9e010fcf 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 8b2a57ec28f7fc3c6e6ce0bb948a7d672ba235db Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Apr 2022 03:58:49 +0000 Subject: [PATCH 0138/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5991b15b2dd83a6ea09a28fc1cf7bf23cc47178f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a9e010fcf..f1f02dd2b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From ca2d8c0d8b687b499a09be2515a18811ae6d5dd5 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Apr 2022 08:55:15 +0000 Subject: [PATCH 0139/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@089ee6b3664d0ad4e05bbede13c2e6c073731e6e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f1f02dd2b..8078fefb8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 76483982c2823cb607b1876737bad70c0bde93d9 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 7 Apr 2022 00:39:45 +0000 Subject: [PATCH 0140/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e2d9318d67610561157f77b0c1302b3347caaca1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8078fefb8..4f061d1de 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 56d86c6c6bc12b4bc4b0cf9731dfcd9807ce7d3b Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 7 Apr 2022 00:50:17 +0000 Subject: [PATCH 0141/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1ec54ef5b0ec4b16d08e2124c7c4c7467f9cee57?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4f061d1de..2ef79b7a1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 7199dbafff3d842efd7870dea7d5de101e5f23bf Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 7 Apr 2022 01:00:38 +0000 Subject: [PATCH 0142/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f03840285786317de476a2e5f760f706242f5bf0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2ef79b7a1..e513dcefe 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 2a877e2a2eb4725f350db5948b73a140865b3abe Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 7 Apr 2022 01:25:39 +0000 Subject: [PATCH 0143/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c22daad790cae38fc63a8b700c759dc36aa43adc?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e513dcefe..6ba1857be 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 6d09205fc1854517e8f47a7bc7f265781e962742 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 7 Apr 2022 01:31:33 +0000 Subject: [PATCH 0144/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e733b5ff5e4aedfc9ddc832817b88250dd5967b8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6ba1857be..a80c6f68a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From e7f8b29e2cac9f4d98cb3bd845efd22970bd077c Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 7 Apr 2022 01:51:53 +0000 Subject: [PATCH 0145/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b9d8e99dbe3aed15318d77960a71bb9718934e06?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a80c6f68a..a04ffaa11 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 7832c3910ed4e1cf97f58a8ab508c6399a0b4bd6 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 8 Apr 2022 02:01:07 +0000 Subject: [PATCH 0146/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2424a7a3b7766d8e2829a24c513d211792182c53?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a04ffaa11..2bdf37b75 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 7c4112aec269ef9fbaca1b580b62c02732b98c87 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 8 Apr 2022 02:03:28 +0000 Subject: [PATCH 0147/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b68c344fd9651b6a0214e32ba2054bcaeae24fff?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2bdf37b75..30ba525f0 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From ed1e328d520f3b8edd39df0cbe0992f0d297c4dc Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 8 Apr 2022 02:04:25 +0000 Subject: [PATCH 0148/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a23a9827b9227f25f76f5fc2bc9e7e0385c35747?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 30ba525f0..680fffe8c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From b543a383a14b0bf11e2a93307a400a29371c8f80 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 8 Apr 2022 02:05:20 +0000 Subject: [PATCH 0149/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@71660dc5816c50dae21833a44c8dc9cc2c0fedb9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 680fffe8c..19a81425c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 48912f16410cb4185b855aefd30c2adc062844ce Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 8 Apr 2022 03:06:44 +0000 Subject: [PATCH 0150/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e75909e586b444312fdcaa8d003761c41cc91ec8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 19a81425c..aac10c15a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 1faaec03762dcf2eb3c3087ca2e70574c0f69067 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 8 Apr 2022 05:18:40 +0000 Subject: [PATCH 0151/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ef73a80d59ec3f44ba607fcf43858aca7aec5d82?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index aac10c15a..a497b8517 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 73d46614f83aa27fc3cf6979293d9be515688cc0 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 8 Apr 2022 05:49:12 +0000 Subject: [PATCH 0152/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8a06976ec6dd1d2438d309cc9f2f200b80e6050f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a497b8517..9567da97c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From c670a1a760d2299dfbae1c448223cd510dac21c4 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 9 Apr 2022 04:26:01 +0000 Subject: [PATCH 0153/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@02086aaf5355c20ca5031239e8c1acbec6764eba?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9567da97c..858adc021 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From ec88e5287678befb4b5654bb2f4523a709b84e8c Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 9 Apr 2022 04:26:57 +0000 Subject: [PATCH 0154/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@294bc93dc4dc6f8d3c6747366ab04d47170baa7b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 858adc021..498bf8dd0 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 00c46962c325e6743607faa788cf200d13487948 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 11 Apr 2022 08:35:59 +0000 Subject: [PATCH 0155/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@171813b040bff401b8bab98ec92dc5c42d73dc6a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 498bf8dd0..62ddbe160 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From fb0de6c502ee514e69db36eb8d307b3f2212a0b1 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 12 Apr 2022 02:48:30 +0000 Subject: [PATCH 0156/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@921d2f6e16cd3fc10b086c6b45922b2a4208556e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 62ddbe160..8a1e708cf 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -803,10 +803,7 @@

    Get the source

    terminal commands
    -
    git clone https://github.com/Convex-Dev/convex.git
    -cd convex
    -git checkout develop
    -git pull
    +
    git clone https://github.com/Convex-Dev/convex.git --branch develop
    @@ -819,7 +816,7 @@

    Convex Projects

    convex-benchmaks
    -

    Run benchmarks on the convex network.

    +

    Run benchmarks on the Convex network.

    convex-cli
    @@ -827,15 +824,15 @@

    Convex Projects

    convex-core
    -

    The main convex core library.

    +

    The main Convex core library.

    convex-gui
    -

    A local convex network running as a GUI application.

    +

    A local Convex network running as a GUI application.

    convex-peer
    -

    Peer library used to run convex peers.

    +

    Peer library used to run Convex peers.

    @@ -2385,7 +2382,7 @@

    Transactions

    From d746af1d81b7cbb2d25a40ce1242d57f67b064c2 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 12 Apr 2022 09:21:46 +0000 Subject: [PATCH 0157/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f6e8ec367e79100277cda93f3fc9238721adc48f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8a1e708cf..741f89c02 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5a571a25e13f89706cf5579e0196d85ec90e87fe Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 15 Apr 2022 12:43:56 +0000 Subject: [PATCH 0158/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8c4d87ebf95eebc6a44a83413397724763f87504?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 741f89c02..dc823a28f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 31ef17b1b7e21fa4574fc3090a9c412f1417f1eb Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 17 Apr 2022 06:05:29 +0000 Subject: [PATCH 0159/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@00487c8e163d17ebed68e192452f8196abb23647?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dc823a28f..4f233ea3a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4b9c825d5fb3b633e09846690b3975ae1f1e2a21 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 18 Apr 2022 07:47:11 +0000 Subject: [PATCH 0160/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cf5aeedc5d030a02dff9fa19d29efdc47b06ad3c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4f233ea3a..b1aebdf95 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -803,7 +803,10 @@

    Get the source

    terminal commands
    -
    git clone https://github.com/Convex-Dev/convex.git --branch develop
    +
    git clone https://github.com/Convex-Dev/convex.git
    +cd convex
    +git checkout develop
    +git pull
    @@ -816,7 +819,7 @@

    Convex Projects

    convex-benchmaks
    -

    Run benchmarks on the Convex network.

    +

    Run benchmarks on the convex network.

    convex-cli
    @@ -824,15 +827,15 @@

    Convex Projects

    convex-core
    -

    The main Convex core library.

    +

    The main convex core library.

    convex-gui
    -

    A local Convex network running as a GUI application.

    +

    A local convex network running as a GUI application.

    convex-peer
    -

    Peer library used to run Convex peers.

    +

    Peer library used to run convex peers.

    @@ -2382,7 +2385,7 @@

    Transactions

    From 77a3972d1842f1a7e0dae2170173341b34f30908 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 18 Apr 2022 07:54:00 +0000 Subject: [PATCH 0161/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7e11791d2aa78d824f508964d1af10c7f780e125?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b1aebdf95..4c850a868 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2385,7 +2385,7 @@

    Transactions

    From 44d8c410e457bc47f88fbcf381f9947086a02bcb Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 18 Apr 2022 08:39:13 +0000 Subject: [PATCH 0162/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@de7dfb3a157fa1977925d48d6f301139f26ed9b1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4c850a868..c95b8dddb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -803,10 +803,7 @@

    Get the source

    terminal commands
    -
    git clone https://github.com/Convex-Dev/convex.git
    -cd convex
    -git checkout develop
    -git pull
    +
    git clone https://github.com/Convex-Dev/convex.git --branch develop
    @@ -819,7 +816,7 @@

    Convex Projects

    convex-benchmaks
    -

    Run benchmarks on the convex network.

    +

    Run benchmarks on the Convex network.

    convex-cli
    @@ -827,15 +824,15 @@

    Convex Projects

    convex-core
    -

    The main convex core library.

    +

    The main Convex core library.

    convex-gui
    -

    A local convex network running as a GUI application.

    +

    A local Convex network running as a GUI application.

    convex-peer
    -

    Peer library used to run convex peers.

    +

    Peer library used to run Convex peers.

    @@ -2385,7 +2382,7 @@

    Transactions

    From 95263719811c6ba55ad677c232e20c03b24e1381 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 25 Apr 2022 07:56:29 +0000 Subject: [PATCH 0163/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@53d57e9e84a01d2eca044a37aff19b1381c13558?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c95b8dddb..80e2220f6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 86d4a37fcdecfe456c879dff32c7634b61609837 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 25 Apr 2022 12:08:14 +0000 Subject: [PATCH 0164/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4c6775a11aa0ef6e0e83c0e1cb86811036feb1f4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 80e2220f6..b4297f913 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 436244d6b6baeb62ec74bfff62042283302c05f9 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 29 Apr 2022 07:48:44 +0000 Subject: [PATCH 0165/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@58f1ddbf588d9630c27e23a26b04a66a5133ad9e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b4297f913..c7619d7d4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5bf92126e48f0ede0130656c0e9d56eaf8427605 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 29 Apr 2022 09:25:59 +0000 Subject: [PATCH 0166/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a3f811b6be5f4869f4ebc60d2b49cf930028479e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c7619d7d4..ab7e1d69b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2417b66924ea2830fe8b7e6e6971798c9509bc35 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 2 May 2022 07:16:41 +0000 Subject: [PATCH 0167/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@065c32b284e99e13e7b3006d2c5623be98eed975?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ab7e1d69b..c703974e8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 862f1f17c83033263006a56338a2c5b853b2a710 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 4 May 2022 23:59:24 +0000 Subject: [PATCH 0168/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8c1d71e7d2094b044792f0579cd974bce99d8725?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c703974e8..e3ccb5812 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 85ed4fde6383b755c57fa61c17aed22cef1e012b Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 5 May 2022 20:37:23 +0000 Subject: [PATCH 0169/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8670cd8683731e779fd4876fb721535188b213f7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e3ccb5812..fe0d9c34e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From dac280b2656e88206517d643b2117cc958533829 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 11 May 2022 06:15:12 +0000 Subject: [PATCH 0170/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cda231e0e3f07169c3ad5ed1213d17d4b762a989?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fe0d9c34e..5e14f2511 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From e40d146ca42de9e996d884fc848e0503edbc6c8f Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 16 May 2022 01:17:01 +0000 Subject: [PATCH 0171/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f2ea7c8db40444ae67f22b6687fe800d5f105b73?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5e14f2511..78f13172c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 432c2264143285c722ce07b11f26f5762bbdbac7 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 24 May 2022 07:45:37 +0000 Subject: [PATCH 0172/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@61d290050bf75b2ea7d371248d026c5f4a4c9c2d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 78f13172c..00817cd02 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2f0c283664f28866d2d42cfb6916abfb3d8170ae Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 24 May 2022 08:21:40 +0000 Subject: [PATCH 0173/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@72a1ae4c3864fb4bfeab1bedd8ed8e51471eb93b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 00817cd02..df6374025 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 129bbeda553275d626fc178046c78a9000433063 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 26 May 2022 16:28:34 +0000 Subject: [PATCH 0174/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@31f82ad77333f9fd46bd1fd6c8a7ac9a9ebaa107?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index df6374025..1e7f5b79f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 65cf565d36b390fc82a7c142ea8675417911e8b1 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 26 May 2022 17:12:11 +0000 Subject: [PATCH 0175/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cca5fc01774453c803773dc54ee8ec75f651dabe?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1e7f5b79f..3343d76e6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 715166433b33907bc2ccda7ec9218ef9ff30679e Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 3 Jun 2022 07:52:31 +0000 Subject: [PATCH 0176/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@94b552192ea8db1508560ec23f52ae5101357d8c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3343d76e6..d6003e9a6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 10e44d11d71fb8b6384543b6bf66b2c47788f159 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Jun 2022 07:04:01 +0000 Subject: [PATCH 0177/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@aef78e6550b492090913d9e30a2d4e33a9a11410?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d6003e9a6..1664a871e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f8aa636cae2b0b30d2888ef475aec27219953399 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 13 Jun 2022 05:16:17 +0000 Subject: [PATCH 0178/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@75a33e9213a0fa84949caf731fe5195e86e8630e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1664a871e..f9f020d0b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 1f6384ec8158bc2baaf3aac9a8e5e31ae6d314cc Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 13 Jun 2022 05:23:59 +0000 Subject: [PATCH 0179/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@148a8ad22b89145deb85988d3c287850e95b858f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f9f020d0b..ddcadc788 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b9aa05638dc314e4b43efaf8c1bafea7280b128c Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 13 Jun 2022 07:25:53 +0000 Subject: [PATCH 0180/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f14c5381ba540fbb4bba94bbc38eb437a99e1cac?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ddcadc788..40080192a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7a9e55ae115776c715124fa05a9ff59fc02b0dfe Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 13 Jun 2022 15:37:40 +0000 Subject: [PATCH 0181/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@72a1ae4c3864fb4bfeab1bedd8ed8e51471eb93b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 40080192a..67d356bb8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b28d19ae81d6834a28841928e5f376356dffba1c Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 14 Jun 2022 11:07:43 +0000 Subject: [PATCH 0182/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e3827d6691ba274649285741bfbf801dd10de3ec?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 67d356bb8..ffec090a4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From e1268227a3d5e2e367c8a1133cbd99476e42e786 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 15 Jun 2022 09:40:05 +0000 Subject: [PATCH 0183/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@77a1df812b421d1efd50a4b2e9fade18f50f41a8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ffec090a4..79fd8b1b3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b4d4054b3118adea5bb4fdad0dcf1e7a94374465 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 16 Jun 2022 10:38:40 +0000 Subject: [PATCH 0184/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3de8674d218eb6c092ac73d5538b743c65f21e25?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 79fd8b1b3..e3e71429d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c12b147d7684eecb81b08bfbbd4cbb8da97250a5 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 16 Jun 2022 15:31:50 +0000 Subject: [PATCH 0185/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1c88a3996d488c5edaed4f2c2fea03d375b77fc0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e3e71429d..64791615b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 440cd0de88e3d67bc9688094c6a82c67269debeb Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 20 Jun 2022 07:31:25 +0000 Subject: [PATCH 0186/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ea45c9fd0befef521f07787b09e63a4cc252df54?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 64791615b..cfcf42375 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d1715521ef7233016719053c1d7336a9e3e7d301 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 20 Jun 2022 09:05:31 +0000 Subject: [PATCH 0187/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@80abcb68c62d65e43103bcd2c53be6f4b07f6373?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cfcf42375..610384b0c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 0eaf3dbf88b95f44697e9e3d53858a46825bdca9 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 21 Jun 2022 08:37:26 +0000 Subject: [PATCH 0188/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@95f3feb3ca66f7f965306041efa44fb092100bf7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 610384b0c..a351af44e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From a1edff21d74f67a95647cf7029eabdb87915ee68 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 21 Jun 2022 08:56:25 +0000 Subject: [PATCH 0189/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f8f6a9fbef15f8537ae5d8e2448b79f149629916?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a351af44e..5b5085ecf 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 77129dc30abe45839ebaaaef5a1924cdaeb709cb Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 21 Jun 2022 09:37:14 +0000 Subject: [PATCH 0190/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ae192817b3921f67c0612fa715fa9217a3196b2c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5b5085ecf..055b86992 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 9b919fd0a2bed720ab62dcb4a574b231bb5db7d8 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 21 Jun 2022 16:55:06 +0000 Subject: [PATCH 0191/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@918845372880383cf57f61ba3d5bbeca102a497d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 055b86992..fc775f89e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From fa8bc9081abc574cac3f211931ab6132a5fece16 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 28 Jun 2022 17:02:32 +0000 Subject: [PATCH 0192/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@df6c6d71f301d30ea00369b97f0bd0c7d138d7c3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fc775f89e..fc3249080 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 8609bce0d93765a313d4dff448c82bdac906e00a Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 29 Jun 2022 05:44:53 +0000 Subject: [PATCH 0193/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@15bb34c50c036eee56a7559e9d92407e0529f383?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fc3249080..b0a09970e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From a89601461f38ada75c7686201f16091a89be29c4 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 29 Jun 2022 06:31:54 +0000 Subject: [PATCH 0194/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cabf58d4b57beabedc9607046795cddeb4cfc843?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b0a09970e..e1c281d00 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 3a9c435de71391a5fd11d80a4b756dfcaef65735 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 29 Jun 2022 06:32:57 +0000 Subject: [PATCH 0195/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bbf137b1da3b934972989aa0db00bfda74e7d613?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e1c281d00..12a976f8a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 67a7ea16c79ac07bd4ff2615e4f1d9079192e7a6 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 4 Jul 2022 09:24:36 +0000 Subject: [PATCH 0196/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4f2932cc570184b031e29b0f80ca82cccc455f09?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 12a976f8a..4116dd8cf 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From a560029162094a8ae7c1b7287a824e154048d4b4 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 4 Jul 2022 14:26:58 +0000 Subject: [PATCH 0197/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f8a72ecea2fdf40efd40a3808f7b4b57bef41bbf?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4116dd8cf..82cf3db2a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7b68ebff0036094e5ff76a07ba1e4297cd280540 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 4 Jul 2022 14:53:24 +0000 Subject: [PATCH 0198/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a93c8c81b77970d7ab7eb3e1ea8cffce08952749?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 82cf3db2a..63dadf856 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b4d18751816a2d101098a0cf37293c72cd4e16df Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Jul 2022 16:14:56 +0000 Subject: [PATCH 0199/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@774b3f4cf44e323356b8018801057e96d762e779?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 63dadf856..c906240a7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f746550159128c1bf33672b30d2e6a2e05e12823 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Jul 2022 16:24:16 +0000 Subject: [PATCH 0200/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@41072d0df2373c99b0384fa656f80f89b94370c1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c906240a7..90b076994 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 078442963f4a1fe68cbbd2b0ee4c9ed972689111 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Jul 2022 21:09:47 +0000 Subject: [PATCH 0201/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@974684c08dce5f1a3f6ef3ae36f30d4722e96a0c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 90b076994..3249e0027 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4e3c59e8d847fb440861da1970468a0ff1f8bad6 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 12 Jul 2022 16:21:00 +0000 Subject: [PATCH 0202/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8948ec9a0b150ab9c72e334d5ab24e5ae298c439?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3249e0027..06fba4ee4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7d54b407ebc48d7fd0aef5458cc726b48301424f Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 12 Jul 2022 19:48:34 +0000 Subject: [PATCH 0203/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@627264f2ff315fbc60ec6147ee75d8922697b371?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 06fba4ee4..4da06b4ed 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 38e07ba240c8f9222c39eed3da95c71a91ce8e17 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 22 Jul 2022 17:16:32 +0000 Subject: [PATCH 0204/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d3bfeaa76e8303d6d38b2a0992f3a938adc3a5bc?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4da06b4ed..596867c55 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From db90a321eeb990ed2b5b3dd3dc394c85be8d3a4e Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 22 Jul 2022 17:29:43 +0000 Subject: [PATCH 0205/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c5ef4731e887f95fad6ecf5124373085fda969ae?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 596867c55..d02acfb1e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4a19237316b55b28390856d406af29a3bac56fbe Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 1 Aug 2022 10:06:06 +0000 Subject: [PATCH 0206/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b6703f4bdb87d41727d9ea5d3adc5b6b15c527ba?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d02acfb1e..67011b5cb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b06c7103cc8d469fe9720ab5df439a8c42dee42c Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 3 Aug 2022 08:16:18 +0000 Subject: [PATCH 0207/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9acbb65138f21209db8df8b7e5b35243e1a27467?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 67011b5cb..3553f0115 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 10f3140a2e354da3edb0397968e32648250730a1 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 3 Aug 2022 08:49:34 +0000 Subject: [PATCH 0208/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d592580f0c9051fe889fa17bb25da2816019c77a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3553f0115..906926c4e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 02a8343b9f25a87f864afd917e8b3abe07286ea0 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 4 Aug 2022 13:43:34 +0000 Subject: [PATCH 0209/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@afbaf030a70fc3ad761346aac9261b8e117d50c4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 906926c4e..50eb7ff25 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 80ef514789e7c853dabfdf31c4072a8fc630ece4 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 5 Aug 2022 07:55:55 +0000 Subject: [PATCH 0210/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cf62cddbcae29efc1c2178444ccdd400760915de?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 50eb7ff25..dd9256c44 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2037aea996d3df10425f2a19531c2abdbebecf1a Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 5 Aug 2022 07:57:51 +0000 Subject: [PATCH 0211/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@077a5876b5ef7b70afbe3589c3e9a5b5bf5a2d38?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dd9256c44..04c288b64 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 15a3d8f37c03041d349b1652fac21cd665b1e377 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 5 Aug 2022 11:24:00 +0000 Subject: [PATCH 0212/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f198bc9e36ce74873f3ae131d56ae94dbd11a06b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 04c288b64..d856029c7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d616e07bf0a16bf4cba5c99a7fb1fdfe1173b032 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 22 Aug 2022 18:27:22 +0000 Subject: [PATCH 0213/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a79df4b8dc615fc789e7ec1dedad911cda755354?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d856029c7..2d8d9d5bb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From aff9e090992bf13c565250d81c315d1eac838aee Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Tue, 23 Aug 2022 16:15:46 +0000 Subject: [PATCH 0214/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@267cad68b086e03f3665b040fb277ffe4aaa767d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2d8d9d5bb..687a1c66b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 1b9dd04d8a0b0031f30a5f874ed4ffe702d7a22d Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Tue, 23 Aug 2022 17:48:58 +0000 Subject: [PATCH 0215/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7ad850b0e28277d725ac0053e71acc904da28591?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 687a1c66b..a3ee83fb5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2ef2434a2c2c22af6bc039325adccb68c50e0abc Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 26 Aug 2022 14:43:23 +0000 Subject: [PATCH 0216/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8d3e0423b4ae3ed6b61203b4a4cfe8eee483da4c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a3ee83fb5..4e419f95b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5389c0dffce9a3818d2a8c540e2d5d56b3ea1784 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 26 Aug 2022 20:32:37 +0000 Subject: [PATCH 0217/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@71286d348b209d3ea897651826bc477570a8a650?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4e419f95b..13fe4c19e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 53fbf66a0212456c787566d4e7f3c27e54f95aa6 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Fri, 26 Aug 2022 20:32:48 +0000 Subject: [PATCH 0218/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@35250057e09f1c59e630f04d8491204cdb6d5f3f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 13fe4c19e..67ce3e314 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2acb7a4c75553544567a0dbf5e4119a9887ed660 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Fri, 26 Aug 2022 20:48:19 +0000 Subject: [PATCH 0219/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@65cc9625ec79acd341e5dd7efc95c412fbc95357?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 67ce3e314..b08d7bc5c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 1076c68594637332aca5658d70725ced718ca6cb Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sat, 27 Aug 2022 04:12:45 +0000 Subject: [PATCH 0220/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1f136ef18d3ce41a761d0f3dc524e692a2414c30?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b08d7bc5c..4e813f355 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 0f0618a30d074bcbc6fbba84648aae32ea265a9a Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 29 Aug 2022 08:27:42 +0000 Subject: [PATCH 0221/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a1bea81cc30d0cf80bca3a5b76757544fd1ef95c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4e813f355..f1f3d362e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ce179389c96de917c644d6b9526a0841caf9c813 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Thu, 1 Sep 2022 08:38:17 +0000 Subject: [PATCH 0222/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0fac33ca89b08fdf175ee7ee132312d5c50e199b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f1f3d362e..447b98bab 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c85ace0cfc2b7371cd1213d5c1e119db0d53e276 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sat, 3 Sep 2022 09:42:17 +0000 Subject: [PATCH 0223/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bbfa849e03259296b65123e1db2a0a0473ebb75a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 447b98bab..9179f620e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 9ceb90da174bf24885ad59b718921b07eefa7d8f Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sat, 3 Sep 2022 10:05:31 +0000 Subject: [PATCH 0224/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0f061f68f14ed67ea49aacae66685b0dcc357616?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9179f620e..b6d7dc677 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 0216d4922e84b5c12f25b86738544085ebfcbad7 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sat, 3 Sep 2022 10:31:04 +0000 Subject: [PATCH 0225/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9c48e0927d5852e8f27856058200102b8a7cea07?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b6d7dc677..1456e31ba 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2ce77e2e3c647f3069f27c22cd5b1da48e341f7d Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sat, 3 Sep 2022 10:56:32 +0000 Subject: [PATCH 0226/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@820a0a005ce4b4447b015cf404dc81312a4318e6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1456e31ba..1f002c94d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 99e3b2c1be6fececa8ab10e5e645ad563bca09b4 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sat, 3 Sep 2022 16:54:26 +0000 Subject: [PATCH 0227/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7feff3f7c375237af7e6ccb21fb22956a16e740d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1f002c94d..91e2e26ca 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 8b5099b085080db4662eb20d2cfdaeacf58d680c Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sun, 4 Sep 2022 06:14:27 +0000 Subject: [PATCH 0228/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4d54ec39aad8ddba8eddac24d1981b7f3ae5c375?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 91e2e26ca..ec4761549 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 9463d75a619e7d9c3729c9a38a2323a8ec979828 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sun, 4 Sep 2022 08:24:42 +0000 Subject: [PATCH 0229/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8c23130f77e20ae0a3ecbc4836e48c8a2b07a2ac?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ec4761549..fc6460ae5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 6364df0afe05d86cc31e5ee0c95814d4567fbef8 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 5 Sep 2022 10:45:25 +0000 Subject: [PATCH 0230/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@47c7c97501e6d870aec02ea4cd40dc997bffdefb?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fc6460ae5..75fe075ba 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f4a4ab07f35594f50afd3b20417322509b34be53 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 5 Sep 2022 10:48:32 +0000 Subject: [PATCH 0231/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@32a14e2de2c418b45d8943e8baed78309301ee75?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 75fe075ba..ac6e8c5b3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5225b3e981c5c0f6e180bba89872f80020f51306 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 5 Sep 2022 10:50:16 +0000 Subject: [PATCH 0232/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5e69fa5cb12a2f173f7369d2dfd1a26d8aded0c3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ac6e8c5b3..b6a9f1278 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 1bbf73523d42bd59a081f9b5d7c4a3bd67214fd8 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 5 Sep 2022 14:56:36 +0000 Subject: [PATCH 0233/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3aece03b20db2c3177933353f3a75c997e22bfed?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b6a9f1278..6d68c07bd 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 535ac95d15b440816847b3a73ae5b9163cff52c9 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 5 Sep 2022 22:55:59 +0000 Subject: [PATCH 0234/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@23388d56f2340be3d8d5ea8f799c28f72447c7dd?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6d68c07bd..b6c662fd9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From cc729ff47ae3d4281b173b498dc4f2c84b0defde Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 5 Sep 2022 23:13:25 +0000 Subject: [PATCH 0235/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@913d9fc9c7862546a08282146dc24bce97ae29cb?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b6c662fd9..e5e17dcae 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From e8773126e1ee6f305781177adb45bcfad36a7b48 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 5 Sep 2022 23:13:32 +0000 Subject: [PATCH 0236/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@913d9fc9c7862546a08282146dc24bce97ae29cb?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e5e17dcae..1fcf7af9b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 8472e6f209e86863444786e898ec4828bafc785e Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Tue, 6 Sep 2022 13:51:41 +0000 Subject: [PATCH 0237/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4e422e1454857a0021124cb2251cf9cbf0d227c1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1fcf7af9b..fd2238481 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 8af633098b9d76befb40d8ebbda62f934ae40f11 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Tue, 6 Sep 2022 13:53:13 +0000 Subject: [PATCH 0238/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c7864b8051f1b2e03ef0cca92a4e08b7cb2047c6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fd2238481..fa1975a25 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 16a38e9e0485b762c2ea696499fe0b4b942950f3 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Tue, 6 Sep 2022 14:03:39 +0000 Subject: [PATCH 0239/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a5be0cb0a93f3c949761e7d39abd72da5142e1b7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fa1975a25..d82f5947d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 0b9f34fa5904292b3f7e73f187a223587cfde9fa Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Tue, 13 Sep 2022 16:49:47 +0000 Subject: [PATCH 0240/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c840a5c000db448790884e0f12304f4a1b8147f1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d82f5947d..f3093b3a2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 6945083dd68c66d8d17675ae904bae4dbfcf48f6 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Tue, 13 Sep 2022 17:40:02 +0000 Subject: [PATCH 0241/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b41e0ef878caa23dd7546a024a38ee220b65a256?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f3093b3a2..05070285a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 933c3a930a7dc16e5f2ca87dd6802fe007089def Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Tue, 13 Sep 2022 17:40:16 +0000 Subject: [PATCH 0242/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b41e0ef878caa23dd7546a024a38ee220b65a256?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 05070285a..dfc97c80f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 98ff7bd4024f079b1272425f9a7d997d3b633e75 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Tue, 13 Sep 2022 17:40:26 +0000 Subject: [PATCH 0243/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b41e0ef878caa23dd7546a024a38ee220b65a256?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dfc97c80f..4b0d8a159 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 099022b9211fe4b52b3c831363ba2f086e6ab428 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Tue, 13 Sep 2022 18:50:30 +0000 Subject: [PATCH 0244/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a4326bc2f7f6ca27ba748dcd91d9ca24daba3257?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4b0d8a159..7c2967d65 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 0ed51a4c0be9a32dd07456e0df1f9edbca1a44cd Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 20 Sep 2022 16:55:29 +0000 Subject: [PATCH 0245/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8e972e39dccaa3cd334666d8ccaa45111c755c7c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7c2967d65..2ff69d76b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ec76a8989f663a8885a7a6857d94118d30159e7f Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Wed, 21 Sep 2022 06:22:52 +0000 Subject: [PATCH 0246/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f2b1c70bc73bdb8e27b66b3c82781565848b34d4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2ff69d76b..0cb2aa346 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 36af197e83201be234083ae26c27259eead819bd Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Thu, 22 Sep 2022 00:37:55 +0000 Subject: [PATCH 0247/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e75c0f614e6a65df816c27d5f4eb382743b0746e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0cb2aa346..3f362da8f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2ddb750ddd235d46ff34bb806c0f65b0e26265db Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Thu, 22 Sep 2022 02:15:43 +0000 Subject: [PATCH 0248/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ee24a8ba67b20382f8dbc10e00a9af96d1eadb90?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3f362da8f..cdbae86eb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2ae2c796c4410714b9ec778b85056f4eb53b3822 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Thu, 22 Sep 2022 02:16:01 +0000 Subject: [PATCH 0249/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ee24a8ba67b20382f8dbc10e00a9af96d1eadb90?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cdbae86eb..8051759a7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 8c16b5aaac8a80e9a3d34e774c01bd2003e84693 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Thu, 22 Sep 2022 02:16:09 +0000 Subject: [PATCH 0250/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ee24a8ba67b20382f8dbc10e00a9af96d1eadb90?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8051759a7..8801d8e01 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From fdba3872447a4c0bac596107ee5e64f936e032a5 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sun, 25 Sep 2022 19:44:46 +0000 Subject: [PATCH 0251/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3a9ce158e3ea7764ee8f744a4196e65b6b05d2a9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8801d8e01..f1b95cc43 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4c9dfcbc03df46b3f5ae4e26a3a0ea356b8b59a0 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 25 Sep 2022 19:46:28 +0000 Subject: [PATCH 0252/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c17dfbb07a038885812d519a5ccb477ba3c983c6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f1b95cc43..335c1ea6b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4eb277dacc461e84b9ff90faf548d37296d02a1a Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sun, 25 Sep 2022 19:49:10 +0000 Subject: [PATCH 0253/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5bc618ef4ed6f4dafce887ce45cf8da45330c807?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 335c1ea6b..82769c468 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b5715b55dae5904bd8a97466a41d8a7944ef11ef Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sun, 25 Sep 2022 19:53:09 +0000 Subject: [PATCH 0254/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7919c49b6b92ba870d657b4d16e7d72a9e2c4a80?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 82769c468..4c6d38c11 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 51d6e3008d014e82a07cb55f22610c37ff3de690 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 25 Sep 2022 20:14:56 +0000 Subject: [PATCH 0255/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@85f14953f226ff787e9200d3bda3263815b709c9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4c6d38c11..aecefa283 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 39986bdf8bc3e8375432b30c5c4aacc96ab94d7a Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sun, 25 Sep 2022 20:54:02 +0000 Subject: [PATCH 0256/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@342df23674c69c099530936942f3430b46c41e85?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index aecefa283..5ae16f018 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 470e04540531ac695acf45aef48f586a89e3a417 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sun, 25 Sep 2022 21:05:48 +0000 Subject: [PATCH 0257/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d4c28f2959da8ec5bb3f7d3eabc40ca18edfe2df?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5ae16f018..3cc6bbffd 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 0da3ba6f6a2b048af23e57dc5d5cb598b4ceba17 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sun, 25 Sep 2022 23:52:39 +0000 Subject: [PATCH 0258/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@84b92450a1600e1da7cbf2dac576176bc42201de?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3cc6bbffd..87fdae389 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c665660b52c8d354e731595f979effe1ca70f428 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 26 Sep 2022 00:29:41 +0000 Subject: [PATCH 0259/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1c96237fd47e1fab7e29e41fefd4f0db93d1f660?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 87fdae389..6799d6f0a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c7aa2604284bf74f61da75f36e0f8cc8e02de8bc Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 26 Sep 2022 00:46:14 +0000 Subject: [PATCH 0260/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@42a2983e85670de4a848e41894a0d1cf1747faf8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6799d6f0a..c8c982e1c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 49eb2d7dd7e390d4109f7ea85783cafb432ff957 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 26 Sep 2022 00:51:21 +0000 Subject: [PATCH 0261/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6eebc8a40794c3a6d421b707672ed7b2ac9ba8b8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c8c982e1c..c5a0b8ab9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7d9cbe2b33832caae65bf4943ae62ac7527097c1 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 26 Sep 2022 01:31:20 +0000 Subject: [PATCH 0262/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@efbc09481bd49d4965835033d83619f64b5c360d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c5a0b8ab9..1f5083455 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From bfb679a0ebde3944e0742c88ed970d9c83f71de7 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 26 Sep 2022 16:08:22 +0000 Subject: [PATCH 0263/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c86100693e7fc04437ab701262310155f67d1d1b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1f5083455..6a0c40753 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 6f4dfe51d67e0c85f9d54f122dacb518fd5805de Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 10 Oct 2022 09:49:45 +0000 Subject: [PATCH 0264/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@39795e48b9072b770f2a4765fbba997da1807ae4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6a0c40753..da175b2b9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5571e6f98d1a039086d1a44059c269753cccc445 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Wed, 19 Oct 2022 13:36:30 +0000 Subject: [PATCH 0265/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@79113055e47d144f27b1d4d3c62c121b56605ad7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index da175b2b9..e281b438a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From aa821dd4883348d8b190dd752438d98f42a1948b Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 28 Oct 2022 12:36:39 +0000 Subject: [PATCH 0266/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@85a7ea722b349903e4a7d397b9861a4b4d5f0eef?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e281b438a..e61c10ee5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 767e6c8a8b1280b76c7735c081c328db62450eb7 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Fri, 28 Oct 2022 15:30:29 +0000 Subject: [PATCH 0267/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b3f281b4037200bb94a8ae646b6120f546a05bcc?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e61c10ee5..89128dca6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 809a09fe2617693484dfe1a18750f20103c70f2b Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Wed, 2 Nov 2022 09:46:17 +0000 Subject: [PATCH 0268/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@32315031e2bad2ff8b92e631a63de40afc8bcd27?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 89128dca6..f6c28cdcf 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 720cab2fd72ea7d0d5d55cd43449fcb8f4e32e37 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Wed, 2 Nov 2022 09:53:43 +0000 Subject: [PATCH 0269/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5c325fc5a564c99fa85d610a76b75c0f3474793d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f6c28cdcf..ab8852f79 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7fa7b7de1bbc5e96bf6ab0f4a5365b347827f6e2 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Fri, 4 Nov 2022 13:45:04 +0000 Subject: [PATCH 0270/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@fbf52a1c5a336fafafb57eec6963c9e0a8af3005?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ab8852f79..f81b5d939 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 39d4f939ce687638121e6b410a37b08604281457 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 6 Nov 2022 18:15:51 +0000 Subject: [PATCH 0271/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2693bd240d6bdcfbdd737344b6eb789493e047b1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f81b5d939..45c240ae7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ae29baf39a8db664879c806bcab72de7e04014a1 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Tue, 8 Nov 2022 07:58:58 +0000 Subject: [PATCH 0272/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@dde1896c8e42efb2ba078ad965ebe0785e920027?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 45c240ae7..295f06786 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7643447f841f08d71d8011822dd6ab94612980e3 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Tue, 8 Nov 2022 08:39:34 +0000 Subject: [PATCH 0273/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ca473ecdde802dcc8025514e2448c8011dbb9c8f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 295f06786..7d3042dcc 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7fae197c55196004fd05edd747a86ff53989903e Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Tue, 8 Nov 2022 08:42:48 +0000 Subject: [PATCH 0274/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6412c525d6e21f59fdf932daf6a686581396d7cc?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7d3042dcc..438c11bb3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c460d7df56d7c1afadd6190a14ee78f0108bcde4 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Tue, 8 Nov 2022 09:05:38 +0000 Subject: [PATCH 0275/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a9fadbf9e364b4a3b396539ce37a316ada394ab5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 438c11bb3..0c0a32a2e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From cffa40949737c7ce2ee39ec24c73427844dad10c Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 14 Nov 2022 13:27:26 +0000 Subject: [PATCH 0276/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cbc96d32ef7d1d60b29af87486eb625f41f7fc9f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0c0a32a2e..493263b53 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2fc8a76a9deee7d57a2786502ecac1f4c44d3414 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 14 Nov 2022 16:13:57 +0000 Subject: [PATCH 0277/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0532d4bd0663cf0bd619232802bcb7040e2c13fd?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 493263b53..aeacc61bf 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 9e003b96336beac39bea3f367d31e218f359f210 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 14 Nov 2022 16:47:50 +0000 Subject: [PATCH 0278/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e3ce865c43d77a9b100205dddfccc42393bd3f24?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index aeacc61bf..76053fc66 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From e6f8a67cef69b7c5b3a4edca7feb865da544c2a9 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Tue, 15 Nov 2022 16:44:19 +0000 Subject: [PATCH 0279/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ec2260b099bdca9b5aa6cbe9ebb7516b342d01d6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 76053fc66..5fac0d38a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7e3d9abdbfd156cd2c7616aed48e7b72e1019f3a Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sun, 20 Nov 2022 15:29:53 +0000 Subject: [PATCH 0280/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@508d6b0650c6fc5144fb476d8c5addda0ea23bd1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5fac0d38a..9a382b9d4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c5055e7a63dc191309390873a46204f5d0634d1b Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sun, 20 Nov 2022 18:32:59 +0000 Subject: [PATCH 0281/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@13ea891ce369dd1a3f015c1dd73e3c7d8600fd27?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9a382b9d4..8b80dd99b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 186d301d8fe8bdc8184ef4ba2a8bfac4982abaf1 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Mon, 21 Nov 2022 05:49:11 +0000 Subject: [PATCH 0282/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@78f35aa6e84d014a60ab3bdfa1f22e11519cae4a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8b80dd99b..2a4284a89 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From eb61ef148e2bae698b71e68996732f3484733f28 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Thu, 24 Nov 2022 11:41:09 +0000 Subject: [PATCH 0283/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@53a5301cd1c3d44b2f8738a48ee6690238da288e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2a4284a89..2e5425bdf 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 216f58a75c54fdaa55e315e2d4202bdfb0cbaa5c Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sat, 26 Nov 2022 04:20:12 +0000 Subject: [PATCH 0284/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@855b221dba28fe3789abc9ddda88c5d2a981574a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2e5425bdf..3b41946d8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d01694b7ed0afad5412731fd74a83678f5fc8dbd Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sat, 26 Nov 2022 12:21:23 +0000 Subject: [PATCH 0285/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1eced44a130f5182f625d9931557ad67b0b9f41d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3b41946d8..46e4691e9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 32be753e57fe5e94625f369b5e10fce9ec746d88 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 26 Nov 2022 12:25:43 +0000 Subject: [PATCH 0286/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@04cbeaaf20e315e580f566fd4a0e3ef55088e3eb?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 46e4691e9..3e80e6966 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c241c2b532834c6ee77c14e3c0a4a0152bc374b6 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 26 Nov 2022 12:41:48 +0000 Subject: [PATCH 0287/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@47ac4c6c5f5f204d30875838fd265e501f88c2bd?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3e80e6966..0423c0b86 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b331d0ceaa41b9f5d562e54bc586065c3fa56afb Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sat, 26 Nov 2022 12:43:23 +0000 Subject: [PATCH 0288/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4915a62d80109d1c5ddf5ebfc974eee28ffe2541?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0423c0b86..9171a1d70 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 68aa5bab3e7bec7f2570db9617701bb547ef4b99 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sat, 26 Nov 2022 13:13:52 +0000 Subject: [PATCH 0289/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@573db2cf5b1beb624c81970c2e2657d896eca08c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9171a1d70..e4840aecc 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f433f4064cad15051936011939794ed6735ac778 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sat, 26 Nov 2022 13:19:22 +0000 Subject: [PATCH 0290/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4f846db26460ac18f72ef4f3e6b12afafb12bd91?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e4840aecc..2f4b1d536 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d19da7f6d1173eb70f1ad6eea28cb18f45e990a8 Mon Sep 17 00:00:00 2001 From: kroezone <42369792+kroezone@users.noreply.github.com> Date: Sat, 26 Nov 2022 14:28:41 +0000 Subject: [PATCH 0291/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8442e98b3f52db49a520be331f533a299c07d660?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2f4b1d536..ddf1df884 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5b669fde2d8c9b8e1bd2524e24a5a0737bdf0d29 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 12 Dec 2022 15:30:33 +0000 Subject: [PATCH 0292/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4c3cae4f880104c40b02098a4819627651d63e2b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ddf1df884..4088514ff 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 963340665a094ccd6e5f1c8abd28967a0fdcd6f9 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 12 Dec 2022 15:39:26 +0000 Subject: [PATCH 0293/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@84bdf19f4c6932fb4769d346191a347932210712?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4088514ff..ffb44137a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7a98041dcbe0a068b803aae372bf6393eb760e63 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 12 Dec 2022 21:10:31 +0000 Subject: [PATCH 0294/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2bb103e9679ab51a5dfeef4f2f4841a4c852d7fa?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ffb44137a..4e44c8e2b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 67a4e1e1dd2006b43af75097cc9250cf1e4080bd Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 12 Dec 2022 21:40:54 +0000 Subject: [PATCH 0295/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@531185a0a56e069ddd2a7847dd1b7506dd82e084?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4e44c8e2b..cf16ebdc6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 3aa83a99b96dc126e1f59740eb06b3b15c73cc26 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 12 Dec 2022 22:01:21 +0000 Subject: [PATCH 0296/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f02219672d536adcf95713bdd5495f0c40ebd814?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cf16ebdc6..7c9630c41 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c04b0700a4cdee17b3ed91716ebdea2f8d601c0c Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 13 Dec 2022 16:38:07 +0000 Subject: [PATCH 0297/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@664e7dfbcd3da799969de232589db3160b15db11?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7c9630c41..1bd10d585 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 25de76b424990e658b4ca8695187444ef05b5cd4 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 13 Dec 2022 16:47:15 +0000 Subject: [PATCH 0298/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c17dfbb07a038885812d519a5ccb477ba3c983c6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1bd10d585..f6e10119e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From fb5c72025ab68b95b11a3c4b7787786e76624705 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 28 Dec 2022 11:07:38 +0000 Subject: [PATCH 0299/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5b90576be02b15847f3ed47f2afdecd714c353ce?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f6e10119e..8609f0b3a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c45fb83926aea822de63a523a80a660b2e5fe0be Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 28 Dec 2022 11:14:06 +0000 Subject: [PATCH 0300/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@081789c8d25ea8e6f08bd14bbf46f06c213092b1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8609f0b3a..25ad04271 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From dd59ff988661b6bbc848c998e9aaa5c345fe40e8 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 29 Dec 2022 11:31:35 +0000 Subject: [PATCH 0301/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@064bf40531ea8266ea28a352763840f44e2c1419?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 25ad04271..74476ac85 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d4ba628889585e2389b79914eabbf556638b2a27 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 8 Jan 2023 12:12:46 +0000 Subject: [PATCH 0302/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1185fe09a61a592c22eb5ed493c9639947e39aa8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 74476ac85..c3839c784 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 6e898de23d6b3f92e09abe2a536409318bd36625 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 12 Jan 2023 12:19:58 +0000 Subject: [PATCH 0303/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1ccca97abee43d0a22fb0e38fa27dcac8b98c5f5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c3839c784..2a57be757 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 02129f5bcc925c4fcadf330729bb65d4a5989335 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 16 Jan 2023 09:16:33 +0000 Subject: [PATCH 0304/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@36a3bc590762de67f5b02c20ce753bde5cc10243?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2a57be757..84ea1ec74 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 475995a58562bd62a23b77b3f98489a9f3257de2 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 19 Jan 2023 12:51:59 +0000 Subject: [PATCH 0305/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9ba0efb18e911f7630a11b714f87c157881aca4d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 84ea1ec74..13946d55e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ad2e71cd6cb08d825049f0d40223ad77da388e85 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 19 Jan 2023 13:37:58 +0000 Subject: [PATCH 0306/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f4a4e56f0b61a977d12cc8570d186603572e0a34?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 13946d55e..ab63763f9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c64a8b4b6abdabd533b285e7f9d029b310712d3d Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 19 Jan 2023 17:56:57 +0000 Subject: [PATCH 0307/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@de80354a3f454fd66f31a10e003eb25f177994ae?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ab63763f9..60562ad61 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 22aac327a51584489aff3d0e074fd02237e3ecd2 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 19 Jan 2023 20:20:09 +0000 Subject: [PATCH 0308/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@73c746203feead7e5379fef74cd4b977c0ab1914?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 60562ad61..a743fd9a6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 25bfb92c0707bd0dd1e4a0fac3a726ca7e9f56b5 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 19 Jan 2023 20:45:46 +0000 Subject: [PATCH 0309/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f2944d7b246bd1a144a9e9da9eef2a34f5d6c88e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a743fd9a6..4d9daaabe 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 0e1b764289d2441b85541360ca1399beff9889b5 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 19 Jan 2023 21:04:57 +0000 Subject: [PATCH 0310/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@04eeaf1d18a3f01004399c98aae2ef3fd8f06847?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4d9daaabe..45fea9bd0 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 98e14745a3c68cfe748522f364b828aae280e75b Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 19 Jan 2023 22:11:57 +0000 Subject: [PATCH 0311/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6a0c082077b2bf321bca4f7e7c4f6c10d753de3c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 45fea9bd0..5e1f455e5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 03f4eff8c9e765d00945e01f1707204ae0194316 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 20 Jan 2023 00:15:47 +0000 Subject: [PATCH 0312/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e3fd95ad5909bb196c509691dcd4d14ae36eecea?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5e1f455e5..61f899c42 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ca096e3768f4657187d3b03e3e046c00bde1d741 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 20 Jan 2023 00:26:13 +0000 Subject: [PATCH 0313/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@00b172e0493fa70059bcc55fb52eb4bd8cfa2bed?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 61f899c42..333f09fad 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ead4c95345de3d2554aa74c24acded8a4d44246d Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 20 Jan 2023 08:00:32 +0000 Subject: [PATCH 0314/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0751f042141f93443d495ed3921f2d9424f062b3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 333f09fad..9cebd314b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From e8823a7cf29811d58dd20c4732e1ce7ac7cc29b5 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 20 Jan 2023 18:25:55 +0000 Subject: [PATCH 0315/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6805f597e3c560cc6b4d8c1b7d6fe8ff06e8bddd?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9cebd314b..db342ee0e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 8108c519cd3859af8d17626f855bf1e9054fcc98 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 20 Jan 2023 18:31:22 +0000 Subject: [PATCH 0316/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@acfe5f7e13fed64e5336f0dc4e39675a9eede56e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index db342ee0e..16c037e30 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d617dd8fe2a5b6574e4ad738750c46593cea310d Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 22 Jan 2023 21:11:01 +0000 Subject: [PATCH 0317/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d1b6b717bfa49a0783041949bfc6be527e1aefc1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 16c037e30..4bedcc1dc 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 1d1834101785320ff0fdfb1a51d79bf9305d7e28 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 24 Jan 2023 11:55:32 +0000 Subject: [PATCH 0318/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@52d9f83a4b4ac6ed6ae9412f37118c760f4b9cd8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4bedcc1dc..9c9442507 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ae95b5c06d484af320de9a56c5ca2482ae521714 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 24 Jan 2023 18:23:00 +0000 Subject: [PATCH 0319/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9d47c3eaee78fd5213bfb5cb44476185f74dfe5c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9c9442507..ca8c02e81 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4ee9e4f6deeae4d9bf50bc8826d72dc0b10d1b3e Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 25 Jan 2023 04:32:56 +0000 Subject: [PATCH 0320/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@caa6bb677b1a2602d8e4d99e8350d73a923b8e2c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ca8c02e81..38d5c64e2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 727f3c2b4bb06f31ddf0a66bc4633e17d0c4e851 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 25 Jan 2023 05:15:13 +0000 Subject: [PATCH 0321/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2afed6ef51d1e49e4d55f0c2861c1afa1cc03812?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 38d5c64e2..b47803b2c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From bb1495cda8399c16bb1bbab64c568bd31adb574f Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 25 Jan 2023 05:33:54 +0000 Subject: [PATCH 0322/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@fc0cbffa07c249492767442d0b5e3a46af0bcf56?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b47803b2c..662bade63 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ec9bf7a475942d9590f6d7d8c5781e7652672bea Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 25 Jan 2023 05:58:26 +0000 Subject: [PATCH 0323/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c8094f8b34f029c5ae877aa5b21ee6c6f5135f72?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 662bade63..e4dfba856 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ceea18c6aa5aae7f06cb10307d55058543ed1607 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 26 Jan 2023 19:10:51 +0000 Subject: [PATCH 0324/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c0fbbd4087319142b02d60b0367e7a79f195b9e4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e4dfba856..adc7ed752 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 79473a3b7f1ac3076961edf0ab491bdab01a5938 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 26 Jan 2023 20:08:44 +0000 Subject: [PATCH 0325/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b66494c833745dc4d3668d6a912aa21cf9922e9d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index adc7ed752..e9f552b2c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f682142af5df71c9a495413d555e0e1ad668435a Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 26 Jan 2023 20:13:19 +0000 Subject: [PATCH 0326/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8f4d2ab6036e45bfaba8d31db7779c81f54c5038?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e9f552b2c..0a426c683 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 3f04491e95629efdd05de4ca3fe4aff056efb226 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 30 Jan 2023 12:08:50 +0000 Subject: [PATCH 0327/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bd544bc28acfe5b174fdc88629785bafb9d57129?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0a426c683..730fba93b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4b2eea7d53042d13ee2a795248ffc0511462a788 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 2 Feb 2023 01:42:09 +0000 Subject: [PATCH 0328/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d81b8f29f55f20740f3223e2dea461e0c56da48f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 730fba93b..11c7ec0d8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From cdf032a1d284c2fab9e88221a080ce0faf0aa5b3 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 2 Feb 2023 02:47:49 +0000 Subject: [PATCH 0329/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e8ba6de4116713d64a22bbf850b48826f63b41f1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 11c7ec0d8..9b70ae18d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2362092a6d11c74c990bebd3f716da962b30907a Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 2 Feb 2023 03:13:49 +0000 Subject: [PATCH 0330/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9092d2a1ffa1feffd569ee5a756b340665cc589d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9b70ae18d..ebd24369b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 43ed790b4b67ab707ffec177d76a8434f273c07f Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 2 Feb 2023 03:40:23 +0000 Subject: [PATCH 0331/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ea9cc315b1ecc0bf47af58cda5f1a55cbe7a1093?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ebd24369b..924d10133 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 65283b079ec050068f80e93968288d3e242ea564 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 4 Feb 2023 04:15:33 +0000 Subject: [PATCH 0332/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@244286f649afc9eafbb30b3cef67ceed4cbd3668?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 924d10133..c9bc2479e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2979d0311db106dbdeb41d66ef13366030a10d95 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 4 Feb 2023 08:04:46 +0000 Subject: [PATCH 0333/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9ab9475125da9dac39a663a0d96d52a56366375c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c9bc2479e..fd3d89923 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ef21ecdbe3aa562af58ef356e478b266c2bf9170 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 5 Feb 2023 05:43:44 +0000 Subject: [PATCH 0334/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@95761b8d0efa527616cf5ca82ad4875f1dc66ff0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fd3d89923..9d9334534 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c5f85a270305969954af1fa8c157ff5378893df8 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 5 Feb 2023 05:49:12 +0000 Subject: [PATCH 0335/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@aa183bbc3e420c5feb53c206b18165fc7db579d9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9d9334534..79014b6d0 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d36e7f403cf56489678b62125661a9fa553b8ace Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 5 Feb 2023 06:30:31 +0000 Subject: [PATCH 0336/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c86e91f8ede0aa5113a128b96928548381c4d8b4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 79014b6d0..fdc3d26c3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c1303c713083404d8a2dd7e2b53d484429cfa605 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 5 Feb 2023 06:47:07 +0000 Subject: [PATCH 0337/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f631fc2874499252da8eeea38751d8cf061bf6fe?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fdc3d26c3..d2b0cc81e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2a0ee137cdc8260e9240bf09c2dc317b6a068ce9 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Feb 2023 06:15:35 +0000 Subject: [PATCH 0338/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@62e163e80d2c583b48049c63c1e512fb6009d68f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d2b0cc81e..d85156703 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 074530444455129225d757515f585ce1b9d60bab Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Feb 2023 06:42:08 +0000 Subject: [PATCH 0339/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@064599ada4bf9afbbad2f8842d29aa96dce6134a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d85156703..a30a74f72 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 607723061c34a0eb1118cb1b8543d71a5f5db47d Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Feb 2023 07:00:54 +0000 Subject: [PATCH 0340/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@98864cdc2d4f2e0dcc1da848ca6fa5b91a9c2c9a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a30a74f72..a17336025 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From e66f7d2685421162303b63c453aa1aa16a5932d7 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Feb 2023 07:36:50 +0000 Subject: [PATCH 0341/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@baa3fc158ee6a95d9a20de7880440a3678e5089a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a17336025..1ba918497 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 8b8d66403fa6de320e5504f73bd16b95d80bc94a Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 9 Feb 2023 01:53:12 +0000 Subject: [PATCH 0342/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b85da9a7b877d6afba1faa6384cc3d7aa0b9a366?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1ba918497..b2a00e959 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 48c83ae1d27c1ea41d94f8326601f2a0da676195 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 10 Feb 2023 02:22:59 +0000 Subject: [PATCH 0343/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@459544058494fc68becfd9d4d34ffbea1c06aee2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b2a00e959..2bfd0747b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 204652c5311c634e3fb24f7792c837c666bcc512 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 10 Feb 2023 02:40:29 +0000 Subject: [PATCH 0344/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@83f6d537b549057025e751a6e0d55d0a1310002c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2bfd0747b..001dee319 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4136b85156858346e3bd22825e4c8b8f1e9e5cd3 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 10 Feb 2023 03:15:36 +0000 Subject: [PATCH 0345/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b655cf9cc241b61f4a423320924e860d6087dc93?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 001dee319..7a6d4fdc1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 65097bdc6b2c23fc587606c8796d6b4e57a9ec1c Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 10 Feb 2023 03:29:50 +0000 Subject: [PATCH 0346/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6b4933e345568ffdbc36dac52d3a3eea19028251?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7a6d4fdc1..a5804f23b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 11d409ebc1501f4223ba83fd8c997af627227e8f Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 10 Feb 2023 04:18:29 +0000 Subject: [PATCH 0347/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0f879b867b7af2256dc86e7f60c2d65918114135?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a5804f23b..42410190b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 251ad11be99d80a0c8f3a2fda7752a82b3d7c866 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 10 Feb 2023 04:32:06 +0000 Subject: [PATCH 0348/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@add90de33457238420c52c8e58efe4222d7fc721?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 42410190b..fae49070a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From becc1fb440fbc4bd5575a8ffee652b83ffb4db42 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 13 Feb 2023 03:14:01 +0000 Subject: [PATCH 0349/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@fe48bb262dfc7ad47d2036eaa70fa5117138dcf9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fae49070a..f4178cd86 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 3716c0253a294c33d8813d04292e4fc1ae1bf0a3 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 15 Feb 2023 07:24:43 +0000 Subject: [PATCH 0350/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@34aa71bd07bcc3b8dd8e77d4ac6dceae4f68987a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f4178cd86..acf61f091 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 3b00dc35472fdd78f85c6a3b44afc67836640885 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 16 Feb 2023 03:08:08 +0000 Subject: [PATCH 0351/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1f3cd4635a9a0a8d2cab08f6680a22b980332e14?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index acf61f091..c6cfe2e7a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 09f7027a8434a84e69ae98c57782f32e1135bede Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 16 Feb 2023 13:03:35 +0000 Subject: [PATCH 0352/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cbbecea79cdea3866a8aea2d945ca45945a02293?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c6cfe2e7a..6a724aef9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From e622a1e9bb2e5146e6e5302afbf54bceb7289217 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 17 Feb 2023 07:44:27 +0000 Subject: [PATCH 0353/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@73b7a9c2f9e0874a6ecf3289053ad8faa20bc06e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6a724aef9..5336a3c90 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b0879e29e7104776dde91671cf77ed062aa0c85d Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 17 Feb 2023 07:57:49 +0000 Subject: [PATCH 0354/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e7bd36221e3cefeebf7e01377e26e64cf6902cad?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5336a3c90..3f4317071 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7e7a141db620ecf1a2d8b000ee83bb0640305baf Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 19 Feb 2023 02:20:02 +0000 Subject: [PATCH 0355/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@66749d6bd804ee32cfdaeb3b33eaf58bba699004?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3f4317071..ac58ecaa6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d3fe3581564a122d69fac6b35179c0ae0174cae0 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 20 Feb 2023 08:41:12 +0000 Subject: [PATCH 0356/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cc468d5f860bb72ba92e816b7623a2c2df269227?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ac58ecaa6..219dada63 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 88e84a7526474948259941329ed8d2de831a4a8a Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 27 Feb 2023 09:03:42 +0000 Subject: [PATCH 0357/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@23272f233f9350ce477ec90aceced019c42c36b1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 219dada63..5c5036b78 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ada35e4a11b0844e590e31f9c19387a3befde2a2 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 27 Feb 2023 09:19:25 +0000 Subject: [PATCH 0358/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@278e0751c4de01d67fc3bf1fd4b5181c1b1262dc?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5c5036b78..b963fd01b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f151d04127aeded7bc6f97654d2c38e039ae053a Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 1 Mar 2023 04:10:39 +0000 Subject: [PATCH 0359/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2a4f5f883772e1ee6e8f622c0c93030ae9a8ab8d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b963fd01b..28c6a8bf6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 563b7006bc4e5ee64d37753b188be712e8b96d6f Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 5 Mar 2023 06:15:08 +0000 Subject: [PATCH 0360/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d841e69a0625e807156f2d0cd224fc8d57c97ba1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 28c6a8bf6..20c335195 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4514e91814ca44b6651043592df77ade9ed38290 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 5 Mar 2023 06:40:52 +0000 Subject: [PATCH 0361/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f28662dbcb21264c16013f130eaf7e0697851fea?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 20c335195..584daa4d0 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 8070b917e6f8df7fcbc79f0f8d5a2d3da5b317fd Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Mar 2023 02:38:23 +0000 Subject: [PATCH 0362/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2d09266e23ba3c8eb78874ae3adc43c88a3534fe?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 584daa4d0..0b4eef08a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ae8351524feeea45a7b73a0497eba963e21ca701 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Mar 2023 02:53:34 +0000 Subject: [PATCH 0363/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5008027195ba2ef0b1ea6991bc73f4d7551fa007?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0b4eef08a..e9dec873f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 781e5470ac70d1a66764a88106b20c9abe40513b Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Mar 2023 03:21:41 +0000 Subject: [PATCH 0364/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6634a2f7c299e0ec04050099b71655dbd987d960?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e9dec873f..1b3238755 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 0ddaf30ae1e3ee231000b8c80a140d205751986e Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Mar 2023 03:24:00 +0000 Subject: [PATCH 0365/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3b8556eabc785ea439a22427c996e0e67bd361ce?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1b3238755..4f30ce341 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 70aa38cdc3202bf0050c0dd37144374ff291f1fe Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Mar 2023 03:24:45 +0000 Subject: [PATCH 0366/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@447b4f959c9177f239c1c02690c00e1c31a1045a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4f30ce341..11a24ee39 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b21499e39a563db9fcdde64b9e2a4113c87deeef Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Mar 2023 03:25:35 +0000 Subject: [PATCH 0367/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bf79b09f7c91fbcbd2a7acdc55e7bb8dd06480c1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 11a24ee39..1b3fe453e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d0b2db8cedaffaaa7775e37e1885cfdd9297cbef Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 Mar 2023 10:03:31 +0000 Subject: [PATCH 0368/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@60e3640de2618dfd6ec158acc37a72633f469254?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1b3fe453e..b1c4f49fb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From cdc1da4a95771ba0cba1254a7ff34881530463be Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 8 Mar 2023 09:01:28 +0000 Subject: [PATCH 0369/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c65a205805b94023081328cb93a93624bc74c38e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b1c4f49fb..7fbdce712 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 074adf7b4b729775cbe824c7171b785040e504cf Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 8 Mar 2023 10:13:33 +0000 Subject: [PATCH 0370/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@18cab53558b6dcfa19635093a5f9b2e691543b54?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7fbdce712..743ff850e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b6602b1e34497905c6986ac9fa908af0f439739c Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 10 Mar 2023 05:10:14 +0000 Subject: [PATCH 0371/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3eaa58eecd2576f9241997aeaa189fd8b8b9ffca?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 743ff850e..7d02cdf7b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f7a6cb9d6605aa12291887b8a82dad2642ae2008 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 13 Mar 2023 01:45:07 +0000 Subject: [PATCH 0372/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@de33cf84bebb59fe8b7fb7237785cb7f2bec655f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7d02cdf7b..cc3f75155 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 074713fdaed640d6548703e156b765c9d62af680 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 13 Mar 2023 08:50:00 +0000 Subject: [PATCH 0373/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bb81a58b4a2e19ae4d2adeea91f2020d2445c9ca?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cc3f75155..50c62d897 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 6ae0cfe723574246b48d5d6bb8d36c3177a1bb97 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 14 Mar 2023 01:15:49 +0000 Subject: [PATCH 0374/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8c1fe16ec6097e3ac7c375a93123ea386480c79d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 50c62d897..8ce734811 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d8e75deb321b4505850358675de9b1bba74276ef Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 14 Mar 2023 01:24:32 +0000 Subject: [PATCH 0375/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0d0afa457f5ef73d78687ff422993607f50ab149?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8ce734811..0370d9bee 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 10cd29c11ebd13c614b3960d4a7089ccffbf945d Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 14 Mar 2023 03:24:18 +0000 Subject: [PATCH 0376/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f5e8b03f4044621c3f1bd133fefdc14544707658?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0370d9bee..6cef73be9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 726cf316cc72d1b98854100ad5e864a9aa227844 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 14 Mar 2023 03:27:18 +0000 Subject: [PATCH 0377/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@fa2614a2b780dc844c3612829968c838b3d65bbd?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6cef73be9..490fc00ca 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 744ab8076cb18c6938ee1b5d7d7d4bba217e94d9 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 14 Mar 2023 05:00:33 +0000 Subject: [PATCH 0378/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@36eb71df78b4f2fa611d4c5f2af8f6f27153871c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 490fc00ca..27602fd94 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 48480358a58b21803ac9a6003f1f4504bf904058 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 14 Mar 2023 09:32:30 +0000 Subject: [PATCH 0379/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@11b4061d5dde1983a5a7da1f81dee5d6b4655060?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 27602fd94..986862bd4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 31cc752ebf2e0677dd70df5c3915ef48e245a8b1 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 14 Mar 2023 10:17:46 +0000 Subject: [PATCH 0380/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8795bb28dfbd2ad4cd28c776fdce7b1cbb67c51d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 986862bd4..0d73a2695 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ff12cb2b1678335dd87dd7e886fd5fc7d81087aa Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 14 Mar 2023 10:35:31 +0000 Subject: [PATCH 0381/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b6caaf588db5075e189d6a56e0d2d9d9a4bbeb15?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0d73a2695..521a8a977 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ae41661067096ae51bc5eec11439d1c2d25382c6 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 14 Mar 2023 10:38:10 +0000 Subject: [PATCH 0382/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4e68dd9132a9b67abbd63af915ae19d012dc09b1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 521a8a977..4482fa59a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 61744c9e9c562ee2737c8946c7e8c9ca7ce33783 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 15 Mar 2023 01:28:31 +0000 Subject: [PATCH 0383/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5d945b2a0c7f3b3c1ff79af73bf1c9c9c2105977?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4482fa59a..16020bd51 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f2b347b33b8a021f2b89298d695341dd08973783 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 15 Mar 2023 01:37:14 +0000 Subject: [PATCH 0384/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ca9074165f9d8754f076bc20a1622ffef6701c60?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 16020bd51..f5a9284ea 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7d0b99e3f7e603ccbb9ecfc71770fca239bfe6c0 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 15 Mar 2023 01:46:27 +0000 Subject: [PATCH 0385/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@349d8354758915ff30d8e9fea677fd4e5cb79412?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f5a9284ea..8b97c5058 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 401c90d06bf4284af395d29895431f7960ad99da Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 15 Mar 2023 01:49:37 +0000 Subject: [PATCH 0386/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ee1e967138862cb5165980edb03aa971b68c4688?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8b97c5058..db3056748 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 67fa278849c663f38d4a194b806d7b87c9801d7b Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 15 Mar 2023 03:39:37 +0000 Subject: [PATCH 0387/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1a80a9707151c6c5de761c0a5a0ce2c800217ad7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index db3056748..84f3a8480 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2ab940d83bc48b5cb9c4083198d1c112c625d334 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 15 Mar 2023 03:57:28 +0000 Subject: [PATCH 0388/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9ce473ca48b00b0966900afac7dc89be4e8bb4f9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 84f3a8480..360609c13 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From e7de2727325d56a1b854264d2bf9df8ded95f9b5 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 17 Mar 2023 00:05:49 +0000 Subject: [PATCH 0389/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@59e43cb2fa29320c4b2f99f218a8604dad8f4801?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 360609c13..9f57bffbf 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From e383d063f56177e7cd0f766cdf55e40033f6f82a Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 17 Mar 2023 00:22:35 +0000 Subject: [PATCH 0390/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e3329db3f947ddccebb5bdf3e0f9e27c5d3fe841?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9f57bffbf..f596259b9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 63b8d3a828446ed738cf9b820b14c198fd2e99c4 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 17 Mar 2023 00:27:55 +0000 Subject: [PATCH 0391/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8eeacb088cb5ea8ac24983f23726cf9bc9b7b9ec?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f596259b9..74f210a7e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 31ae9cab01b42997b14b546e8c77dea8477827bc Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 17 Mar 2023 00:41:05 +0000 Subject: [PATCH 0392/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6c5d7fd42d2f595d0f6f1c3f766905bacd8ab2dc?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 74f210a7e..c33874a94 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 534b5ae83df2c587b2cc37b71b4248117142c062 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 17 Mar 2023 03:27:32 +0000 Subject: [PATCH 0393/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@db52cb73d598cac4f78da0d2ce909af79e01ec75?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c33874a94..251f7b429 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 420c6ff1012827fb9e336451ad0143bbbbbb1417 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 17 Mar 2023 08:48:47 +0000 Subject: [PATCH 0394/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6a3ab5dcc2b746d509d62e0ff932992af5c6b4e2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 251f7b429..798af8f62 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b897242dcb94c5b2685076ada4f2633b910b3309 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 17 Mar 2023 10:39:58 +0000 Subject: [PATCH 0395/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d462b460afed1f7267ef957c2beaf6ba6a4042f2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 798af8f62..4fb372dd9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From bf72bf430f72f3238e9f3508853be09b560552ee Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 18 Mar 2023 11:01:12 +0000 Subject: [PATCH 0396/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4e5d0612297cb6a39e7e125018aa76477b585fdc?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4fb372dd9..9a37ca2e7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From dbbc8584aae828811fe3a5a8ba97d49b6f023b4d Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 18 Mar 2023 12:50:40 +0000 Subject: [PATCH 0397/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@733956d88652a40d93534494f18ba5473074d1ad?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9a37ca2e7..317909659 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4cf0b10589e86f4c17221dd605a874fa48873ff2 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 18 Mar 2023 23:20:52 +0000 Subject: [PATCH 0398/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b2ab99554bc815343de9e87028bc8c605d9cd011?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 317909659..e58058e01 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 76f3cfdcb9d5e30407b3fc0a258919e7a2aad1fc Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 18 Mar 2023 23:24:08 +0000 Subject: [PATCH 0399/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@52bc16f62167550a6c90da3a6e2653bdc14a6380?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e58058e01..5e6ba1b10 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 424ce63debccaa0b43167bc8aa85814a072af261 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 19 Mar 2023 00:34:42 +0000 Subject: [PATCH 0400/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c066e1e6efd33c48c7e86068a59a2fd18023bd32?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5e6ba1b10..931a4a472 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b6599df5e6cb6caf12ba5dcf55e537484e06e929 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 19 Mar 2023 06:55:29 +0000 Subject: [PATCH 0401/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@eec9211b39f8baa69793a943a8acf620b9b7a9df?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 931a4a472..43d375d1a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 0bb75457c8827bc8f083e300e2d4413575a70019 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 20 Mar 2023 03:18:17 +0000 Subject: [PATCH 0402/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@19c195921bab4c4d1b9ecdb80d9ce665b9e5b7d4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 43d375d1a..422358851 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 6da7698a4b30b76502cab817db41d0a1d28f7c62 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 20 Mar 2023 03:41:44 +0000 Subject: [PATCH 0403/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4be239196672c38ba6484612b3a6f3de5aea6c32?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 422358851..65b7515c7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 8c0d75636afd902e129c2ed37d78827a930af474 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 20 Mar 2023 11:43:08 +0000 Subject: [PATCH 0404/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d75af6af0fff4957ef811b58674b6121b60d761f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 65b7515c7..f9527f62f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5c7ea7eec2818167f10c748a882d60d5ac314c7f Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 20 Mar 2023 13:06:00 +0000 Subject: [PATCH 0405/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bcba6d3957f981fb74fa96670f65e317aaefaa2c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f9527f62f..a7dd5d7ad 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 8a8fae3fd3db7ed55098f3afcfd3b3c64be701cd Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 20 Mar 2023 13:27:48 +0000 Subject: [PATCH 0406/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3521b58236c6f605b612feecd84d1231e7d5b8d9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a7dd5d7ad..4b9eb44d7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 001a13362878478ebe8fe3e094fd58ae1a2a6b8c Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 20 Mar 2023 14:03:49 +0000 Subject: [PATCH 0407/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b7fe99c1ddcbdbd864dd12001f841dc3d1c17800?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4b9eb44d7..8813d4c5e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From a4fc92866570e2a82a3ac5704f2340b450b1d2c8 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 21 Mar 2023 06:13:47 +0000 Subject: [PATCH 0408/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5ce74fcfae685c599164335d4c20ec377b1a6c4c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8813d4c5e..941ca7f08 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From dfdd5f06c4b076014ec165803d5942675284216e Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 21 Mar 2023 06:23:54 +0000 Subject: [PATCH 0409/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@68ce683bc98931b09f8977cb6cc6aa84c246d8ea?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 941ca7f08..4da3439f7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 29f75ac8ec7629519a1c0e9da0a8681f481860fd Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 21 Mar 2023 10:39:24 +0000 Subject: [PATCH 0410/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@718ec27d3220d04fd04ea6c736c961d42bed0712?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4da3439f7..08d278444 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 00208a5d4c629cd308e4a66701fc3f02eb9e06d3 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 21 Mar 2023 22:12:53 +0000 Subject: [PATCH 0411/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@402e64ba2ace52f78c239010e65a6984f6cb2438?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 08d278444..3efd79aa2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d9b7799cf1c2891007f57445993e7c416487cbce Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 21 Mar 2023 23:29:46 +0000 Subject: [PATCH 0412/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d68a1e8dab20d96038a3752bcbfd8031f3d80c6f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3efd79aa2..43543d459 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c6596a579cd50864d2d22d7bc33de3e517c5f869 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 21 Mar 2023 23:33:49 +0000 Subject: [PATCH 0413/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6602ca3eeca3e6629043739e2d48fd039a6deae4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 43543d459..25bd28990 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 0d5b7935d6e86325d88c148b66f2dc95845a4c64 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 23 Mar 2023 00:09:13 +0000 Subject: [PATCH 0414/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@fd2cfec5cefe1b251c46aba2a7c5c9c998acb7a3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 25bd28990..1e80ed38c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 15d23eb4481fdb84e8c17ca9ac3bfc0263e3f315 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 23 Mar 2023 03:45:39 +0000 Subject: [PATCH 0415/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bbcb77a867b61c13d395bb8a7d30889a835f563d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1e80ed38c..0560250c4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2ace8fdc5e8ef01c76e53b4744ce6279fb0b7110 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 23 Mar 2023 04:53:09 +0000 Subject: [PATCH 0416/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f3d97eb9ea1061182045152a097320f995f859eb?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0560250c4..247c93099 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ec7d1c2158ea9e444515bdcc434cb94063154df8 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 23 Mar 2023 05:31:34 +0000 Subject: [PATCH 0417/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1c730acba8d37a4e7d986f89454e395e24b6518e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 247c93099..9762afe8c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4f63b71230e3f83798a8559cf1d35aa8644f83bf Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 23 Mar 2023 10:16:45 +0000 Subject: [PATCH 0418/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5106e8f0e2a21152e61d07c718ed3daa959e48ac?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9762afe8c..10774e134 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 1c8219d68c13651f8426a37ef803089a781e406a Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 23 Mar 2023 16:46:20 +0000 Subject: [PATCH 0419/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@fd73108435968cac87b5930783bfd79918d38ca3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 10774e134..858b5f457 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From e55281871810b47acce43092298afb6a0aed7fd4 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 23 Mar 2023 18:08:56 +0000 Subject: [PATCH 0420/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a006167dafcca1a829e575fe2c1ac84f664bf0b3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 858b5f457..9c9894c70 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 37e0e39a54acdbb48a535c0b03aef5eeae6e237c Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 24 Mar 2023 01:50:12 +0000 Subject: [PATCH 0421/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9ce4656fcdc6d3b972a27f544b988c9641a7b862?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9c9894c70..3a2280f03 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 724deffb954951fc84e3d02e4718e7de74f8fbd7 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 24 Mar 2023 02:48:04 +0000 Subject: [PATCH 0422/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9b4eca7410def31c362665e7362f489f40d8b84b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3a2280f03..1386df61d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d22db0caa710dd02a03a08bcbf12f777be890e03 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 26 Mar 2023 09:51:17 +0000 Subject: [PATCH 0423/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d250a25d9a2ae81111cad75bb5d9ca2d353cf47f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1386df61d..48c3517d7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 9e077f6dc55d849e51fec408a81c6f06b1d86aff Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 3 Apr 2023 21:59:40 +0000 Subject: [PATCH 0424/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@266efc84a17bfecfde2dcab58e4ad005954d60c3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 48c3517d7..4e5fcadf4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c52f3c5c5688abd2b9819a38c83f7ee15f1888dc Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 3 Apr 2023 22:52:53 +0000 Subject: [PATCH 0425/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a8c5b92f38459c17a5f8b02ce26f36e76ffbc810?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4e5fcadf4..69d661d18 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 756e43e7a14a32f7f780d666d5d667dee46180bb Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 5 Apr 2023 08:05:30 +0000 Subject: [PATCH 0426/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cba68a6d0a6bc02c514d66fd601a8112cfb9c38d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 69d661d18..5ea71dc25 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 63f5e53123783cd0cc42188712728d32798938ec Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 5 Apr 2023 10:56:25 +0000 Subject: [PATCH 0427/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9179a07325e1b4e1e4414635385300cac5e44aee?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5ea71dc25..0bc5e22a0 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7fe593769a50d29dbeca81be4cfced675e8cea92 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 5 Apr 2023 12:09:28 +0000 Subject: [PATCH 0428/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a18990f8d4f5f1dbf607af983fa2be961bb1bc5e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0bc5e22a0..a502a6c90 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 73947bcbd7bc274d99e8427b604e286751552cf6 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 6 Apr 2023 05:52:50 +0000 Subject: [PATCH 0429/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8b6b25b565175efdf30f009c6ad5b5bc04daa437?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a502a6c90..743d187b4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From e6195b6dbc88cab71d511468990caba0cc7a0528 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 6 Apr 2023 07:04:38 +0000 Subject: [PATCH 0430/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@428828fb435d71e8df3ff911970e37f1dbea159c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 743d187b4..9427608fb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4a27a64ca96e80dd7f9a945a76b6f6a0f01450cd Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 8 Apr 2023 01:09:58 +0000 Subject: [PATCH 0431/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c96e023d4728dfd41908c0161ef91f0a3b3cead7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9427608fb..5f54aacdc 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c09296e9543628f76980cdaba895d21d80fb3142 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 8 Apr 2023 09:07:09 +0000 Subject: [PATCH 0432/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@014fb2663d3a7568311162b31b514f581731d51f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5f54aacdc..4b54083fa 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f80312a46850022791daa307cd5a17bf238e966a Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 8 Apr 2023 10:29:41 +0000 Subject: [PATCH 0433/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ada09456648f365fb306375b70fd797a53c80635?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4b54083fa..186c6f86c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From be8518d2721f2f06aa5c7b5e79a807c49426035f Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 8 Apr 2023 11:10:17 +0000 Subject: [PATCH 0434/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cf1af9d305bb6d7db63e2e8f79b4c620bc0d4579?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 186c6f86c..73885bd1e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d5bc9290f24cc32025851341aaefbe0af51e04f2 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 9 Apr 2023 07:47:24 +0000 Subject: [PATCH 0435/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@34c6fd882c09306d5fadb91b9e222cb7decf10c0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 73885bd1e..ba2995978 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b9d65be897e7c474c21cc727226d8b665285f275 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 9 Apr 2023 09:07:19 +0000 Subject: [PATCH 0436/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@019766717f38af42ae2a8845658a16a98267f9eb?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ba2995978..23db55a43 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 144ff2bc99b59eb1994ea641c0bbb437b45d75ee Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 11 Apr 2023 09:01:08 +0000 Subject: [PATCH 0437/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d7b044318e95656e92ba8d3b3d2e78ab36180666?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 23db55a43..c5bc4bfb4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c48ddf8c3d5f3e7eb99a1f9508357987763e3d5d Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 11 Apr 2023 09:25:35 +0000 Subject: [PATCH 0438/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9e38749162f5a0ea7a4f7219e70e52dbcbf4eba4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c5bc4bfb4..c66ea8b20 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f5f6c140aa2060e214a3ff2a28bbde1b46e1e2ba Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 12 Apr 2023 11:11:53 +0000 Subject: [PATCH 0439/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1b9daf3902fffec50b0cc8a861757dcc30bae48e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c66ea8b20..9e18cc0ce 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f987936426ecc998fb6eb01990cbb35b52217ca7 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 12 Apr 2023 11:20:15 +0000 Subject: [PATCH 0440/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c89b0ee8a3eb0ad3344a7c6de1258603c1520e65?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9e18cc0ce..5122a419b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 49e7e35b3e10f58046121e546162d65936435c90 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 12 Apr 2023 12:37:21 +0000 Subject: [PATCH 0441/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@96b0598aeadb4ee91e92efa82d28641a160348a0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5122a419b..73eb5ba96 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 1afc9a35fa27df7a902384302928bd1ff687d41e Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 13 Apr 2023 06:51:55 +0000 Subject: [PATCH 0442/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2b18fa5772d878a3cd543853c3af036de8d316bb?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 73eb5ba96..3492d9802 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 510dba839dc46acf5ae8bbd44134afc85997526c Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 15 Apr 2023 08:08:24 +0000 Subject: [PATCH 0443/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d5039e95669ab3f41a893735a4b9d9fa8dc32d1e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3492d9802..bb5ed46fa 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c322744750276b00ac7201ab52ab0671dc2c0f70 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 15 Apr 2023 10:32:14 +0000 Subject: [PATCH 0444/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@129c439c2b7f8c32e2210f7f0e3b9d9ab78cddd9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bb5ed46fa..0942c3f2b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5cb5510d7e938d3fca472eb4a5f88bef7781c6d4 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 15 Apr 2023 11:24:53 +0000 Subject: [PATCH 0445/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@71aa54e7dbfc25e9af1b9b531b4d019114c38d8f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0942c3f2b..895783f82 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From fa60bf8de94f25269ffc1501781fde3466349e41 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 15 Apr 2023 12:05:35 +0000 Subject: [PATCH 0446/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@03548853b4e9a46c163b0b79dd4661dca3fd7070?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 895783f82..0bb91c340 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 3a96cc2acb7bf7cd4a3fcaba39acfe604aed3bd2 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 15 Apr 2023 12:17:05 +0000 Subject: [PATCH 0447/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@54099faa46d3abbad7f2a2d8ba78028ed6eeb0ee?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0bb91c340..135c643ce 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 9dd8551a7a42b914acbfbc46c8620cfbd32ae6d4 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 15 Apr 2023 12:27:40 +0000 Subject: [PATCH 0448/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@29cde7000c2c646f5b37a7a5a1969a9622cf344f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 135c643ce..db2867cb2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f485156396c420cc24d6561657d99c0f3af5bb39 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 16 Apr 2023 07:12:14 +0000 Subject: [PATCH 0449/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c8314976143a2e42f1838e283a900efb054dd3e0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index db2867cb2..a41453555 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 06a887a78df6e876736a4f55786a76a3390ed57a Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 16 Apr 2023 08:17:12 +0000 Subject: [PATCH 0450/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7244b6e53ee8b8693bb4bebc08931ce455710bc4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a41453555..d80ddb31a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From a0d82fc5734681405ab32b5ac97394039d30c2cb Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 16 Apr 2023 10:15:24 +0000 Subject: [PATCH 0451/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ed3c5a6765222fcb05bd929567ead22cd45e640a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d80ddb31a..66d560c20 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 76c0f77619df62ef7615c8c5a87f6923b546510a Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 16 Apr 2023 10:29:55 +0000 Subject: [PATCH 0452/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@17a8934b1eb35abdbf8e1cbd94a6b0634bd80a94?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 66d560c20..dda22e95a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 6e06dad41b90d1e73d4f84937c5264b5b432483e Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 16 Apr 2023 10:55:03 +0000 Subject: [PATCH 0453/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f876e7ae594dc8e5986be383f5000c60e65d2892?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dda22e95a..083890614 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From db8a12d0a686603266e5214a2b5b5526597af3cc Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 16 Apr 2023 12:14:47 +0000 Subject: [PATCH 0454/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6b52053e56564343592597a715966b4a3f7c7163?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 083890614..9031010d4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From e096a19ade1d09f3533432ff340d71c5a01fdc10 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 16 Apr 2023 12:36:40 +0000 Subject: [PATCH 0455/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e9a7e81075f542be9b143ee803aa2254684eddd3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9031010d4..6c0fda394 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f300c216246ed9c143ccae2b32b626199a4d64ce Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 16 Apr 2023 12:53:37 +0000 Subject: [PATCH 0456/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3253f0a78405bdbaeccbb3df9dabd4c3c129d33a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6c0fda394..985e906a3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 0a7dcb55d807e6a23eb4bb8097c4c6a03133d947 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 17 Apr 2023 05:47:42 +0000 Subject: [PATCH 0457/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@999fcab53d1da6ce25a0a5551d660a42fc9720fc?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 985e906a3..46dce4820 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5ddb9b53d7f48fdaeb445833153a46ffbda7c212 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 17 Apr 2023 07:30:59 +0000 Subject: [PATCH 0458/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@37c034c6a03dddc9b3522b6da18a7207b22aab91?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 46dce4820..1759b9228 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 926710d707beb49cb7219a45512e0726be2f3d55 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 17 Apr 2023 07:52:10 +0000 Subject: [PATCH 0459/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@72b7b74858f270c31b0d9e8a15f39519349aa26e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1759b9228..7894544a3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 643f08f2b726d547f8670c1510eb5590c2035951 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 17 Apr 2023 12:15:16 +0000 Subject: [PATCH 0460/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8fdd60ab8639c8fa3474585dde7edf402b99c44d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7894544a3..06655d61c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 523bb5777fb55cad4be79e48a7c52dc6221e50ea Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 18 Apr 2023 08:42:05 +0000 Subject: [PATCH 0461/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d82de49c3b1987eed9d4bdf1984b21cd1481df29?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 06655d61c..31fd571eb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4a465db49e2adb41687762a59379341af09a7bdd Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 18 Apr 2023 08:54:47 +0000 Subject: [PATCH 0462/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5cc299c37c0ba3882893def3167b8c01ab436e50?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 31fd571eb..de2efa7fc 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 95ae60fdf14a9b4bc32df1084d73556f1a572e78 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 18 Apr 2023 09:20:56 +0000 Subject: [PATCH 0463/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0ea8b30050ac085aebd79f137472e078cfaa3516?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index de2efa7fc..d8cbcbe6e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2c0e8e8d097d5ad235e85af46271322447954531 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 19 Apr 2023 06:59:39 +0000 Subject: [PATCH 0464/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@05ad532f7c8543629746bdd390b2a48cc573b54b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d8cbcbe6e..658bf9fb5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From e52616374801d6db3d85b393dced5c61b4f4b2cf Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 19 Apr 2023 10:29:01 +0000 Subject: [PATCH 0465/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7025104c31c35920f3b35d887d8d3e1819bfd92d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 658bf9fb5..dc966417b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 9bc1831f93e7038d39b366556c771c91cb8b33ef Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 19 Apr 2023 17:00:14 +0000 Subject: [PATCH 0466/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@13b77c8342df0612105403827582696f5db64768?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dc966417b..de0eb88bf 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 9cc85f52d79c09ac056931b14fd9598dcebe2c29 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 19 Apr 2023 17:20:24 +0000 Subject: [PATCH 0467/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bbed734d542d446ee18c010ca0ba809d2ee348af?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index de0eb88bf..e6c27df9b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 044c128f97a0e8db235b8a694227cefdb5b60008 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 20 Apr 2023 09:17:26 +0000 Subject: [PATCH 0468/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b2553cff1f1c209c69947503337c26bffb6d2adc?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e6c27df9b..0c5de87b6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From edcb5113a9e9e4a0fe88740226feda04ff74a5b4 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 20 Apr 2023 09:41:22 +0000 Subject: [PATCH 0469/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@005aa8b743236223d6145954d6d712e4cc241c2c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0c5de87b6..6fcd1bdd8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 063df213303b7b35e8b802d8466a55861cee5b79 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 20 Apr 2023 16:39:44 +0000 Subject: [PATCH 0470/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f01c2ea773cfb67dbb46f154b60b921d3c95d208?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6fcd1bdd8..6a6e8efd3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 80aba93f4961b83f261c18ef2ff3e806850adb51 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 20 Apr 2023 17:54:12 +0000 Subject: [PATCH 0471/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4cfa587fc865c57de6126b4aeb3bd956479a481a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6a6e8efd3..83158b6e4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 38c80b3801a91ed4d2d7e9a325f937c6b448b77f Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 21 Apr 2023 09:00:21 +0000 Subject: [PATCH 0472/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1b9725f8dbb402c6155f2009d733ef401be9f0a2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 83158b6e4..11d7d0bb0 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 540a2853946592f79f55822779469a09c1578f4d Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 21 Apr 2023 10:16:51 +0000 Subject: [PATCH 0473/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2ebae164defc4b66cb25d91ba96312088604bec4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 11d7d0bb0..ac874a883 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 510100404eef146f9cde3f8708dca32874409c39 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 21 Apr 2023 10:26:51 +0000 Subject: [PATCH 0474/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@42e0055430e9becdd242b0b81c3a076b330d626b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ac874a883..5e8593007 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 67bb8fe68e61d868492640034cbd8d3f57db8dde Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 22 Apr 2023 08:40:33 +0000 Subject: [PATCH 0475/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6d5525085221cf4be51e873bdfc18c80a11910a2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5e8593007..d52c16710 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 30dd5338a82bcf90d916a2f47a6bdad9d86fe193 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 22 Apr 2023 09:08:48 +0000 Subject: [PATCH 0476/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@aaff328786a2c75b11620b188652eaada15768f4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d52c16710..d6537f136 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ce86c54c801b5f7141971b95f0eface83615a121 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 22 Apr 2023 09:36:10 +0000 Subject: [PATCH 0477/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@eb2cf4cd5a2f01cff13436d69098f8462c6b38e3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d6537f136..c680223c6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c8002415a2bc46ce6ce7eaa18ccdd0b250b4c533 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 22 Apr 2023 10:18:53 +0000 Subject: [PATCH 0478/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@528f06407679213cdf060fdb3dad13286dc019f2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c680223c6..65bf32d3c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d66e399cd96714c432aadee085d6c43563c030c0 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 22 Apr 2023 11:37:06 +0000 Subject: [PATCH 0479/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bd42b62cc572d84ff410ad121f402a3a15525f57?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 65bf32d3c..9e109585e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ef62baf91e239936bcd69207b22b4917502078e2 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 23 Apr 2023 07:29:47 +0000 Subject: [PATCH 0480/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4a6b27e347c1510a05e4e12459af15c982c59d89?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9e109585e..8a39016b1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From a70f6831de59963dc651cb250eb2d2b6bbcca020 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 23 Apr 2023 08:12:10 +0000 Subject: [PATCH 0481/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d7bf9605e40702eed2abfb8b3d54eb140d2e0026?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8a39016b1..32d772af3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ec8a2b75ffea7b45567599e89dcd69043b722290 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 23 Apr 2023 09:38:04 +0000 Subject: [PATCH 0482/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f153d20288924118c4e7e49362ac8276f8f4efdd?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 32d772af3..32c38fa9b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 28ff6b6baab7ecfe642eba8e51b9f04acc71d84d Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 23 Apr 2023 15:43:41 +0000 Subject: [PATCH 0483/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@fd01b81b6b481ab1d85e80568d8c5d3713ab53b5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 32c38fa9b..b2c500246 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7c966786d67b438548c44578aaeb6abe8418a52d Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 23 Apr 2023 16:22:20 +0000 Subject: [PATCH 0484/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@272c0c121902484163a790831573ee4eb5589111?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b2c500246..2d260aeb1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 1138f2199da4f2e11ee6f4969faf41c83450afd4 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 23 Apr 2023 17:13:37 +0000 Subject: [PATCH 0485/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0fe1b3b6dac6eaacb700d3fe3d4a3539cda416a4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2d260aeb1..cfcf88f58 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5f33c55360aba5d21a38307a32af0b69574e85f9 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 23 Apr 2023 17:39:07 +0000 Subject: [PATCH 0486/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@22b849ac75d22b3bb824dd0b33203c364b132755?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cfcf88f58..2ffd5790c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 14d66687278196a3aa755c6049cd5d3eb17482db Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 24 Apr 2023 11:43:58 +0000 Subject: [PATCH 0487/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@127037c619d4410ba157808a1ac6edc58559dc8f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2ffd5790c..c9572e0cd 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 6ade54056082546e2c962515af121adb7d7dafa7 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 24 Apr 2023 13:41:56 +0000 Subject: [PATCH 0488/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b33380b09b0bf8225946630387c6bed66259e215?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c9572e0cd..faeeeabfd 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 00a42a726d81aa4704b25e7946bb93a55e5824cc Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 24 Apr 2023 13:45:48 +0000 Subject: [PATCH 0489/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a53f909254145d198a9e9461b4329aa02fe05ad0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index faeeeabfd..354fade04 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 43826b92aab8bead5a6087c1f875571cecae861c Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 24 Apr 2023 13:48:08 +0000 Subject: [PATCH 0490/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@64fa00c38facad8a18fa63cfaa98171c462f474d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 354fade04..56d7a586f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d67e34177a042618f5646d402097afc8994ac8d5 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 24 Apr 2023 13:50:58 +0000 Subject: [PATCH 0491/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@05d014a2feab973b2247cc210ef16211d42a9055?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 56d7a586f..cac51f371 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 06a34d01f72f527d0466eefb0e8e0b2131e69843 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 24 Apr 2023 13:52:20 +0000 Subject: [PATCH 0492/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e82199171ac9a99b2c3fddcdf8ca26c974bd3b10?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cac51f371..457ce68f0 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 56c3f047d36737e11f15813906b22ee381c6efb7 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 24 Apr 2023 14:21:17 +0000 Subject: [PATCH 0493/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0f63d7578564938c70bb747e086fa8c5469db5e2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 457ce68f0..e3ff76dd4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 630f818510b364f1aa678bf9d9215b8ba53820fa Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 25 Apr 2023 09:00:46 +0000 Subject: [PATCH 0494/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e94ef35b59f0f5e963041fa53a807c3661b7ff53?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e3ff76dd4..cb9d27163 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From fff710856375010f48cfb59dcd6bcffcff5db49a Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 25 Apr 2023 19:13:31 +0000 Subject: [PATCH 0495/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3b8483d6fcac9b591b60cf817f91c3b5d53cf355?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cb9d27163..b5e13b8ea 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 1f176fe99bb0681561e06e196a613ccc11b70bc8 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 26 Apr 2023 09:22:20 +0000 Subject: [PATCH 0496/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c25e0ac3e8529affcc84fa6e5bf5cb652b30e81b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b5e13b8ea..09c1b0513 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2f8f78bb2e680349d58a43fe81e3ca23d5956a32 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 26 Apr 2023 11:53:49 +0000 Subject: [PATCH 0497/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7e4ad7029a8b32f88b473083ca6829647788a792?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 09c1b0513..ae86e83c7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b685e596d019888562f251351252c03adeb3e2e6 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 26 Apr 2023 15:53:13 +0000 Subject: [PATCH 0498/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b0d9cd08b4ac04e68f4718c7a1b96fbcca3e6fe8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ae86e83c7..bfc76fd8d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From a1ab3ea74d352c5ae5f28f2edaf9fe5f6b8000a2 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 26 Apr 2023 17:32:15 +0000 Subject: [PATCH 0499/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2bbac4ddd0e7f5954a48870a80216c18769a3c3b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bfc76fd8d..2891b38e9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 9ac78ac140e720af66487b30a52256ba11272cda Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 26 Apr 2023 17:41:00 +0000 Subject: [PATCH 0500/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c1f57ff6ee7a331366bebc80368d8c5c86893f3a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2891b38e9..1ee1df06d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 0d7407851db923a3286e275eecee92d83782471f Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 26 Apr 2023 18:04:09 +0000 Subject: [PATCH 0501/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0e74cfe925201738afc6a9419737c170ec04686d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1ee1df06d..b86be161f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5ebc112d2c0e3b74def22da2824e69a827fc8188 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 26 Apr 2023 18:25:03 +0000 Subject: [PATCH 0502/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@325605e80d59141a7a4adb6a5cef99e7604ef0a7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b86be161f..3e074b50c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 28a196abc1557465c4d38f036cff13591632ee29 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 26 Apr 2023 20:20:36 +0000 Subject: [PATCH 0503/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0afed7836ec5c36133f72eaf8097c5eb4ad2661f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3e074b50c..799069e12 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 0a07607621dd7df1ec627263f9dfa960d973fb37 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 26 Apr 2023 20:32:08 +0000 Subject: [PATCH 0504/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b4f79c59655519cfa84403f12b344c8e9d2aa816?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 799069e12..ea5cf0b71 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From a78c5cad41636a6da770e4af3696cf39f839ae22 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 27 Apr 2023 08:23:25 +0000 Subject: [PATCH 0505/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@04f5584d3a159527ffa463c575e0f9ac9842e67c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ea5cf0b71..661e7974d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4f2c8e7332bf48ea2e7c9dd5f6a293bf2e4b38fd Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 27 Apr 2023 09:25:50 +0000 Subject: [PATCH 0506/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@fd8d3cc4e4c02f8b72b8bca5fa938aca4d7a1d31?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 661e7974d..1e492c933 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From df432e7e475ae012a186ef4ec6acd730f5339e6b Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 27 Apr 2023 10:15:28 +0000 Subject: [PATCH 0507/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1ecbcb0cbe85bbf1e8a256cb3483995ce7e8efd6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1e492c933..e3007206c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 6c21e815618ad435b9b37012a62ea6e9b2e4d017 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 27 Apr 2023 11:58:37 +0000 Subject: [PATCH 0508/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@968252cc948f637ad946f6fce7eaf4c16d51b03c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e3007206c..7d37785d3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7a6d5ef472cee4c8d7fbf76826a4130bf1dcd773 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 27 Apr 2023 12:59:07 +0000 Subject: [PATCH 0509/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@85b5d6cf467ce8e6e2b6b4422be0735708addb62?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7d37785d3..e91e24140 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ce9ea573e07ba5f5947a79fdcc09cd47a3f32a41 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 27 Apr 2023 13:12:22 +0000 Subject: [PATCH 0510/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d2dfba320235426d5dd6b57da16ffc37ecfe1b83?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e91e24140..4d29e737b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 545be24e193d6178c8f5c20149e927381bd285ed Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 27 Apr 2023 15:38:50 +0000 Subject: [PATCH 0511/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7332c49385a88572d89e7281dc5991bb5f964ff8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4d29e737b..0956a2163 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5b223db613c8112d38d223dd7cfd07a8d524c2f0 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 27 Apr 2023 15:57:16 +0000 Subject: [PATCH 0512/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c68d84d80629868ed83044f7afb88adc8a8b6837?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0956a2163..a15ecf0e3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f5c33a6c58faef84e60136e7ad2147a5d369f722 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 27 Apr 2023 17:50:12 +0000 Subject: [PATCH 0513/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@81d8a989d9b63c58a4f771058dbd0d866e10025b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a15ecf0e3..b25338842 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 490341b8e27f58fcc261d3aad171689f267ef3ec Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 28 Apr 2023 08:11:17 +0000 Subject: [PATCH 0514/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6e8df03bc51a703deace4129868cf61dc156ac64?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b25338842..846f636a2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 9354dbe77fb4541303d7d4f84bdbdc57e9e074a6 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 28 Apr 2023 09:48:00 +0000 Subject: [PATCH 0515/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c2e349f3e47b361e4c73b1976ba0e6210cf5d9e5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 846f636a2..c765012d8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 1607d45234fe2fbd7706ef587cec79f855037403 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 28 Apr 2023 09:51:03 +0000 Subject: [PATCH 0516/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3eff06a0e7e0ff88a8e0a28d5db2eb27cea54c8d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c765012d8..cdf2057d9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2db1d0ca511659ed1a08573c5b80b4b66e07d7ec Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 28 Apr 2023 10:10:54 +0000 Subject: [PATCH 0517/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e8333b4787a23f05b47be510ddccfeaf0f18785d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cdf2057d9..bb0c3c658 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 048c36a75b8245e01f42b7aeb11da0829c4f6687 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 28 Apr 2023 10:39:38 +0000 Subject: [PATCH 0518/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d6755529d5d62601a358ff5b1e87037198725bc3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bb0c3c658..f73a1ccfc 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4f600612bbce7942af9c4f68d71d4a8a9e5a6e27 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 28 Apr 2023 12:04:49 +0000 Subject: [PATCH 0519/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@da7012aa15829484f2721e4b3d283da62858e28d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f73a1ccfc..daf726a6d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4f9b3aa37ff6cc0f1451bdc6df08316f75671162 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 29 Apr 2023 10:37:06 +0000 Subject: [PATCH 0520/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@63a71a999a49a3143a70dcfd30b5e6c57829b8ee?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index daf726a6d..571a27d27 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f5d2a56741c6f910541cb0e55f9dd9e280039d79 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 29 Apr 2023 10:49:56 +0000 Subject: [PATCH 0521/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@451632e887ded5581f2650af9d84f8f0569f0e2f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 571a27d27..8d5ef0dd2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d4b13e5c0ae5cc15bb27727e02e642c5b84bf01b Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 29 Apr 2023 11:01:33 +0000 Subject: [PATCH 0522/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8acefa733ac8f7fb3743efde0f87331ceab50ea2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8d5ef0dd2..f0e3393c7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 9f613ae88e98f5d2cf0b9567ab8cae86bfc26358 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 29 Apr 2023 11:51:56 +0000 Subject: [PATCH 0523/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@abbfb1daf30696c445d0a40d6cca24bb10c80a47?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f0e3393c7..dc753c009 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 898ef8c577111adcd38574e901a594d7a1a40697 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 29 Apr 2023 20:13:00 +0000 Subject: [PATCH 0524/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@fc7ce04fd5f5ae014f956bc70fc70dcd6429f552?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dc753c009..328a9912b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 88dd5fa9d8d62c2f7ee51a60f7907669025ed88e Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 29 Apr 2023 20:20:37 +0000 Subject: [PATCH 0525/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f7796e33e9ff2a9dd2868b47d5cd066b3ec3472a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 328a9912b..1c4a35f28 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From a8359d0679d6e16a6627fcb91b70344cbb1fd3d3 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 30 Apr 2023 10:02:53 +0000 Subject: [PATCH 0526/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@04cda0272269d2f88afe93c2c3d79475f6cb25a4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1c4a35f28..4bd6794c8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 306b2e043ab15690536d0b9b5636a8111f7c726a Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 30 Apr 2023 10:17:50 +0000 Subject: [PATCH 0527/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9438d3c8f5a50aa3ef711081dd3aee09256e0dff?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4bd6794c8..1b49dc8fe 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7923ac97d6192b9a2a3120fb95a064126e3c2d5b Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 30 Apr 2023 10:38:58 +0000 Subject: [PATCH 0528/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e1ac44150a944a4c2678dd8564fba51353bc36a1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1b49dc8fe..d66f4a588 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From a8e445b9765b61682d51fec86e82e3aae91fde92 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 30 Apr 2023 10:53:52 +0000 Subject: [PATCH 0529/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@287c0aa115dedeea071fabf4bb2d709464dd00b1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d66f4a588..99b9ca9a8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 600b96dab2503b8c9e0a1e7d20e0d29d3a450cc6 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 30 Apr 2023 11:15:32 +0000 Subject: [PATCH 0530/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4605c4e10ca4b37c9f5b0e15af9d412e654951e3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 99b9ca9a8..cb6c899dd 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b235bdf87740bf5d0145fb2d65393e87d52e36cb Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 30 Apr 2023 11:16:50 +0000 Subject: [PATCH 0531/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f1e75a343a96af5081b87c0a17266e3b4c45c13a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cb6c899dd..91e40805c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f90827cfa01b9671fa4ba02594628daed55aef4f Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 30 Apr 2023 11:45:52 +0000 Subject: [PATCH 0532/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0961c98df0cecbbec4b41c66be8aa8e84d06cbf1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 91e40805c..83cb9baf7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 16923954fca0f3e769345f193f366a4951e24999 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 30 Apr 2023 11:57:48 +0000 Subject: [PATCH 0533/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c018d56d3362780c4a46578ba14552b3a7127ac1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 83cb9baf7..084337e87 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From e9c9d36242eebcf589b3baeddfad094b6afb4620 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 30 Apr 2023 12:49:05 +0000 Subject: [PATCH 0534/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@91eb721a11eebc6b4e29c07108bafe0481eaeb7c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 084337e87..cedd54ca1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 815df4fe7dae4410ca4c963f43b9c43ee2628d14 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 30 Apr 2023 15:21:00 +0000 Subject: [PATCH 0535/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0f60281c61c8303087b506d74f74d5f8659d6bfb?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cedd54ca1..95d351403 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f890abfaf9a8173d4531ce63003423d799b0ce11 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 30 Apr 2023 15:52:41 +0000 Subject: [PATCH 0536/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@743a6fe0a292c9991e2a7e43c648bee505526877?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 95d351403..0ebc77653 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 3a87f360b14a7fcca391af9aeae9936fd01db2e5 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 1 May 2023 09:49:55 +0000 Subject: [PATCH 0537/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1191ed09136a92661b7f83a4d34661431986386b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0ebc77653..dea00dd4a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7217bae217516a1fac35269324a6cbe6d61d0a4e Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 1 May 2023 10:58:42 +0000 Subject: [PATCH 0538/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@68a8bc464569be55cb4544438e2056e93382dc17?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dea00dd4a..d3872bc2e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From e7c1d0b77ca910bc268f6d31080db5aa0038b65d Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 1 May 2023 11:11:15 +0000 Subject: [PATCH 0539/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@900c2756ce71f798dbeac03945a241b729edd98f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d3872bc2e..dfdec62e1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f750a64dc239477ce1a94da311f224a18c03c8af Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 1 May 2023 11:27:29 +0000 Subject: [PATCH 0540/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@349e1b74ac0e74a155b4c02753e66f1d53d345fb?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dfdec62e1..af643f7b4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From cdc1ae6c044d0fca7ca8eab4bc7a933d16b7c28d Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 1 May 2023 11:43:47 +0000 Subject: [PATCH 0541/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@756046c203e4745f23cb0a4fa223e4873c953a4b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index af643f7b4..4b2118433 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 837fca03bca0a38690f1f9a602a997d830f0bc5b Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 1 May 2023 11:44:45 +0000 Subject: [PATCH 0542/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@29b64754f715c90d6c44388c1fe8e4607ab07f1f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4b2118433..361deee35 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2843ad74a13cddc2f4184bd80c8984f477eaf083 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 2 May 2023 08:50:20 +0000 Subject: [PATCH 0543/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@86eaf1d6924c36995b77f6d43d7d5c06f00b4b08?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 361deee35..e3662a184 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 899201ec5810c290ba79056b46830b4231837d23 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 2 May 2023 12:00:52 +0000 Subject: [PATCH 0544/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@280a4b7f8adfc06c09218b82799ac0be0230477b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e3662a184..4e9aa8396 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d28b066fa94e4cbbc4362401c0d0655a8ebeb06c Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 2 May 2023 15:51:51 +0000 Subject: [PATCH 0545/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d65f9bf4e56124544b2255849c66d96d60cddbfa?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4e9aa8396..c1e1e0d9b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From cabee169468c710375b404b9ac182aaf3c209ae9 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 2 May 2023 16:20:23 +0000 Subject: [PATCH 0546/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5d018a6de358d8ac517117b1d7bd89aee7fa0516?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c1e1e0d9b..e720f21ff 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 300396e9ae805f4b6e269615aa46c40e3ddfac55 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 3 May 2023 08:28:17 +0000 Subject: [PATCH 0547/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@647d62b1f03957e07d742557d842f3587a528cb3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e720f21ff..de13cdfef 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c8ad772c7f53b5c2af3a4b61eb290d384eb10231 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 3 May 2023 09:38:45 +0000 Subject: [PATCH 0548/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@26364bd68b62b9dc7e674bbffa30b6e4e8a0677c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index de13cdfef..28d511715 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 1eda07cbab522357c445d6e508fd0cb53ff6df5f Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 3 May 2023 15:24:17 +0000 Subject: [PATCH 0549/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@22f8307dee18dbe5c4926fdc7beda3fef89cd018?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 28d511715..6fb45f0a1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 23b5d9f61bb5ffa5c514c14fa73dbd29409de256 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 3 May 2023 18:24:53 +0000 Subject: [PATCH 0550/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e8adfcad267facfcdd02ddf7f59e61f41b862e5d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6fb45f0a1..e51efefdc 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 9be3899838e952c09be67ebf23ffc5ca697d66f1 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 4 May 2023 09:53:46 +0000 Subject: [PATCH 0551/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e514238e178de3fff41028b2bba820ad7d3d096f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e51efefdc..bbeb1e180 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 436e045d54c760f9962e6a52105af689aa07ba72 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 4 May 2023 09:54:13 +0000 Subject: [PATCH 0552/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e514238e178de3fff41028b2bba820ad7d3d096f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bbeb1e180..e479f8f60 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c7c657d17fae3d104865d81de36cf8bdff7fe5de Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 4 May 2023 10:15:02 +0000 Subject: [PATCH 0553/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c37b77d6e0f78f50df546d012310f385db73de27?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e479f8f60..0afbf2029 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b44ebef87217cfadfec731f0513db97dc0a6ba81 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 4 May 2023 10:46:26 +0000 Subject: [PATCH 0554/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@854f921787f15d4b9c7b1adc28ee0d861499c166?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0afbf2029..d8a205450 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 439e326af505a9d7d943c886308d78ad43d1fd44 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 4 May 2023 17:29:59 +0000 Subject: [PATCH 0555/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c80addb7154e3b1ce6c8edef8f43241b5ef1c4d6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d8a205450..d2e375bda 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 33bdcfad521931ed8400667292b4f1bbe38a747f Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 4 May 2023 17:30:26 +0000 Subject: [PATCH 0556/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2ada5e9a72697a6b0e18ada94bfe6c69b7316432?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d2e375bda..c72852751 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 243c964837fb3188b1825a6df547c938da2e9239 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 5 May 2023 03:39:38 +0000 Subject: [PATCH 0557/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a75bb14a27db26b37fa61a1b753870b4ab774fad?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c72852751..5c78d8e90 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c2f05594cf0739232a60cf797c012bbb7852d46a Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 5 May 2023 04:17:27 +0000 Subject: [PATCH 0558/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4550b9d00c9eb100b829ad17a97e5df431533fa8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5c78d8e90..f6dfd7ca1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 76c08aed5449fc6793b1d282988118bcf566aa6b Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 5 May 2023 05:03:06 +0000 Subject: [PATCH 0559/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5395f2fd52d1a40c5cdfbd49925f4897b8ef43c1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f6dfd7ca1..0ce3f3307 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d4b69661b623693889f9f939c7b9316a8878dedd Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 5 May 2023 09:26:05 +0000 Subject: [PATCH 0560/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@720f3866afd84f2fc36d05ccd8a54012c225e859?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0ce3f3307..720af0a73 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5b800b7f9d2219e7ea64dbe3aa33dc51bac39ff2 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 8 May 2023 16:47:49 +0000 Subject: [PATCH 0561/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@062605cd6d2f85c8f7a1e751ebf3e8814816c032?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 720af0a73..9aa1e5667 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 36bd70abac4e23d6091a44c8c4495f95e430d516 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 9 May 2023 05:45:10 +0000 Subject: [PATCH 0562/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e57195010f33271661e4fa1f2e626296b218eb37?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9aa1e5667..3af930df1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 51c1d8c02e83ab5b328bed0daa9d8e24daea0e96 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 9 May 2023 10:40:49 +0000 Subject: [PATCH 0563/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e891ddd869c91d2ce9bb83767cb3f11c8d8dfa2a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3af930df1..b8cab3e54 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 68e2c9135a78fd24a2d9579738586dbcd04bddb7 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 9 May 2023 16:53:37 +0000 Subject: [PATCH 0564/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@dd744aab187337138078698652048d8b5f7c8970?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b8cab3e54..575efe6f8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From bf5fc3c45dd79b8ea98b0c05ea5ec13ce4db817b Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 May 2023 09:08:59 +0000 Subject: [PATCH 0565/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f67438422d02bae10f245c1dfe5240e80560b3ce?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 575efe6f8..bc840fae8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 32b536a9fa60892f46f78744389675337e0c1ba7 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 May 2023 09:18:08 +0000 Subject: [PATCH 0566/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1700e73111d72afa06cea0b67e53699e790ed653?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bc840fae8..769ae7889 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 7bda55b1c9efb4418d9da5958d9686c00b9486a2 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 May 2023 09:56:33 +0000 Subject: [PATCH 0567/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4d22ed754ef60649f36d37122ba90ac62e0e72c4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 769ae7889..095d3bf8c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5d71f5aa4e4c328ad9af8eb544620589fa740395 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 May 2023 10:13:07 +0000 Subject: [PATCH 0568/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@83fd62ce6c34b21dd53f06f57a21f6846ddc3c09?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 095d3bf8c..b2fdc76cc 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 0eefc7fe26492c68e3c6c897476fda907c1042bb Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 May 2023 10:32:19 +0000 Subject: [PATCH 0569/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ccc4d6bcf9239fbf309d12058402dbe7aa6de928?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b2fdc76cc..a0e2c650f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 825bb6ebf5be131984476f75ec561fb2079f8bcd Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 May 2023 10:49:51 +0000 Subject: [PATCH 0570/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@fc3a69b7c5c0599f6e079bd517acaf8f9b3678a7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a0e2c650f..385c6caa7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 45dcebb2c9a158852ad0b374d3603c849e1d8392 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 May 2023 10:53:47 +0000 Subject: [PATCH 0571/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1ef0f5a3b777ee24ed8336e879085100d854464e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 385c6caa7..1ecdced0a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 184040d944c73bfe1954a711aa74b6145990ebe3 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 May 2023 11:07:38 +0000 Subject: [PATCH 0572/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6d1a48bd46c187ed54c0c4704d5c067efce7339a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1ecdced0a..5c582240d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From a6ab00dcbbe232c7b4c9841d0badd13042da3d9c Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 May 2023 11:21:20 +0000 Subject: [PATCH 0573/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@796ab8ef23db0ed95bbfad5856b8493a447f5b3d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5c582240d..c96e8fb31 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 91a659bbcf6c624d233085083450c844f42f6452 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 May 2023 11:35:33 +0000 Subject: [PATCH 0574/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ecff85b4adc3c5550577886b8f8795ec11144929?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c96e8fb31..9060b9ac5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c213eafbd64ce970da679938ed6a5a5b2ae5fd2c Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 May 2023 12:02:37 +0000 Subject: [PATCH 0575/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@036e58e4796a0a8adb73432de821898c41d06106?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9060b9ac5..d38e3d5ee 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 99dbca7ea8362cf17eeeab38074b887deda1bee5 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 May 2023 12:11:50 +0000 Subject: [PATCH 0576/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4f25ddec5b40f11268d37da231a4912a61fdfea0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d38e3d5ee..f417a3df8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5de1f6a7e4158321f09bffa2c572707513178b3b Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 May 2023 12:20:56 +0000 Subject: [PATCH 0577/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5a3004d700e65d470b9c4e9714feb0192caf6c31?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f417a3df8..38641e76b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5c4563f3b6d02563f350a5847d4c08c37738899c Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 May 2023 20:45:13 +0000 Subject: [PATCH 0578/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@580f251accb96de4456f94fe9cccc00bf1254f4f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 38641e76b..f5bf87053 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 86ca0931d91a4de0607f1963456ac58339857353 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 May 2023 21:12:02 +0000 Subject: [PATCH 0579/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d9f4e04d80db098393deeb1d4a268ec98ddcfba8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f5bf87053..99160a148 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ea97f2e5c3fe3aad6c0ab06ea6a265a618c1c954 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 May 2023 22:36:18 +0000 Subject: [PATCH 0580/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c4636c4041810bfdd4fb4d6cae20104d0b36b76e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 99160a148..d44f68a55 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 8ac7cf1e3354f4f105e3a6c7a6b13cda0f03fefd Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 11 May 2023 00:08:08 +0000 Subject: [PATCH 0581/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b8431c88a845f7eaad3fbd8fbf3c50d3a8598c4a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d44f68a55..264f1fe55 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From dfb4e4524db2297023eb411cd714b8daf570f8f0 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 11 May 2023 00:29:18 +0000 Subject: [PATCH 0582/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ed1ed6b2c670f5269947bae400991bc3cf19f574?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 264f1fe55..bf7e8ccbb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 8ca54232e1f54120d9fcefd23a80e24a140c4e6d Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 11 May 2023 08:55:16 +0000 Subject: [PATCH 0583/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bc2890dd86becd1960f97d54a6a3236de6bfb1f3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bf7e8ccbb..754f22693 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From de7d9db7045b78a0edd5740c6807004c3e42a6fb Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 11 May 2023 09:59:18 +0000 Subject: [PATCH 0584/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ef224b46735211548e5faf6b3c2f1020214dd512?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 754f22693..7d7edcff6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 473dcc8911596528355cd1276eaa45f1654089b1 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 11 May 2023 10:16:36 +0000 Subject: [PATCH 0585/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@457832519573c5d049d5eeffb4aa3434ef45afc2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7d7edcff6..7d82ec3b1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 286877fa25b62eeff1784c35abe53449037815c1 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 11 May 2023 12:37:57 +0000 Subject: [PATCH 0586/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c58eea8edf84a7e18f38056738bbf9a0011aa659?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7d82ec3b1..50c27bdde 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5e98ee54601f34705f3922e2e2ce8b1368ebed59 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 11 May 2023 16:54:09 +0000 Subject: [PATCH 0587/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5857acd6aa04d3c9795bf7158e16cb2af04742d5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 50c27bdde..6871c2ea4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From b3408e8d1f8854bb5f6ce49ea05db78e8d79506e Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 12 May 2023 13:11:22 +0000 Subject: [PATCH 0588/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8219ec51d6814e9506b1bdbbf24dab23baa7431d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6871c2ea4..a9a255ca8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 68354438c6655e1475f2875df13d2750cec10931 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 12 May 2023 13:38:10 +0000 Subject: [PATCH 0589/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1ba7825abe5dbd72f7862e3038cc6115ea6ae14f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a9a255ca8..43149b171 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From d01a1373b5f11cbd56b3f602024452c75408129d Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 14 May 2023 13:52:30 +0000 Subject: [PATCH 0590/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f1430dc9e16e31a9f8a3897de82ee82a7d5911d4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 43149b171..4dd4e61fd 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5395863f95fbfeed6b3b078c4decf68d4182027a Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 May 2023 08:06:11 +0000 Subject: [PATCH 0591/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a8c98f6b687e5346ab0ffb14f7478fc481e176d6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4dd4e61fd..d888b2b83 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 3f277de3744645062da2bd5a7cc236039ab40cf9 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 May 2023 08:09:40 +0000 Subject: [PATCH 0592/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ca2e987b735b1186ad6ab129ef79bf9de4f211c3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d888b2b83..1eac71fb1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 292a94dad1c69390b79bffef6e74684bd8e72e16 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 May 2023 08:14:05 +0000 Subject: [PATCH 0593/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4f4bd4a1667044ecac33a7317f9b8869f62d9514?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1eac71fb1..a417e94f3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 405804327ac5cf90e903878e1347cf182ebda17f Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 May 2023 08:25:49 +0000 Subject: [PATCH 0594/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e4f73421c26076db5773347f0510e197b8316405?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a417e94f3..bea33c18d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 49384160cb3ce3626bf1c997ced984c37f3e2886 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 May 2023 08:56:59 +0000 Subject: [PATCH 0595/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c2798ed1e0fcb12f701a7140476f83458c440077?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bea33c18d..c14338cc9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 28f07abaa655a20f655973db937b20931573f3d9 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 May 2023 10:48:49 +0000 Subject: [PATCH 0596/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@126fd04bfbe35ee1527bbe065a36498dd2174626?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c14338cc9..68d4f4aed 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 45cb453d5b2d69d7ae48c337c150ac3eecd0ed4a Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 May 2023 10:48:57 +0000 Subject: [PATCH 0597/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d73c2bc258606e0f45555ea0545cd73048dd9011?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 68d4f4aed..9353621a0 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From fac450e4b488cbf7db3e982a83d199217417e7e0 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 May 2023 11:21:38 +0000 Subject: [PATCH 0598/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@db8689b30639d2b55979536c478e10517de63bc8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9353621a0..a5d0aca34 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 63c0e10e3105440535e5b236772c97ad0d091a0c Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 May 2023 11:57:17 +0000 Subject: [PATCH 0599/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8b8890503339dcd1532c69995e7df450824b248a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a5d0aca34..47dfb4654 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From c5fa0504b75118ac710cc3186540d86fa77e0d3b Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 May 2023 12:54:17 +0000 Subject: [PATCH 0600/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bbf54445e932ea9c9947ab12192abf5788dedd88?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 47dfb4654..364b65ffc 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 6ae57ea785ae65482ed2051271296af69e2cb5c4 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 May 2023 17:13:34 +0000 Subject: [PATCH 0601/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2a9c6d110036887d56cb7d82e0037d16b8a541a2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 364b65ffc..aba150a77 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 9fad8a361b3b7fd23c366404b3fab5d05a625e81 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 16 May 2023 09:28:57 +0000 Subject: [PATCH 0602/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@12b042d9b0f67e5383055a2a5164ced09b3a5aae?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index aba150a77..c1b964c5b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From cfe4be6b758cdaa8e0902d4e33c180cf502071c5 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 16 May 2023 10:29:19 +0000 Subject: [PATCH 0603/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@803f82b5ac6bea531ac04779a8c551e5b110e611?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c1b964c5b..ba2a62fb7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From ac29de46e09d2ebb77f58a3c0f8c87ff1fa5dcff Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 16 May 2023 10:39:11 +0000 Subject: [PATCH 0604/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f0be3a08fb7a27a549400d8a1ba395a3c8b7a12f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ba2a62fb7..9222ab34a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 14b2392464464b7c94373345714bfc8d3f8ccc28 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 16 May 2023 11:13:17 +0000 Subject: [PATCH 0605/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1e7e62af7a8a635aacc1c85ae4813412cd0c8b69?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9222ab34a..5a8c95c20 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 4e5ab2633e8d997804340fb5c946bc8c43053ddc Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 16 May 2023 16:37:29 +0000 Subject: [PATCH 0606/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3c0cfeda438bb51966069b77d288103940e047dc?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5a8c95c20..2e8dc6b76 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 511d7b18650f1f91afa85c928cfdd26bb698612d Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 17 May 2023 13:52:44 +0000 Subject: [PATCH 0607/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6704d474bf926817956ce2ae172f60d657ef62c8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2e8dc6b76..8411c4716 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From dbccc91a209e01aef4fe845f124a83c2846f6373 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 17 May 2023 14:22:20 +0000 Subject: [PATCH 0608/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9d6db24418efc2c834c2204943ec806e30efa9a9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8411c4716..3c2f7b7e9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 81b120efc7f901b29ae61d4f01bd33a3bb726a34 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 17 May 2023 16:41:39 +0000 Subject: [PATCH 0609/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7e0b66cfb92aa05363ecb7877c6d69bedf5bb074?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3c2f7b7e9..45898cb97 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 89e7a3b1e2e4d575515679902dbeb60c9f7f789f Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 18 May 2023 06:15:26 +0000 Subject: [PATCH 0610/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@eb36896bac2e5c99d9687796eb88bc88484a5d58?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 45898cb97..08d7cb9ae 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 1b4be3fe714126512d126e17879d44af8977c71d Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 18 May 2023 10:58:18 +0000 Subject: [PATCH 0611/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8e61ade3e22944b8ea0d1de9137060ad0094a7df?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 08d7cb9ae..2b2f2ad0c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 80f11bd66f5c7ed7fe2341c9137ccd9c95563046 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 18 May 2023 11:31:30 +0000 Subject: [PATCH 0612/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5265643765ea45e164b1a476546156ac78f03ae0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2b2f2ad0c..599b5df11 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 75513f318795bd8ed477c07014fa4a87566f777f Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 18 May 2023 11:39:42 +0000 Subject: [PATCH 0613/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@50e9a2cca3dc0f2ec97843cd19318cb4d9ed7064?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 599b5df11..80109e0d9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 0d132ba98c441c9a63f2afe11730038554ae4a5d Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 18 May 2023 12:15:24 +0000 Subject: [PATCH 0614/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@32d6a4ad0a3fb5ca7db7d70d2f6a06d0121cfca3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 80109e0d9..4ac1800bf 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5494fd452daf066a7705e37ba567ece4bd4409bb Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 23 May 2023 08:44:36 +0000 Subject: [PATCH 0615/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4c70c4e4cd79c7c5baf66ba405027c0036867eed?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4ac1800bf..eeed56bb3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 93e140ad31d3d8689b76658bd026dc67a3d052ff Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 24 May 2023 16:26:41 +0000 Subject: [PATCH 0616/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3886cec0a0fa75b8481a40d4ff71572f6e6cdfaa?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index eeed56bb3..b8bacdfc6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From a3d635ebb555b756f2a43d7a9335113c01061e10 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 30 May 2023 15:32:31 +0000 Subject: [PATCH 0617/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d5c4f2248eafecbd9fbc9da85b78f24e407782a2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b8bacdfc6..45b14297f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 55cc7e9a45281ac1bdc0f10705943ca7285f31cf Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 30 May 2023 16:30:09 +0000 Subject: [PATCH 0618/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4db2099b99cd47a610bc606a35b3e7993f8e5962?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 45b14297f..f34708ba3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 2f75630464a618585a2c93bb734b910331406ac5 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 30 May 2023 17:29:34 +0000 Subject: [PATCH 0619/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6e05eb4c2a13bbe2292398d69c52f52013c47378?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f34708ba3..e6b1f3932 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From db8ceb2933199dd6b18326194353450c1b4cc4e0 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 30 May 2023 17:31:56 +0000 Subject: [PATCH 0620/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@71546108cacaeb08e859467400558f2ddbe68434?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e6b1f3932..022e34b7c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 5fec40870454d995ad21fa6fe3ffc1517265e09e Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 2 Jun 2023 09:50:34 +0000 Subject: [PATCH 0621/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@859eb1b4189d2ac139bb5c6964d19d2ef22918d0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 022e34b7c..56e8952e7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From a0ef1c77ed463ca89822b129a501518ee58bc641 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 2 Jun 2023 10:15:05 +0000 Subject: [PATCH 0622/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6f47b8516574edb7bdc607e053988b1afb717afa?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 56e8952e7..f7152dc12 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 490920625a6453698f02f9c40ea2e6ae0bf89274 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 2 Jun 2023 10:46:34 +0000 Subject: [PATCH 0623/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@93d9f89744bc99cf32b7b220db746057933afa1e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f7152dc12..921845718 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 73dffffb616350e467ff16a5d7282b86f28a5bec Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 5 Jun 2023 09:44:09 +0000 Subject: [PATCH 0624/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7e2bf6d37d16a5354f29a14b6d23941fdb99f83f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 921845718..1d87dec0a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From bfae8e49b5a33252d2faa842e1d7c5c9737c8809 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 5 Jun 2023 10:28:31 +0000 Subject: [PATCH 0625/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@230f3dd0386808a396c1d99142430a6a0fe9ab3d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1d87dec0a..0c1d4451c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 544a1a1bc0c7c4cff409c35aab8cf6381591437e Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 5 Jun 2023 10:33:05 +0000 Subject: [PATCH 0626/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cb13b33d79cdc91881811e914d5ef9bd50d64a9f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0c1d4451c..c215fa995 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From a2c66566ea014014254b1f4672ac83524f01c79a Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 6 Jun 2023 08:57:37 +0000 Subject: [PATCH 0627/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d389a9ca4aba37adc0eb8a23171b4e2271dacf90?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c215fa995..3fafc8747 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 295506a5a8895c0fa7b7f10ac131d5eb700542ef Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 11 Jun 2023 16:40:16 +0000 Subject: [PATCH 0628/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1acb03fff0c5d0b18161a5f2d6432f9ff66b0f34?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3fafc8747..b4a1ff5e4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 88544aa1d7f959f39fd4eb8d5654828f0de6f482 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 13 Jun 2023 07:10:24 +0000 Subject: [PATCH 0629/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3a2ac220e33fa26faea59d24bea74b41a0c19204?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b4a1ff5e4..396c6c5cd 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From bbce6433d86f86c768581a849c8bb80edbac029d Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 13 Jun 2023 07:32:28 +0000 Subject: [PATCH 0630/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f6f515c6b6d84abbb135849ac4a9e598a2abbb52?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 396c6c5cd..c614d58eb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 714c9207112c08486e4d7559f9716464882b5fda Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 13 Jun 2023 07:36:27 +0000 Subject: [PATCH 0631/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bacedc7bea766cf18deaeb77fbeaf5d0bca5f842?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c614d58eb..7b6988813 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From f231d794fa6e406527e2b63bb1ded951b284c5f2 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 13 Jun 2023 08:18:03 +0000 Subject: [PATCH 0632/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@455b728b04f76acdeca2725b885d374e699b1dd6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7b6988813..10b2d6f4f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 336405ba977aaa347dd3fa64541cb9ee0b2a18c8 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 14 Jun 2023 07:46:34 +0000 Subject: [PATCH 0633/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@08f0125f9290c9391d15befccdda3c1d7bdefb3f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 10b2d6f4f..9f381a6cb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2382,7 +2382,7 @@

    Transactions

    From 8662fe34211cdc7f372e5b5e61b8834e68cc63c8 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 5 Jul 2023 09:23:04 +0000 Subject: [PATCH 0634/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d20aa0af56a273bc08c077914a3d098b154b8b41?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 282 ++++++++++-------------------------------- 1 file changed, 67 insertions(+), 215 deletions(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9f381a6cb..69c005436 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -4,25 +4,23 @@ - + Convex Command Line Interface - - - From d4f4a13526af1bd00ff0577eb898d8df9a0288e5 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 5 Jul 2023 09:53:39 +0000 Subject: [PATCH 0635/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ae76d67322bd7c77b84c87aba5dbcb30d460574a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 69c005436..0e7e99b2d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 819ae1f9dac6a4faf933f5d5963b4c63259f022e Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 5 Jul 2023 10:55:45 +0000 Subject: [PATCH 0636/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b9390451e88f56064af8936dc71f411415f6fce5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0e7e99b2d..4d9ba46f4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From b332d9c108e2772b4955042290358e706ddba8b7 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 5 Jul 2023 11:14:47 +0000 Subject: [PATCH 0637/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@411500fa663de464a85c03411a2b4429c74c94d1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4d9ba46f4..0b505476b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 950e2de83c4da7c2d971298ed845af3bbfcfe309 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 5 Jul 2023 16:07:15 +0000 Subject: [PATCH 0638/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5dec49ae2fcbceaebae0b3c73769396f0438f214?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0b505476b..8c18cff7b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 3f0f5fd39d88604b210d9722b6b66574c768ddb1 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 6 Jul 2023 09:55:16 +0000 Subject: [PATCH 0639/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f8c76ef201d1709d208780a7f7b995a060930daa?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8c18cff7b..b2f189bdf 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 7a79b451aee215b052f21d31d45a75ac0258b505 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 6 Jul 2023 10:32:09 +0000 Subject: [PATCH 0640/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@12187aef0f720401a5d1f35f5e31165d6335a3d3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b2f189bdf..8fa5b08f9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 9ba15a363b9e1f11aa600bbc726a3bcbaca487ce Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 6 Jul 2023 11:01:39 +0000 Subject: [PATCH 0641/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f8a9cde08c0b690d3c2ed4609f4f2d4567c1e359?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8fa5b08f9..64ed59f27 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 2d9447fcfb3dfa6eacbc76d5386e4a1256805e08 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 6 Jul 2023 11:05:06 +0000 Subject: [PATCH 0642/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b38e00fb5253a8370b7f41ca85fe484f5ed3fbaf?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 64ed59f27..1c4adba11 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 2b9b7c8e1badeda24629264c93ea18fa82270740 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 6 Jul 2023 11:21:20 +0000 Subject: [PATCH 0643/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7f8c8fa57209a0e2bffe33f4e5d6e9d19ace16aa?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1c4adba11..9e84fbae9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 21fc605c216b860617586ad72465811eee06bcf7 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 7 Jul 2023 14:13:33 +0000 Subject: [PATCH 0644/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@76262e48ec0314f4fc8210f840213493350ad948?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9e84fbae9..a9b3d712d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 701a1aec6f25bc7e0d453275903d1f460893c890 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 7 Jul 2023 14:36:51 +0000 Subject: [PATCH 0645/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4eebc830275a8780716e22842a642f3a252ae1d4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a9b3d712d..d28550fd9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 3097b010539e1deff40652c7f6dd9df45e940d05 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 9 Jul 2023 22:46:26 +0000 Subject: [PATCH 0646/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e3ef345da99c3f3baad174fa07b44d2a9cbdfca7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d28550fd9..d5e36310a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 084e60e242040608c59b69c70194826894021949 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 9 Jul 2023 22:48:42 +0000 Subject: [PATCH 0647/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d4f1a147c66697a7c0bf647ff5075e97cb489556?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d5e36310a..0224c7d5a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 57a2de424cd7ddbce9af43c1091b46fe6163349b Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 10 Jul 2023 09:04:42 +0000 Subject: [PATCH 0648/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2837da67a78f8662df1f498f8fd28e848f7c9135?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0224c7d5a..bbf57dfe8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From da517434a0fa33245ebd665d1e8cda1e33dbd2b4 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 11 Jul 2023 08:44:24 +0000 Subject: [PATCH 0649/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cb099a24a7fb8d4eb04588e9e2e16d79732786db?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bbf57dfe8..8d2c8aee8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 168dce45bd5dd212ad077a0135487ed4820081f5 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 11 Jul 2023 08:57:11 +0000 Subject: [PATCH 0650/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0a6933100bf6f4a1f8e21dd8d94f6789e62c1bf2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8d2c8aee8..fd92ef9bd 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From b2778a6e571b7391285f4d96d1f7509b4ced73f1 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 11 Jul 2023 09:12:11 +0000 Subject: [PATCH 0651/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@004e1c4e053956731041b35852cd9b2edfbf0a05?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fd92ef9bd..c0f2424d3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ba367ef6e42ab442654d105304fb84e0617d995b Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 11 Jul 2023 10:05:32 +0000 Subject: [PATCH 0652/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c750995f889f0488caeb55b848732a90a04d47c8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c0f2424d3..f8f563b3c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 0d528efce5cfff9e5418755d1b6101ba64b36a85 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 11 Jul 2023 10:40:08 +0000 Subject: [PATCH 0653/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@805890557a7434a63ca23f58f7b499de92f5bf36?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f8f563b3c..fa3f7370b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f8fe346101cb084c95f1651f152970e25cde8250 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 11 Jul 2023 17:26:27 +0000 Subject: [PATCH 0654/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@023497860d712fd4bef29dd8a5f3be62c66b833f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fa3f7370b..e2ce38994 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From e331db18a5becc7efb08cc26590f2dd71b3714dd Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 12 Jul 2023 00:39:22 +0000 Subject: [PATCH 0655/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a90eac808d4d7ff44c7206ae845d694aa872708d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e2ce38994..49eaac60d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From bea9bcea0be8fed27be04c36c36f8e664d039ee7 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 12 Jul 2023 11:03:18 +0000 Subject: [PATCH 0656/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d66653c34af992dff644241b7c9f8926e4016ab6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 49eaac60d..fb0ad85f3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 624ca5d3903b0e4c01c81e7d7ebd8e153f8f1484 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 12 Jul 2023 11:06:36 +0000 Subject: [PATCH 0657/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4283e698c081041e065ae0df8404dfcaa594ce86?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fb0ad85f3..eb1a60470 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 69abe96d2c3ed7089d581cf63e24b9050ce95f87 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 17 Jul 2023 09:59:06 +0000 Subject: [PATCH 0658/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6c20db6c170c830ffa6f5f34b9bc987913158143?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index eb1a60470..3043abd92 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 772329f26fa33cf1fd4cdf374f24d5543739801b Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 18 Jul 2023 10:18:21 +0000 Subject: [PATCH 0659/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f5da4d8be286b7c4d4fbb85c4b13777296c3561d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3043abd92..33022cac8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 15aa60e2d57a23c41565a2f5614ba0ad0c15258e Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 18 Jul 2023 11:15:47 +0000 Subject: [PATCH 0660/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f030d20e0220a5445a96b4118684ae9bccd4db4e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 33022cac8..4c8710d1b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From de7bdcbb13dbc0951738f6034a73a766c1a8f97c Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 19 Jul 2023 10:37:30 +0000 Subject: [PATCH 0661/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f7695f87a657604fd3a08015eb8d9fd6424d4a1e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4c8710d1b..a2c9aad0b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 405c613a6bb7844d6df1368ea8c004df70244f5e Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 19 Jul 2023 11:38:10 +0000 Subject: [PATCH 0662/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@81556b75e2f9f279da20025ae48ebdbdf89cd0b2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a2c9aad0b..380f5fca8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 04d993d039fbb0b056da0e4e09d06cfdd0d73d04 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 20 Jul 2023 17:09:21 +0000 Subject: [PATCH 0663/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6c0a1aea8b4fe313f166c237aec826dfd3d91ba0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 380f5fca8..c9c744470 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From bdbe36005665c2a0f50be52afa2128de78eac307 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 25 Jul 2023 11:04:08 +0000 Subject: [PATCH 0664/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@900b32b0deca4e0c49d8baca472333a01dd25379?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c9c744470..45030e369 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 69a372acf3649ec3ccfa347599b447f981d2e03b Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 26 Jul 2023 19:18:47 +0000 Subject: [PATCH 0665/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4a575da67879cc116b774b1faaeb3af03268efca?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 45030e369..ab96e2222 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 79a9e3a295fad7b34cd684e277051a7465924327 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 26 Jul 2023 19:19:31 +0000 Subject: [PATCH 0666/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e2ef2bc0c478a9dacf2209247598b331f02f62bd?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ab96e2222..d9c1b62d5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 75938d140bc2d7b245b8d928259715913e53671c Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 27 Jul 2023 14:53:22 +0000 Subject: [PATCH 0667/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@41123c042fcbe158b4aa613c71ed1146f6ce5b1e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d9c1b62d5..7aabb331c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 7fb964325377bd9ad323d0de243a703b5e88e510 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 30 Jul 2023 13:44:06 +0000 Subject: [PATCH 0668/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cf4a96bf78e70c448a25bdd8f20cb6fce5af5c87?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7aabb331c..14e131633 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ecbdfd2be325c865105b125af84f567633800390 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 1 Aug 2023 23:41:22 +0000 Subject: [PATCH 0669/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4f11f85afbc3233ec203ea4b5fc0792bbff88a54?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 14e131633..d75d3636c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 4a813e85579709062a414510723dfe473af22b69 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 11 Aug 2023 08:28:57 +0000 Subject: [PATCH 0670/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5be2fc92c3834e7458d32f00e9afd4b343252243?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d75d3636c..76f4020cf 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 8652f265f18f7658fde93162fe32809f71067240 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 11 Aug 2023 09:08:56 +0000 Subject: [PATCH 0671/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@afe3bcf4c56e9912a0ef8bd2c2ea59b262f943a2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 76f4020cf..aa07b4ba0 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 7b5d76003e0c623e4460207fe86817e0b52af8bd Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 11 Aug 2023 09:25:14 +0000 Subject: [PATCH 0672/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@53df4211eb8dac17727bbe88f0b7dd9e69657e09?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index aa07b4ba0..10b486768 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 0c99263adb0fa90a3b3164fe77538776454caa79 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 11 Aug 2023 09:34:44 +0000 Subject: [PATCH 0673/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@aba330837fc3bd004cd1bfe929cf716c7d3511c4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 10b486768..7e3762efe 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From fe3234f35211b162b6c3624cdcc186f30aa4d935 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 11 Aug 2023 11:03:41 +0000 Subject: [PATCH 0674/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@49de6fd364825e1ce181737317954ff4cd507041?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7e3762efe..c236a37ba 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ac24cfd617c0126e4569e5cef742e5149568ca82 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 14 Aug 2023 09:08:53 +0000 Subject: [PATCH 0675/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@da8e75bdb21b46a3bb47091e703b6bcdf38d079e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c236a37ba..9f477bb7d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 40bef506e7f91560cfdd58b634af42cc642b89cc Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 14 Aug 2023 10:05:00 +0000 Subject: [PATCH 0676/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e281e6c7932696e34aa4583ed093b0fd9de9d17b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9f477bb7d..e9f3c909b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 242e7aac7e89bc7dc11949337e59630556652f82 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 15 Aug 2023 12:48:15 +0000 Subject: [PATCH 0677/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ff650b4fb43efe2dc96f5f4f7dc940c83bc2fab0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e9f3c909b..947fc7b80 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From fb62d7fa9ecc492c7dd3b0c7f48bc5c01db8fea2 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 16 Aug 2023 17:10:21 +0000 Subject: [PATCH 0678/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@67f5b758157f7f7ddb9c16e1710f88fb4fe087cf?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 947fc7b80..398e4d145 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 32fc5afde5e33500982d4977b756246ef3dbed02 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 17 Aug 2023 11:57:55 +0000 Subject: [PATCH 0679/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@80716b11c1073f19758187b9e5249edc9b933347?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 398e4d145..6f1f98dc2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ebc2db2ebfe4577bcf5f64b73a9e4e480a3fe80d Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 17 Aug 2023 12:22:44 +0000 Subject: [PATCH 0680/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4ee361e504f854ddc582b8df425335f41dce04f8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6f1f98dc2..a894e6c0e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 83618724e7010a7e58650d96807360ce3b077415 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 18 Aug 2023 11:35:02 +0000 Subject: [PATCH 0681/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f009c5ccdd4d968848673d5bb265f3a2c83921e9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a894e6c0e..cec4d3854 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From eec76f0e7a510ee7bc21393ec2658196241f555f Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 18 Aug 2023 17:00:36 +0000 Subject: [PATCH 0682/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9d79d880e95ebed9b7ff6f4a9242d78897b51a27?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cec4d3854..9d14e8402 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f63c456d33427f5c778c9d38613eb26f8ad2e65a Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 19 Aug 2023 10:49:23 +0000 Subject: [PATCH 0683/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@40bd4b8d08c9022848c9b7c606f7798f35f367e7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9d14e8402..443626e0d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ea5f752bf0453404b5191a14941b03ce7d5fedf1 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 21 Aug 2023 10:22:23 +0000 Subject: [PATCH 0684/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@31c7a8a5d810858684fd437619c487a44dbffeb6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 443626e0d..2397169e6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 1ed02d1d8abf8933778543c8d43d258589452141 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 21 Aug 2023 11:06:37 +0000 Subject: [PATCH 0685/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a2a99ff5de673d83c27ddc1911de0133a6752184?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2397169e6..dba457cb0 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From cba9f14be77b744ca8e6ba5f9f6c397659230f67 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 21 Aug 2023 11:32:13 +0000 Subject: [PATCH 0686/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@db8e3f3375737570baefb120b568cb443c3ef114?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dba457cb0..de6b45c3e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From eda2d062a910af8dcf70c854c95f33b23b3fa0ab Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 21 Aug 2023 12:01:44 +0000 Subject: [PATCH 0687/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5262d304ca865f82ddcccf0bc3493c13644da08f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index de6b45c3e..2401eb989 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From a8cbbcbf5f3d7ecda1e51af91ecf465c5c64f822 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 21 Aug 2023 16:52:39 +0000 Subject: [PATCH 0688/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3133b513750b70544293df603c00e777c7cd47a7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2401eb989..67e5601af 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From a364a272f0cf59f9943f4bbc3ff0e398f9cb3915 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 21 Aug 2023 17:44:55 +0000 Subject: [PATCH 0689/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3aff445aa9008620cb605235f7d92746d4ab79a9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 67e5601af..7b8533796 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 587651527063ca5cf53ef5b52896773ea2eb8096 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 22 Aug 2023 10:30:57 +0000 Subject: [PATCH 0690/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8f25f04ef1637fee973ebc2301da3aee5dcf68e0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7b8533796..71410cba4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 6251773938ebf60bf9a0bdcf23cb7b5b510afac7 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 22 Aug 2023 23:48:13 +0000 Subject: [PATCH 0691/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7b57d678c2ad8b7327970a685aff70bed0b0ddee?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 71410cba4..563ac9ec3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 7e2e4fc528e7c77f3d2ef3ce465d56eb3ec9f063 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 25 Aug 2023 00:00:12 +0000 Subject: [PATCH 0692/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@95f4916a923950aecd84c372806299f88416b975?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 563ac9ec3..bac4510a4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 28ca98189712886007080c9e311e7c5026cc3df8 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 27 Aug 2023 15:27:56 +0000 Subject: [PATCH 0693/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@550d8140f59254cce2c5fb0c1900c8021e7e3568?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bac4510a4..1de7d9e35 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ac5182911d0c736ce9606ead9b9c051b5e0a18be Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 29 Aug 2023 09:27:19 +0000 Subject: [PATCH 0694/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@66e9d052d786d2b35b8800d14f474cbaf15ba777?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1de7d9e35..efc74d6c4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 159de61dccc43877eb0c992b967dbcacaaabef8d Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 2 Sep 2023 17:42:58 +0000 Subject: [PATCH 0695/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3dd7b7c7924e6fd502b3d5f4dfdef075bc0b64d2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index efc74d6c4..dbaa84cb3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f29ecbf79041fccfd9609b9c5800a2acd87d9b93 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 2 Sep 2023 19:49:26 +0000 Subject: [PATCH 0696/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@db7909ebca99aac2fa17acd575f015fe468660de?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dbaa84cb3..c55133ba8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 49ec2f436bc9e08de2e55c0c42bd3a2469101544 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 2 Sep 2023 20:14:51 +0000 Subject: [PATCH 0697/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4cb1295ed698b9d26553857c57cac39bf22036f5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c55133ba8..e52c3ea45 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 582fea08f9c0470a26a7ea1c0b9054d5d2dd2b35 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 2 Sep 2023 20:20:31 +0000 Subject: [PATCH 0698/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f80670d46521d3aa2a6f8f40ddf8519547438b3d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e52c3ea45..74307aa66 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 8b66c9c65f96e4854e00d751b9ff5b0016362370 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Sep 2023 19:36:18 +0000 Subject: [PATCH 0699/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0fcab56ba6be4f356796b1ffd5d02590c6a59dc6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 74307aa66..05c138c08 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From c8fc752faf41f7d0865ea7d816e7f10fb2e99618 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Sep 2023 22:33:55 +0000 Subject: [PATCH 0700/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c510671be93c2c6198a4c080b179a61889d93fac?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 05c138c08..a5c6ebc33 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 39830000a1b321cb9ea59887301bfa1f5cb6a825 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Sep 2023 22:34:07 +0000 Subject: [PATCH 0701/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cd7a856d7dce83a2eaf1eb9890c9b8ae121030c2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a5c6ebc33..979cc9e97 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 92e15be46a7d9308ea2db599fbeb7ab35eabc460 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 7 Sep 2023 15:01:31 +0000 Subject: [PATCH 0702/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4cb18b45aa72b5a0789ea24a6c2abdbe807c3f2d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 979cc9e97..dbd6b4376 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From b50633f9f781bbcffa472f6afcb3bf14625247b5 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 8 Sep 2023 09:50:58 +0000 Subject: [PATCH 0703/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@00d6283fc24215f6c5ed92d638602542606dfdf8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dbd6b4376..5a42aeb5b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 1f7f3a127d2e910dbdfd779ab037e38663c5545d Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 10 Sep 2023 16:51:03 +0000 Subject: [PATCH 0704/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6a7b795aa6859c93d4fd8c37168b3580f5bac6b8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5a42aeb5b..d534febed 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f4def7c29162d27912ca763fb269ca00dc4e775c Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 11 Sep 2023 07:54:19 +0000 Subject: [PATCH 0705/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@096f232a77ec83bf6fbe3c9ec1cfb702103ed2d6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d534febed..8c6f35e88 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 664e4f25300c18ca5b7a4bc92fed9c193e04b7fc Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 11 Sep 2023 09:06:20 +0000 Subject: [PATCH 0706/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2c0b327300b653dfd04b09c7d3ea5bd9fbacf2e5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8c6f35e88..2ab112fe1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From a712a152d5f3bf233b4ab0c5be274e0d429e3594 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 11 Sep 2023 17:04:12 +0000 Subject: [PATCH 0707/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@941f419618789e385fa72adbe149d9f31550c735?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2ab112fe1..7b436da46 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 0bc9a852a7a838d8dee5eb318babc767e5d62750 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 12 Sep 2023 12:42:22 +0000 Subject: [PATCH 0708/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0e0ef06c3c279706bc488c37f4fd60aa0b2947ab?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7b436da46..6e02cb5b6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 9643542bc3755de1c9b126b25be6a5e1dabaa342 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 12 Sep 2023 16:51:52 +0000 Subject: [PATCH 0709/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b3fba751453dc4979b904a1f6098eef6e8baebbe?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6e02cb5b6..07d2ea528 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 7f311a192c95640714dd66f0b051e3704700c72c Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 12 Sep 2023 20:13:55 +0000 Subject: [PATCH 0710/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5a35fe2a2dbbc560b0ec2225eed97fe88f329eff?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 07d2ea528..f957fb620 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 44613c4051a3a81822fd1aca979ded51c60fca36 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 12 Sep 2023 22:40:12 +0000 Subject: [PATCH 0711/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@827ba765e1f4ececd529aae3d6e1934492b30a16?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f957fb620..d81eb4a5f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 525201870647c1b7bac49d175c0a7727bd1f9d54 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 12 Sep 2023 22:47:38 +0000 Subject: [PATCH 0712/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@30615665c17924ba71fc29b78127419627a42039?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d81eb4a5f..95ced8f0d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 4bf3e89fe5f7bdc53ffdf9173118d150f99a52c7 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 12 Sep 2023 23:44:31 +0000 Subject: [PATCH 0713/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@248e8b603934321c25ff7d3bb779b900144119af?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 95ced8f0d..65e0741fa 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 744940e4ee928b00f8ccc707d6929d424cf38be4 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 13 Sep 2023 00:24:02 +0000 Subject: [PATCH 0714/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c31e4ee3e34f52fc5d2bdcfc5a0b19cfacff9c25?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 65e0741fa..6f3ccd27b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 47b7b12c0ad8d1433691ef1ae6a1c9606ae13a8a Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 17 Sep 2023 11:37:22 +0000 Subject: [PATCH 0715/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9417166976395d01eef6ad8f432da793cd2f24bd?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6f3ccd27b..b44c6b310 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From def82788aedf2dda5b32b70b65bdc6fbc95eec19 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 18 Sep 2023 10:24:17 +0000 Subject: [PATCH 0716/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5765fd18be11e0859e529d9dd97aef9e5f124239?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b44c6b310..bf63d70b5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From e05867d29c87eef7c94e144f82715b6b7fe830bd Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 21 Sep 2023 11:32:33 +0000 Subject: [PATCH 0717/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@00be926e2d850101792f6994ddf9025842c8e21d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bf63d70b5..2d5f7571d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 2379b57ee0da2b7d66fefcd0fb9a3a586e319047 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 21 Sep 2023 14:15:17 +0000 Subject: [PATCH 0718/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c49b744012a5612526cdfa41487c256b3f906bdf?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2d5f7571d..98cb903db 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 3deefd401e6911a00f755cba581158fc00292a75 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 22 Sep 2023 09:05:15 +0000 Subject: [PATCH 0719/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@68b7d3a9749b9ebcbd7acf0664ce82691306d452?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 98cb903db..8076ddab0 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 9136440caa66c0254010a4040024c554b7ce3f69 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 22 Sep 2023 10:38:55 +0000 Subject: [PATCH 0720/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6419188fcf5062a540951acc4117dca92e486280?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8076ddab0..a3a8cc98c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 13e71796bc3215ce94423a2fa3176bf95ab7c803 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 22 Sep 2023 23:16:07 +0000 Subject: [PATCH 0721/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@051aafb828a36b07fdc85e56da61d33f1bcb1fdb?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a3a8cc98c..6025fd118 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 10de2b8c77b2b8bc2034d0e1737c8fc749152ef7 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 27 Sep 2023 20:03:58 +0000 Subject: [PATCH 0722/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e2eaa9a99370b2f8abf1bb5dd7daf3953f811b88?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6025fd118..8c8f2466a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 5fbd7b78be0dd0e191fde89b685835a717ad99f9 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 3 Oct 2023 09:29:45 +0000 Subject: [PATCH 0723/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@709ffed13f9c8bdfd3427cfab25da80c6f41affd?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8c8f2466a..b40f42933 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d62989729a953bef4377272ee253fbf64bb3067c Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 27 Oct 2023 08:32:02 +0000 Subject: [PATCH 0724/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6628fc39d158d67d671a7d2babb3fe57e2b4d1b1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b40f42933..94bef5964 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 3483b118966cd2d3ff360c06759712458f573376 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 17 Nov 2023 09:24:29 +0000 Subject: [PATCH 0725/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@767fea6cc8399ae9c8e61e03407926ed0192f5a5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 94bef5964..595180a7c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 14bb02368faa26bfd71f6fbbdf19bfd09efa2cf6 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 17 Nov 2023 09:24:40 +0000 Subject: [PATCH 0726/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d93e3653e933bf999ae4c7384ad04f62aef61e87?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 595180a7c..5ecacddd1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From a244f1167193e9a381675ae053b62746f6a85539 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 29 Nov 2023 19:02:25 +0000 Subject: [PATCH 0727/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@77365692f51b7eb771924f2e1b64ac81140ec188?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5ecacddd1..d6cdad3b4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 7a55c51c60a8e382bc0ed3d55359fe2c9eab6ef6 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 30 Nov 2023 00:55:20 +0000 Subject: [PATCH 0728/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8e443510bacda0a19a66cc60daf553a2bc7d7346?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d6cdad3b4..5033b7cfa 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 06b8b27dc5a56c5666f027d2d412d1cada4c800b Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 8 Dec 2023 02:56:41 +0000 Subject: [PATCH 0729/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@de592357438b64eeb217bd011114848866d958ef?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5033b7cfa..5a0e6429a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 2ef463a147b75988c3a55a9a2cb02f507eeeb581 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 8 Dec 2023 03:05:23 +0000 Subject: [PATCH 0730/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3ff40d3587f63bb7b0bf8141430b521759cfa49e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5a0e6429a..e753c621b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 666a21a56b2b1531604c32c6033ace1ef9e5557e Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 14 Dec 2023 14:44:00 +0000 Subject: [PATCH 0731/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@546017a426503379e42bcd6e5c0d3c039973d263?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e753c621b..66ce62969 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 68d03ce58a1c3db3a437d1b60b2731f7a44a4197 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 14 Dec 2023 14:45:20 +0000 Subject: [PATCH 0732/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5c8997e0d852e2a82cbb0d704448e49b48ea6e93?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 66ce62969..8def9ec18 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 8f98a27c00f69c17621b0cfd54365761fe96c2f1 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 15 Dec 2023 00:08:40 +0000 Subject: [PATCH 0733/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d6bbf09b4d5d34cff543f809a6824ebba549a3fe?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8def9ec18..29ebc91ce 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f5f11e50fe53c3f5c9be358b5ecc086027a4a639 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 15 Dec 2023 11:21:09 +0000 Subject: [PATCH 0734/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bfbd52bcebce9a71896683a3965b662ccaeae730?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 29ebc91ce..3f70a1171 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 32e9b1a6c04227389cc7e0a23c59f5bfbcea017c Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 15 Dec 2023 11:50:44 +0000 Subject: [PATCH 0735/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f574b0ba9f3069002e9c4f864069810d748d5b0b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3f70a1171..b44283ac6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 6f5014f52bf947a245cfdf141ebe1103083a037e Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 15 Dec 2023 12:02:58 +0000 Subject: [PATCH 0736/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d6e02189f95f4ae141c1a0f474778b422e55221e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b44283ac6..2d063d2f5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 4b04874b074d635e0aeb554a87cf2226fbe6739d Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 15 Dec 2023 12:51:50 +0000 Subject: [PATCH 0737/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@da47075c6e9c9acbbed7abefaf579de4428e8b78?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2d063d2f5..5a035fb11 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ab854799d1f49f83e7bcd7a1207c0ad02528970b Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 15 Dec 2023 13:47:05 +0000 Subject: [PATCH 0738/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@42dec3690cb738005d0524ce7c709311b4d6f80a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5a035fb11..d147cede6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ed8abea1beffe29092fdd6503812217ee5fa5c71 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 15 Dec 2023 15:57:04 +0000 Subject: [PATCH 0739/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6bcf12417eeedcfa19c5e3d8216700fc1b4defd3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d147cede6..72e9b1082 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From bd442806fd11cbc3d5ec8d4d0548558094dab1d3 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 16 Dec 2023 14:47:24 +0000 Subject: [PATCH 0740/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6821ffd813e8f5f357600ed527263562a14f9f8d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 72e9b1082..dc3c88c74 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 7928ce4fe6889586e7f090bb14722507b03a4ee3 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 16 Dec 2023 14:47:41 +0000 Subject: [PATCH 0741/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bf54e8210a599c38dcf5a4841a6438ab7c48416f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dc3c88c74..a3aa2e138 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 55d0a1b629640b5687408b70f69114cd42a5b771 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 21 Dec 2023 11:43:38 +0000 Subject: [PATCH 0742/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@abd55a978d38cc85b58487caef8de45f185bd305?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a3aa2e138..f71844809 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 778b270a49d4c8fb4895c7d4a709484d6e566373 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 21 Dec 2023 15:37:32 +0000 Subject: [PATCH 0743/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@008533dd35f968ed2ca975a3924649db671ff17d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f71844809..b79005491 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 7af9f8f839a6c1f0990ac23dd336c57e5319633c Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 2 Jan 2024 14:12:55 +0000 Subject: [PATCH 0744/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3d217e377a37e472a894c850f027d36352616129?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b79005491..c7f204960 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 79edc437934f150edbbfb6ead495ccb4b890de8e Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 Jan 2024 01:10:16 +0000 Subject: [PATCH 0745/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@48e3d82f1d236488a3d7db3e57eee7e11481b758?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c7f204960..1fa2346ef 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 0269f16c0f035e8d185e66705fddb37db9c73cdb Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 Jan 2024 18:29:53 +0000 Subject: [PATCH 0746/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@de22dd45b26ae64806eadf7e7d29e7651472d4a5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1fa2346ef..3b7fdc288 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f3548a00a1ed60fa72ad260619487ca9f670ad98 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 14 Jan 2024 14:15:29 +0000 Subject: [PATCH 0747/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6214bd3e09941266b390b4c35a0abe8797521bec?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3b7fdc288..1788971f1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 9d634bb9957b46fb7d3c7e2540a2de13e03fc4c7 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 Jan 2024 14:20:42 +0000 Subject: [PATCH 0748/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2f7741d87bb3e71fd5f8588bb03e578d58cfbf83?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1788971f1..adc4455bc 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From e83a86bc627e63a512766dbe32044f90649dc86f Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 Jan 2024 23:44:14 +0000 Subject: [PATCH 0749/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@feadb30cf84f9e9048d39536ef36f3937f62d83d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index adc4455bc..4b7789f06 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 679d624e96cf0c21f1b1af9686c672ca1efb892a Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 18 Jan 2024 10:32:52 +0000 Subject: [PATCH 0750/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5f2855762fe5094dc1148d4e5f9ece9b18ed3389?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4b7789f06..c5a86228b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 407bf6e9b1bdd4885374fbb35300260fc9f3da07 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 18 Jan 2024 17:21:10 +0000 Subject: [PATCH 0751/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@343e9a61525a5bf71e543a55c256fb1150626851?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c5a86228b..5451d2aff 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 012cd9213f7da1c47659f0f28aebbf7b1c78de5e Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 18 Jan 2024 18:05:16 +0000 Subject: [PATCH 0752/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8773e7a2b77d8f4f12ae4d27bdb416a059a9f082?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5451d2aff..684ad5d40 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 8520be7c2c7034ba77fad5fdb04f34271f61eeea Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 18 Jan 2024 18:05:46 +0000 Subject: [PATCH 0753/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8773e7a2b77d8f4f12ae4d27bdb416a059a9f082?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 684ad5d40..aa11117a8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f46657840e15f5708426954235d8adf791d27a6c Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 23 Jan 2024 16:01:54 +0000 Subject: [PATCH 0754/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6f029e68b2f4452dd4518dafff66ef1f46e25add?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index aa11117a8..78394136b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -1615,7 +1615,7 @@

    Starting the peer with convex.worl
    -
    ./convex peer start --public-key=dfbb22 --password=my-password --address=<address> --port=80888 --url=<my-ip>:8088 --peer=convex.world:18888
    +
    ./convex peer start --public-key=dfbb22 --password=my-password --address=<address> --port=8088 --url=<my-ip>:8088 --peer=convex.world:18888
    @@ -2234,7 +2234,7 @@

    Transactions

    From e112541ea7bcd7415ecab72142e28cedce956895 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 24 Jan 2024 14:22:56 +0000 Subject: [PATCH 0755/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c6ad1f447c242121cdfd86ab2549751b9e0bf17e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 78394136b..8f5d03aeb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 55e1f1f50ae49892a71250591d720e037f83d1a3 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 24 Jan 2024 14:23:26 +0000 Subject: [PATCH 0756/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c6ad1f447c242121cdfd86ab2549751b9e0bf17e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8f5d03aeb..542bc48ff 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 768d4d0ceff6eda52129660a044463129e7e5d25 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 25 Jan 2024 12:58:57 +0000 Subject: [PATCH 0757/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0589cc96493146b944f8828b2e842e4c74657d67?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 542bc48ff..328659902 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f9750a0fccf00d8b9e231a6828e7489ee54db2c8 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 25 Jan 2024 13:09:57 +0000 Subject: [PATCH 0758/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1463977597ba9287c697ea6c4ece4fb2915b0882?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 328659902..cd054469c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 82c2834ce07e2b124bcfb1608e7c796c066dc365 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 25 Jan 2024 22:35:42 +0000 Subject: [PATCH 0759/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@947cc92da53733326b9c2e32d60c6097e22804be?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cd054469c..57abbb808 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 2225adf8248b48c5d5344276d7b7251a4908558d Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 29 Jan 2024 13:44:26 +0000 Subject: [PATCH 0760/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6a275a0a2f985a3ac734fd6d53f7569b039a2864?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 57abbb808..f6bca821f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 2a2d4cc9ca134d119592f9e57f773471d72cbb9a Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 29 Jan 2024 13:45:55 +0000 Subject: [PATCH 0761/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6a275a0a2f985a3ac734fd6d53f7569b039a2864?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f6bca821f..be731cd99 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 60e1dd4ba6527d5754f33950f0e5470c0fa5373d Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 29 Jan 2024 13:54:13 +0000 Subject: [PATCH 0762/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e076ca0590a393f6d19c7e0b2d0885dce8660e84?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index be731cd99..07d5ed763 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From a8d6dbbfd0d4ddcc0f281229e5a00a50b1014436 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 29 Jan 2024 17:17:32 +0000 Subject: [PATCH 0763/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a250b2357d8e6da5f5e1eff30349a22c4c876129?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 07d5ed763..e9710e840 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 2a9dcb0f6a590057b79d36774c7238ed7749c8fb Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 29 Jan 2024 17:32:23 +0000 Subject: [PATCH 0764/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@41f60dbeba15430542dc2e08505a8e8662774e57?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e9710e840..662d9bbbb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 95bf37487b01dea957a047b8fa12ecc59474a58d Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 29 Jan 2024 19:12:28 +0000 Subject: [PATCH 0765/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a90ee093993f957de3b57a79126c871c8b1113c0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 662d9bbbb..40db8053d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d21dde671ecfa99b98392613f69c742a4a49bcdb Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 30 Jan 2024 12:28:34 +0000 Subject: [PATCH 0766/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@375ede29d5aba31105ab8b7cc046caeeaacc9205?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 40db8053d..edf531054 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From fca9997677e11d43f439c48f1dc21f23c9e08487 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 30 Jan 2024 12:51:12 +0000 Subject: [PATCH 0767/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@122012474ca353e45d9665b7116831e6306fa45b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index edf531054..5df56c792 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 919c295a22138939b8868097a9e32801fdb561e5 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 30 Jan 2024 13:16:56 +0000 Subject: [PATCH 0768/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@608195514068d8a1e55fd38cd91bd51e57e16af3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5df56c792..3d4232eb7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d820d39df5fef0497916621f95b5fda0bf729fd6 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 2 Feb 2024 19:10:54 +0000 Subject: [PATCH 0769/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f85ff8b29cef4a172564369629f2b7cc8fc4348b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3d4232eb7..a747bf6ba 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From fd143b96ad94c35f5ca45de5c638b3870694d8eb Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 3 Feb 2024 13:01:39 +0000 Subject: [PATCH 0770/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@65f8a24f5ec21c9863f12e511f5c061a53e3499a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a747bf6ba..ab78e2373 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 35170a89166cdc025d4f99d654b93fccea1445cc Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 3 Feb 2024 16:09:31 +0000 Subject: [PATCH 0771/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@95c8ac5bf4ecdffcf545088b6d1c73bc0753c295?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ab78e2373..fa537ad64 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 2bba30ee1588688342cd665c89533f5fca0bed3f Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 5 Feb 2024 11:16:58 +0000 Subject: [PATCH 0772/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@185ed14ddddc0042b68a17ff737d232b63dd43fb?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fa537ad64..00da5d92b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 75cc168ab0ae87f47f9d8b5649ee2edbfc2de0dd Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 5 Feb 2024 12:34:16 +0000 Subject: [PATCH 0773/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@20374406ae133bdf152829c87aa0ce9f18a4fb88?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 00da5d92b..1b1e03709 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 7bf090ea4cbfda740449691fde1729c47e06b980 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 5 Feb 2024 17:18:22 +0000 Subject: [PATCH 0774/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@08d76849033e338435eb89958150696015fc4739?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1b1e03709..3820e4644 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 6a2db4a54160ce24630355b41abb4dee3ce34b19 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 6 Feb 2024 13:50:50 +0000 Subject: [PATCH 0775/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e6b311e2d77052f323173beffbb33e7323847bd2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3820e4644..887dc9cb1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From c5d20da58af9317aff6df78606d75c6efa64a16d Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 7 Feb 2024 00:25:19 +0000 Subject: [PATCH 0776/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@29ac2362f419f9bb91b17c44ddfe19b88dafd2cc?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 887dc9cb1..81232986c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 8bbae6f1a05ed615b13bcf30e57472fa04764ba5 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 7 Feb 2024 21:37:03 +0000 Subject: [PATCH 0777/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ef08585a9887772e8ae1450ce648174515a33f0f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 81232986c..42f462031 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 353ffb8eef6a4d8598036dc8fde38a13c045e78b Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 8 Feb 2024 15:40:08 +0000 Subject: [PATCH 0778/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0834a5d1609d4e9f337abc009e353fb37b87ba1a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 42f462031..4e2f5a66d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 326a26d65e0d0d3aad67e49da1f4185a965fd836 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 8 Feb 2024 18:04:19 +0000 Subject: [PATCH 0779/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@26f11ec458f5d8b9dd82c2cfafd8b1893491a3d6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4e2f5a66d..bbef9f851 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From bfd75e7849bcc33d30c5d6dd1f8d6c46d46b0673 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 15 Feb 2024 15:06:28 +0000 Subject: [PATCH 0780/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b5cbddb9df82adbd76dedf76ee2c370426bbc6ef?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bbef9f851..a1411518c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From c133cec62ef81820ac2d809e8323b2f22e28a460 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 16 Feb 2024 00:17:26 +0000 Subject: [PATCH 0781/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@39d0789f20033004c61eaff824f4edb05eb29ca6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a1411518c..f00b6d33b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 8c5d2023ba4eb3c5b885b8d62921538e7b628e8f Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 22 Feb 2024 13:27:27 +0000 Subject: [PATCH 0782/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@71c7462a7e351e49dcc073b65d9f42a9bfb46a35?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f00b6d33b..a848a3d83 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 3ba1c654bd6cf72fc9cf223e60a36746cd9b3185 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 22 Feb 2024 18:08:50 +0000 Subject: [PATCH 0783/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8961d42ac0191ad230c26098766cc69c6780b8b9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a848a3d83..4750e0d18 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 905f53c01813ede75df407eb1d7b7a30048706f1 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 22 Feb 2024 18:34:03 +0000 Subject: [PATCH 0784/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d722c5e38d938ac8a2a1e0d1b5a68e46d76a5759?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4750e0d18..14074dd77 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 0908d8b9def035645e5964c65b1633ec4cf84e64 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 22 Feb 2024 19:18:48 +0000 Subject: [PATCH 0785/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ddf2c92549c0c8c4a51fdcb660fb8b37965aca5c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 14074dd77..43af1038b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 189545d54d83aac070c3064114dca95c71c8e9b2 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 22 Feb 2024 19:38:32 +0000 Subject: [PATCH 0786/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b02da847e5bd3d8d4de72cb5ea2b3dd683788c16?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 43af1038b..f904a5dca 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From c493443e5a27e04d998c1172ed732404af1c6414 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 22 Feb 2024 19:54:53 +0000 Subject: [PATCH 0787/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e6cec96e300e72cd1d22f17d46fc31ae6fb9f8f3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f904a5dca..c3ebcc2b3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 4352727b8f12294b5011ff5ff4fad6d28510fde7 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 23 Feb 2024 13:14:59 +0000 Subject: [PATCH 0788/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@dc096814695a21389ca0bc6612225918500bbb2f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c3ebcc2b3..f060a40e0 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From c53fa0570e16fc32a83e549053f50c1f94e698cf Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 23 Feb 2024 18:12:23 +0000 Subject: [PATCH 0789/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ac19d713d42930554b176ee70e34389108296b01?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f060a40e0..67ba25c8b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 1364d5beda974c55f5ccb00cb6cb0323b953c71e Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 27 Feb 2024 10:22:51 +0000 Subject: [PATCH 0790/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6561df64a7274750332da3bb2c96968b0f4aba26?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 67ba25c8b..28c643527 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From fd6985b60fadb75eb02845b2bf893fe59108803e Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 27 Feb 2024 16:11:20 +0000 Subject: [PATCH 0791/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@58a4ffce4a57dcb47e64702f6f2b178be6d96f73?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 28c643527..4f153c56e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 8ff48e3014339c45647284ad4c9dc2eccbcec7f5 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 29 Feb 2024 14:26:00 +0000 Subject: [PATCH 0792/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3c543f833370336795c678a6d007bc5cb008c031?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4f153c56e..c17418baf 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ac9adc95ec3859d8c656858caef0111a55052cca Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Mar 2024 10:48:44 +0000 Subject: [PATCH 0793/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e88260c3c78dc0799e5c240a7f56f670cb813fd5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c17418baf..7ed19ad5f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 64c9654107da6d066bba53b431f4350a937df208 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 5 Mar 2024 12:41:43 +0000 Subject: [PATCH 0794/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@22909f4927e2672cf133862aaabf048bc935556d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7ed19ad5f..44265d2e7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From e1698749d10900b4af82fb3ac91ad19e98aaea39 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 6 Mar 2024 15:46:48 +0000 Subject: [PATCH 0795/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6f87877c5b351410fa6ffeedb96ff267a535c0ee?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 44265d2e7..fbd2bfbf3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 61e19c65a47f9c155037219727ad4e6762e248c4 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 6 Mar 2024 17:42:57 +0000 Subject: [PATCH 0796/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b75674e928fc8324ebcf439abdf4ad469b533dab?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fbd2bfbf3..a462288fc 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 905da2b3e7bd1d33065bd97fb8cf2b96b40372e5 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 6 Mar 2024 21:34:01 +0000 Subject: [PATCH 0797/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6c3eae4e1c4e3a0b878bd527dbd7513bc130f9e1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a462288fc..b9b83e2fe 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 07ffbcc00240271bc27a969e6c6461872cc020fd Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 7 Mar 2024 12:31:39 +0000 Subject: [PATCH 0798/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1bf82d5fd7a527f13aeee53114f9f72f0241401b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b9b83e2fe..69546625b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From c14c6a9a77c76487c37040c67c92e28dd22dd252 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 11 Mar 2024 10:23:46 +0000 Subject: [PATCH 0799/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9db460af8a4b5e06267d085a2db6183e81cb0b56?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 69546625b..4c3323242 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 686df002f4a18d77fa40121d45cd17e199dba7a1 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 11 Mar 2024 11:07:05 +0000 Subject: [PATCH 0800/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ce926dbfc42f2a8d4a32c3ceddd189c55ef0403f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4c3323242..4ccac12a4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 782f95fca9ed8982b28c3587d4903fdd5b8fad50 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 11 Mar 2024 12:52:36 +0000 Subject: [PATCH 0801/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@069de946689f338c0d6aca0df2c9e07908fac004?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4ccac12a4..4387f9e29 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 98f2c728d76ab20c89ac85df3d12586f056829c1 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 11 Mar 2024 13:53:11 +0000 Subject: [PATCH 0802/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@024786764166968a7dbc29cd0609aa12a57cfa18?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4387f9e29..013cbec84 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 6c0d8db670b858b1ed5486aea3510f404dedbc93 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 13 Mar 2024 17:55:18 +0000 Subject: [PATCH 0803/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6d418a71fb2f612eb54c37bdad19d6b50803fc40?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 013cbec84..6ee259329 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 5fbe098f241a00c9f3e2a4625fb452cdc5d0f1f7 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 13 Mar 2024 18:01:01 +0000 Subject: [PATCH 0804/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b8fa759d2c430f0ea2d764be8d87db647d4b4056?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6ee259329..84816d7d5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From c3ee7e89cdb199bb29c876bd0a809aaafb70c664 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 13 Mar 2024 18:03:32 +0000 Subject: [PATCH 0805/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5f101edba3bc3395ff4503aac288577b5aad62bb?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 84816d7d5..e77c03b01 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From b6e9c68e0a88c30619f6148f54a0e8018511f2b0 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 13 Mar 2024 18:08:54 +0000 Subject: [PATCH 0806/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ae269d1d8cad93040b773f29e3b4def6003a8fc2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e77c03b01..827368a64 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From e18020ce0f39d6098bbbd5dcbc6ca38b6172717d Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 14 Mar 2024 12:46:35 +0000 Subject: [PATCH 0807/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2e027970df1d07dcf6026cd6773d53a43c1aabfb?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 827368a64..63a2c46d6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 98824bea4773c573e1b9921ef7657e42f099148d Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 14 Mar 2024 14:32:28 +0000 Subject: [PATCH 0808/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@59a28067eae0f99ee457a84f21a839808b9ee5d3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 63a2c46d6..2f6552dd9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 12378a5adab1e6b02ea5ffd17550e1063d2601d5 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 14 Mar 2024 15:07:34 +0000 Subject: [PATCH 0809/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@14027f6bb4277258d4fbb7898071e34130b4b468?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2f6552dd9..ab0f6c817 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 77f446055c60933f9a5662e02e2be6cbfe281a70 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 16 Mar 2024 22:39:10 +0000 Subject: [PATCH 0810/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8e0140ede1997d4fc8acd5234da5f35b46f39c86?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ab0f6c817..cfee6da96 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d025f5ee8852ca0a8478d14b48c4f6532da48baf Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 17 Mar 2024 10:12:59 +0000 Subject: [PATCH 0811/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@67a17e87bb6bbe7ed331a25d0c1e5da3b74e2553?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cfee6da96..9f05b9de0 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From cb2ec8692bb2800a1447e2e72cff3f67d49a137a Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 17 Mar 2024 11:23:20 +0000 Subject: [PATCH 0812/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@101cc60f23df6b72589c8cacc12b94cdbff2308e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9f05b9de0..5c67fa1e6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 561203bd03f8e7f08a51b909153453249193cd03 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 17 Mar 2024 11:33:15 +0000 Subject: [PATCH 0813/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@114aa896c17f96bb03715140e63926fd0e03d05d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5c67fa1e6..e28c5b827 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d66037d83d4f8385859ed997b47c93f4236ce869 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 18 Mar 2024 11:17:43 +0000 Subject: [PATCH 0814/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@25e86772c29cae040485478fc738991d5f8fbcc6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e28c5b827..dd0d0efcf 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From a39574129840cb07bea907bf0d695ae1fffe4b0a Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 18 Mar 2024 12:56:06 +0000 Subject: [PATCH 0815/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@401fed05b18385cac965bcb40f80323f43547577?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dd0d0efcf..9c12f2a90 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 9b94b1af1876641f98d44725f8dc9c435b85da92 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 18 Mar 2024 13:09:28 +0000 Subject: [PATCH 0816/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@94adc8f785abe9e6a4b6f9481ab2dd223752a419?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9c12f2a90..855d681a7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 14a0eaa03517e0182273724b7c33646943847784 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 18 Mar 2024 21:39:49 +0000 Subject: [PATCH 0817/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2a948e508c3bdb0e26e43b25ed98b2dcd7a95507?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 855d681a7..21884f157 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 0311b3c4181f8faed6164e2ddf094ecbbce26be3 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 18 Mar 2024 22:07:06 +0000 Subject: [PATCH 0818/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@085012e44add0aedf5ae263f3e6f31a0ec2e2292?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 21884f157..de5f67384 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From fd07f3237dcf65fffcf9dc4b064c6fd3a586fa9c Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 19 Mar 2024 12:44:19 +0000 Subject: [PATCH 0819/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f1748a20fee2906ed2fdd2bf00fe4d7a69802533?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index de5f67384..d529c2b81 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From dd8265a79a15a0004388924b314515b6a2b94354 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 19 Mar 2024 12:57:53 +0000 Subject: [PATCH 0820/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ff92a1cbfa9e3607be7470779c43b54f2107f871?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d529c2b81..90346a5b2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From b697fc3b63e1483c5c19812bb9069e7a57cd2cfb Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 19 Mar 2024 16:59:00 +0000 Subject: [PATCH 0821/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@af68b8040b913f7d536979303ad400498d5f7151?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 90346a5b2..18a5ba022 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From b468db9f170f2e451f32cc508051aa678e2bc5a7 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 19 Mar 2024 18:17:55 +0000 Subject: [PATCH 0822/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@dab6d67a6405cc96b92fd6637e224a7e402bd3ed?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 18a5ba022..e3e56cc58 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From e02ce49d6869f7ea3eb7444c623e122ef64613f8 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 20 Mar 2024 11:29:54 +0000 Subject: [PATCH 0823/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c1535e24362583a9d5bd7851823e2101dc29e960?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e3e56cc58..601f03851 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From b948de5fae918be4a80e6eff1bb10d9275d4903b Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 20 Mar 2024 12:25:22 +0000 Subject: [PATCH 0824/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d9a8c330746277306a25dd84f2e9439574ffbf81?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 601f03851..bf11eccc3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From bda9f55d443cdf01a40a608c3d2e02199a9120d1 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 20 Mar 2024 17:42:23 +0000 Subject: [PATCH 0825/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@280facb3aff794ded9c0e11f8d050ad98ac80d6d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bf11eccc3..db8e633c5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 81373bfa4b1c87136e7fa101c3052557b444e432 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 20 Mar 2024 22:24:56 +0000 Subject: [PATCH 0826/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@65eedc65587d6bc0a337ddce2484b3759869f685?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index db8e633c5..82f57f2fd 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 88b01b2b924a4354a311df16e3d6f3ac9bf47549 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 21 Mar 2024 10:13:19 +0000 Subject: [PATCH 0827/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3be970591cf629515d57280ee929c059f74f746e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 82f57f2fd..e7d24cdda 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 2de54e98ab9350301979c88b96f7e81e6d895033 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 21 Mar 2024 10:21:25 +0000 Subject: [PATCH 0828/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@dc539d7e142ffd20e40529d4261fdf3e4e731fb0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e7d24cdda..a6b588a25 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 964acf3f5451dbd9bdde0c6f8fb3ae8dda00fa17 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 21 Mar 2024 10:23:44 +0000 Subject: [PATCH 0829/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a602047859eb54e0ab728440c8acabfa5d890d74?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a6b588a25..1e92b1b25 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 057f003f133271cc1acba2e9a19feeb08be90aee Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 21 Mar 2024 10:29:51 +0000 Subject: [PATCH 0830/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@16d7d14657c5aab18937efc5f381587343b2bd52?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1e92b1b25..becc7ada2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 1735a57b89e9e7da1f4c610780b9e8ae47285421 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 21 Mar 2024 10:32:16 +0000 Subject: [PATCH 0831/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b453ff2fd92b332290b265b0c0f1cc3871811973?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index becc7ada2..a570049ff 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f5ef9d5afd2dd0277a37258502a52c39ae052b12 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 21 Mar 2024 10:35:44 +0000 Subject: [PATCH 0832/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@45a5abbf59e28f8c16f758b7dc804e2dfcc108d7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a570049ff..9aff281ad 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 4a36c6d60143d10ad1cb78fbe4497316f83ab794 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 21 Mar 2024 10:37:10 +0000 Subject: [PATCH 0833/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@974b60439dd2009ef402197aca58031196715368?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9aff281ad..01699d835 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 0e6fbd66d44baa648f877f131c9bd9f53cceccdc Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 21 Mar 2024 12:25:41 +0000 Subject: [PATCH 0834/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4a9098dbd7af1dce3d2dade8377340f80fc30b92?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 01699d835..4dc2f0c2f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d3f1f0214033f53d740d6b873532ee20e7a001cd Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 21 Mar 2024 12:26:01 +0000 Subject: [PATCH 0835/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9965d92dc77c97f80d3f71718b0504209b268e50?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4dc2f0c2f..c2f344bec 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 7289e16f028f1408fb0fe9d861e5a2d589309dc4 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 21 Mar 2024 15:45:37 +0000 Subject: [PATCH 0836/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f70ab1362938d1e9c90c6a9875c3b322873b47ad?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c2f344bec..8c6bc0ac5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From cf38d32f7b2a34564bb0e07fbcd7099e8638999b Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 21 Mar 2024 15:54:33 +0000 Subject: [PATCH 0837/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@155b55eed9ca8a6f13d7f3ae283741aa95040612?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8c6bc0ac5..17a763802 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From c5fc301cd6af5062ce5a69f2a40dc51b1bb8971f Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 25 Mar 2024 12:05:10 +0000 Subject: [PATCH 0838/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cc8cd2ae76ba7e6326fa9be9236695fb46f44992?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 17a763802..5384014b2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From e49716d2843243e49e6b647ba4b61051b08a6d36 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 25 Mar 2024 13:04:30 +0000 Subject: [PATCH 0839/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@72d874387bcce19d7eb30e61d4802d7838f3fea1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5384014b2..f1003fe16 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From e8d9f4452333e46f89628eac0c3774b733f28119 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 25 Mar 2024 21:00:10 +0000 Subject: [PATCH 0840/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@302cecb196caa23cd11402935b43b603171388cb?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f1003fe16..aa68d8eb1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 1fb0a1418f69750031891354281090a6c1b7ecf4 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 25 Mar 2024 21:36:19 +0000 Subject: [PATCH 0841/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@dc1ceec5bb5f723b6e1744526ae6bd403126b8ab?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index aa68d8eb1..6e075087e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 6ef45d01e24786d68c435bb8dfe757e52ee8adb1 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 25 Mar 2024 23:42:12 +0000 Subject: [PATCH 0842/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@100c7c05ae5cea3d4f5ee2c63098b9432bced7bb?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6e075087e..fbe456a58 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 19b75f33c1f9fcd2d4f6798e0f29543e4993575f Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 27 Mar 2024 13:34:43 +0000 Subject: [PATCH 0843/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f86f8c1758f16ef177eca8ab538288b25c2a3b6f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fbe456a58..c18e40f8b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 8855f7bdc0eeaf6b1a83cd3fd73f101f5e410e9a Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 27 Mar 2024 18:42:05 +0000 Subject: [PATCH 0844/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@454130a5d7aa1eb69408c461b1566ee9197e372d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c18e40f8b..9a4e446f5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From cf5deaf5c6d189dfe85bf39db5b8b2985f19c38e Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 28 Mar 2024 00:46:55 +0000 Subject: [PATCH 0845/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@738bcb9ac3fb681b800ba2a8689d58212cd3c8bc?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9a4e446f5..8a7fd0a34 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f4992f422adade5dd244a640e5af63801801daf6 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 29 Mar 2024 01:10:23 +0000 Subject: [PATCH 0846/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@698ad9a74360e9df9acfd9b2a0505fe3fa69b461?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8a7fd0a34..7907d78b7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 62f4a40b0962629ab1d7dabae7e47fa5c2bc51b5 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 29 Mar 2024 11:12:02 +0000 Subject: [PATCH 0847/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@37efca577c06faa78cd4a53eb14be9497614939e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7907d78b7..08897db5f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d7acd176a073962aeba5a6ff949e60bc1b311b0e Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 29 Mar 2024 12:57:12 +0000 Subject: [PATCH 0848/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0d397e1b1d562af7476e1dfa52bae3aa4feb4d08?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 08897db5f..c166ff354 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 1c3ed3d613d89aa439e8c7cb554e5f6076c2b7fa Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 29 Mar 2024 13:21:07 +0000 Subject: [PATCH 0849/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2bf61e03e0a6442f03ce8e118100f6f1d539726e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c166ff354..af40f9120 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 3a9eb482f6b1bde40b8364d44ff207dab7d48344 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 30 Mar 2024 12:59:47 +0000 Subject: [PATCH 0850/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@28bdb87eeef7d06a6d1a49dfb142f49b532114b7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index af40f9120..dd776615b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From cb698f8fd940c15a3db611eb15015cf3b1d8793d Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 30 Mar 2024 15:08:18 +0000 Subject: [PATCH 0851/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e53b813264e39b9cad24c50613605055ab015352?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dd776615b..47a9f257b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f1048d9ae1907920ee05dcd53e4b763762527d59 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 30 Mar 2024 15:58:38 +0000 Subject: [PATCH 0852/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c0b2518bde740914c2fe0985341957f723750605?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 47a9f257b..181b64205 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d1d5ca71dde9116f245639fd5451172932961a14 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 30 Mar 2024 19:05:33 +0000 Subject: [PATCH 0853/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@19ee5c2ff7c5a0e357ab3d49caec32b9df3a8318?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 181b64205..838c84cde 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d55c9e298e55d77a1b384fb0f59170bc791cd39b Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 1 Apr 2024 12:55:00 +0000 Subject: [PATCH 0854/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e51c88eeb72bc312829284e7f6c622b492416871?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 838c84cde..be2794b14 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From cb9799179b0e0ecd31241fc98dd76e555bfc4cee Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 2 Apr 2024 09:14:20 +0000 Subject: [PATCH 0855/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@764a858422529d9ed555634df8def76717872300?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index be2794b14..6bc089018 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From c62de3a194c71fa05260ef11c8175636e319ef73 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 2 Apr 2024 10:09:19 +0000 Subject: [PATCH 0856/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@67f9a37147c4d3d632961b939afae7a59c5b0916?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6bc089018..c8346c47d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 5c6bdf88028fca09cb66e639d8a228330d97db5b Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 2 Apr 2024 10:36:28 +0000 Subject: [PATCH 0857/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b09241cb88034c2a2a19add993c0722b7fba08ab?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c8346c47d..d5a906de6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 641c2349fde8f76e93e596a06411bb80057fc388 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 2 Apr 2024 10:50:30 +0000 Subject: [PATCH 0858/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f0c9a28e27effb2dd451b88dd3c89e8824c6bc4c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d5a906de6..f07bb2897 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 8e51b5700a58b77fff1d51de8e12766f734aa8cc Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 2 Apr 2024 12:08:33 +0000 Subject: [PATCH 0859/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1cc1bf2911bd77d9974fc777b81fcee5b48f4f4d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f07bb2897..ee040cebc 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 57bc5b913f7bb3dd3054f955856e7b0deb29f597 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 3 Apr 2024 10:38:56 +0000 Subject: [PATCH 0860/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e0a05f4f4257c8ff76a0ddf9545ef0f37160493b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ee040cebc..3128f3142 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ae4b34734ba9f6e72eb19b469d24522fa4d8ca3e Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 3 Apr 2024 14:24:03 +0000 Subject: [PATCH 0861/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@55a8cab8f7b8700af8d5ecfcc253203d909c9de7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3128f3142..8a5306b4b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d95ab2fc0168cbe679098b893b3ecda93181a0aa Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 8 Apr 2024 08:00:36 +0000 Subject: [PATCH 0862/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e84c8139534dfa7f2a261e12ac55c96f9f1641f2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8a5306b4b..238dc993d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d1e23613c4a6deff9db0fa65b2a5db69ea8b11f9 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 8 Apr 2024 10:36:57 +0000 Subject: [PATCH 0863/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b2f5dea1f017df6003c3106bbfe75aeb39b4eb73?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 238dc993d..d27751b2c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From cd489cf20647fdb6935c1f060a6d58923279e046 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 8 Apr 2024 14:04:51 +0000 Subject: [PATCH 0864/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@612582b9e1867f5a9b1e51a1ab2ea640789ff01c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d27751b2c..e404b73e6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 2c6aa7047e8465c477a2077931a1c32c510368b1 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 8 Apr 2024 18:10:56 +0000 Subject: [PATCH 0865/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bac6c0ddbc82d0916ea50975da5e3c341c3a9698?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e404b73e6..070622298 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d692c1a294f97ab163796631f27ba5d135f6d32d Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 9 Apr 2024 08:20:01 +0000 Subject: [PATCH 0866/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2b34abbf86d5ca52b134124bd7efecc0028523e1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 070622298..89e8a1405 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 8604199b3c709ac25bc7e46a26df90fb2fb974bc Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 9 Apr 2024 08:40:27 +0000 Subject: [PATCH 0867/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5cfc3d0eb145cc8843aa2e7af38085627eb89144?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 89e8a1405..7661053e4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From cafdf10d6c12cf35260d22e3749a7c1c6bcbbef7 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 9 Apr 2024 09:41:47 +0000 Subject: [PATCH 0868/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b9321a428cf48831c908ac062bd2f3546f611c54?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7661053e4..e7f41a807 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 85ab5ccae5df65281ca1ca1113e4a7b1da2dff40 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 9 Apr 2024 10:55:49 +0000 Subject: [PATCH 0869/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@80600bc94d598d9a3d479c4bae0e16fb20c6e4c3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e7f41a807..096f6d55a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d9620af3d5be03462614ee225c5800c4f388dacc Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 9 Apr 2024 12:39:52 +0000 Subject: [PATCH 0870/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cc237d96e7c53384a6bb1581dae4c58d5a47f9d3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 096f6d55a..f2b7cceb1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 72d9ee50013894122af7c378c14346b2c2d49687 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 9 Apr 2024 13:35:28 +0000 Subject: [PATCH 0871/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8534dba5a1a8ffe472d9b5644fd9ce7992be48de?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f2b7cceb1..6eba6f844 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 0cc230cb7b2aa2ea549d56efa3ed553a286bd683 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 9 Apr 2024 23:33:17 +0000 Subject: [PATCH 0872/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@15a3194722e608920534bc33a9fb40fa1c059871?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6eba6f844..86d408177 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d83ffece6b3c4f3e00b04f5a441e56e730173da6 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 Apr 2024 22:19:58 +0000 Subject: [PATCH 0873/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@105c47a788a1bd9f730a71e2870ff17a73d9680d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 86d408177..212ff6b32 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 17293dcf377566064920b6a80b69089bac198525 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 Apr 2024 22:33:46 +0000 Subject: [PATCH 0874/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@013581f43aaa23c12d6bd01d3541c11a90b48e8c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 212ff6b32..a73d5d617 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f5d5efc4845d9871fd67b42e4e43390ad560e669 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 10 Apr 2024 23:43:13 +0000 Subject: [PATCH 0875/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0313e5026c0795a032f69f8b7f08e0bbbe1d9bc9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a73d5d617..08763143e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 4078681ea01021126bb132f2f9bf14402718a5d8 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 11 Apr 2024 10:27:35 +0000 Subject: [PATCH 0876/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a34d0a9335643972fa442865a47ae07b1b99062c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 08763143e..ee8d6a9e3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 241a73d1771a51c4511f9594f2b8475d98b27c05 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 11 Apr 2024 11:06:52 +0000 Subject: [PATCH 0877/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a39bdb72ec6fb900ba96f6eb9a94435e6663b3a6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ee8d6a9e3..319aa5ee7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 665abd76bfd9ee436e451cb87d106c08b238c2a2 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 11 Apr 2024 12:53:08 +0000 Subject: [PATCH 0878/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7550d5a68694bea627339b180c5c8a53b4991bae?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 319aa5ee7..fa26a2939 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 1d76259cee2cd205f4ef4fb5cf3a7d6565f47f87 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 11 Apr 2024 13:06:12 +0000 Subject: [PATCH 0879/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c732ad46ab52ea47617236c3202c2215d60dc3b3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fa26a2939..cbb087a26 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 06b17f1ad1eb79e6812bd1ceb0886f70e012bb4a Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 11 Apr 2024 20:15:20 +0000 Subject: [PATCH 0880/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@eac552120079bae3affa0da149ed36b763cbb225?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cbb087a26..a503ddd5e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From a25e332b9d15bcd1816f9c711c5c4664d37471b2 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 12 Apr 2024 11:07:42 +0000 Subject: [PATCH 0881/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b1a1ac39733028044c39199a8fcd3b2d11928dca?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a503ddd5e..55c551cc5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ef9db322ed25cc6acbee30cea1d5fc37792d10af Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 12 Apr 2024 13:42:05 +0000 Subject: [PATCH 0882/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@819cca9194d0b424eabcc6c2cca591bbee4169d0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 55c551cc5..6c59114fb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 442194f27a4b442ad9890eda379f9953d374ee96 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 12 Apr 2024 14:30:03 +0000 Subject: [PATCH 0883/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@eaafae06135469394fcb31e1b00f89f7f454402c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6c59114fb..ef5ca046a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 284fc5009ddaa35c4521131f49fd393302432985 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 12 Apr 2024 18:21:18 +0000 Subject: [PATCH 0884/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@84c548fe4b8316495005fa6fc0b4c20bd751695f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ef5ca046a..ab3d0c893 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From edc6ef3946d4043b2f0b1231e406c2ed3f0e5eda Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 13 Apr 2024 10:39:35 +0000 Subject: [PATCH 0885/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@fc5238d1ee3e667d4a0744ae23c3211ced43963d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ab3d0c893..def517d9c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 5c98fa5140d296780a826bee6b19c357c9078fa8 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 13 Apr 2024 12:49:21 +0000 Subject: [PATCH 0886/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e41947fc449854fa848df2dab6c62a31bd9d4259?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index def517d9c..93959c95b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 9af079fc04f691f4a7270376e77f0dcce2aa1041 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 14 Apr 2024 12:34:38 +0000 Subject: [PATCH 0887/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ab1393b96f2457d0e3a0003018b6b1f5b053c85c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 93959c95b..dcfbb4ade 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 69edde890f1a7117c2f2805aa5632c811a22a223 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 14 Apr 2024 12:55:00 +0000 Subject: [PATCH 0888/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4d60c5be0895c255ded54454f4109e891f284358?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dcfbb4ade..7419d3f5f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 83c90bfdea93edb1353a7c67f44b3af03b31f111 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 14 Apr 2024 17:57:06 +0000 Subject: [PATCH 0889/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6e80eab0947c8594940030bb6f069f191339e647?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7419d3f5f..fa77a4ad8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 8302bf2a96b50de32710d98c7a42266a98016064 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 Apr 2024 09:31:19 +0000 Subject: [PATCH 0890/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d3f077ece1fcfcc57615f13a071ee22d0571b15d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fa77a4ad8..dbca1e759 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 35d620e2ebd832bc8ed6c8e71a3f48ead889ae81 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 Apr 2024 16:26:54 +0000 Subject: [PATCH 0891/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2a67759e61e6e477332af17d240e1977fc9a18a3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dbca1e759..b25b032f9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 52913cfc1cf34cad5ca4128d4ca6925b88e0facf Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 15 Apr 2024 20:26:42 +0000 Subject: [PATCH 0892/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@27523ad5408bb00e1bdf08ccf30eb573219d2317?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b25b032f9..8e54723c6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From cd941769eaf3b91176ddf7147e7e93b6382d72bc Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 16 Apr 2024 09:31:14 +0000 Subject: [PATCH 0893/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f17748cef682dfc7991cf2391f626dee2a8393be?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8e54723c6..c49e5ca00 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f997cf82972cdbdc5d83038fcdc0ab294ce9e065 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 16 Apr 2024 10:07:40 +0000 Subject: [PATCH 0894/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@9197fd3ff483efef92d7fc4e0d2b96688671a11c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c49e5ca00..86e8844ea 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 3acbdb86a254d43925e155a3568cfc0afb5b0b60 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 16 Apr 2024 10:43:13 +0000 Subject: [PATCH 0895/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d651af1e2bac49d105ed88fdf29e1d3df2b3ba5f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 86e8844ea..b9536d600 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From a83d5d2acd9829911083c05d7122bac00a49c116 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 16 Apr 2024 18:34:00 +0000 Subject: [PATCH 0896/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1bb0299704715c3678b1d0dea442e60fd8be78a8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b9536d600..23bceef5d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ff882140d1d309e796e75af466e30a6cc093c45b Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 17 Apr 2024 09:35:18 +0000 Subject: [PATCH 0897/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f5c484eb64e213340654c66d0e16a2f8c2772071?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 23bceef5d..67311100e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From bcf98bc06da60b63942a3821bfef445785c61117 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 17 Apr 2024 15:42:55 +0000 Subject: [PATCH 0898/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3f9c857996682ac8c1037a8bf20cefc4bc560d35?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 67311100e..67749112d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From e942c59838100590a18f50c7c7dab16308d4f76a Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 18 Apr 2024 12:13:27 +0000 Subject: [PATCH 0899/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@92982d7c5c753b3e7b6198755b6f29f4397c824e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 67749112d..4e8689ac6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From c8d391bc26ba1fc692e9b3fe5879442ce0c0fa27 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 18 Apr 2024 14:51:11 +0000 Subject: [PATCH 0900/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ca04cf9c69fd0863375a37364a5bba9e7cb49858?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4e8689ac6..d8f2aafd8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From c0df852b547069439eecb3770a7f2a9ae82f1240 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 18 Apr 2024 16:43:52 +0000 Subject: [PATCH 0901/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@58d119aca8c74525636e21fa24807cf1d5785a8f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d8f2aafd8..31ebdf7e5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d6de2fff26b4827c5cbb273f22e184e4f977bec6 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 19 Apr 2024 09:50:33 +0000 Subject: [PATCH 0902/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6fe5e972aa23c117e86066d60572fd0c74c0ac4e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 31ebdf7e5..d87115407 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From e93b32ec15c946bfdf91e17b987b0636a17b5aa4 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 19 Apr 2024 10:38:09 +0000 Subject: [PATCH 0903/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@36ab76f018fee87a27f13a81ced82f9ebe3d7ec8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d87115407..088a6dc74 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From b2fdea2789391929b41dc81d1fb89211edf430f5 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 19 Apr 2024 13:28:40 +0000 Subject: [PATCH 0904/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4414d90f592e1d03d650efe039e049e63176024f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 088a6dc74..90b609eab 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f2f777edd2ea19357936ff16ee7505bbc3ffe14f Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 19 Apr 2024 16:00:48 +0000 Subject: [PATCH 0905/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8187700ac9bb8c44870cef9064ed374f3eab39c8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 90b609eab..a2e41750d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From bb89dd5b6f65cad741dc7990417226df8589dd8b Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 20 Apr 2024 13:16:01 +0000 Subject: [PATCH 0906/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@121e3338fb27d2486263220dc9af34872a32df7c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a2e41750d..4b0e53c72 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 863b99313db8e455111b840a528e0a986f8d0b77 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 20 Apr 2024 17:31:00 +0000 Subject: [PATCH 0907/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d5c2694ee515b3db13d5b98d1258baea9ca4660c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4b0e53c72..fcd6abe22 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 20137d78a3b67205335ce9714bb972a4de4a033a Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 21 Apr 2024 17:17:15 +0000 Subject: [PATCH 0908/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a9035c7323081c0ce6adf43b9ef3b0b539aad64d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fcd6abe22..677de0059 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 26fda4c11f66d5bdd5372231784abb042fdcb7bd Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 22 Apr 2024 10:35:25 +0000 Subject: [PATCH 0909/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1ff2d0315ddc187277b229b14078f46833775ee4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 677de0059..9ebdf1f6b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From a17ec628f7f4a70bde857643c41343e8c08fd5bd Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 22 Apr 2024 15:14:25 +0000 Subject: [PATCH 0910/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d5d7c8baa8a43a2b63422f4ffc584e4b3938ffa6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9ebdf1f6b..3c48561c8 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 363ef34e2ac2b1f657ce7afc7d93af193f149bff Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 23 Apr 2024 12:31:22 +0000 Subject: [PATCH 0911/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c7b048c7092d82aa269911b492e4dd8f6660f628?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3c48561c8..78d0035d9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 57fcc70680c31bcc0162f8739677d8d4ff9d0703 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 23 Apr 2024 16:52:21 +0000 Subject: [PATCH 0912/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@461b9d4c117a4fe1ddd8de83fba915b8e88bc941?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 78d0035d9..af63c4587 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From aae5a348e04de22a82326e10494713fcba23a0d6 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 24 Apr 2024 11:51:56 +0000 Subject: [PATCH 0913/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@08ddaa18a6f96cd1a15a41119c62f3494d0c79f8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index af63c4587..2f9db3342 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 4e98f7ac351811b72fa94acc11570bcdf0470524 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 24 Apr 2024 12:27:39 +0000 Subject: [PATCH 0914/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@13b782ccd7b39d9ed0380a4c999d97b2f4c6e80c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2f9db3342..a6fd4580e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f354dcd2dcbde6f5c0c6db74db9148dd3d014262 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 24 Apr 2024 17:25:09 +0000 Subject: [PATCH 0915/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6a8cf48499e6aa79f14966cf81bd982c361855a0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a6fd4580e..cdbeb6e9c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From b8c0d047db0b5cb8b8bccbe26bcd471fdcd61f63 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 25 Apr 2024 00:51:31 +0000 Subject: [PATCH 0916/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@11ecd11e89058b539ef12a8ed435702d188e8ac8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index cdbeb6e9c..f4ee65e6c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 54db6b68b9f7834b489266356ba4a5477d183dd6 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 25 Apr 2024 09:38:20 +0000 Subject: [PATCH 0917/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1b1e9f196c2c23d9b759caf0ca8fc74f76560fa8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f4ee65e6c..c8c33ac80 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 2401da8e589fd13586933614386b000e9a886f25 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 25 Apr 2024 11:20:14 +0000 Subject: [PATCH 0918/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@e938cf0be8bbcb627c57f5410a3501b58f4fa768?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c8c33ac80..7fe406094 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From fe793028b2cb052f82daefe5aac26dfd0e1028a6 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 25 Apr 2024 23:06:06 +0000 Subject: [PATCH 0919/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@89bbcec674bc3f4a5754102d1c2a6dd080ae9539?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7fe406094..226d9bc3d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 6f7c4dc5a11c46fb0544a0f1f66697ab2ed08e67 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 26 Apr 2024 12:51:00 +0000 Subject: [PATCH 0920/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bfd56743de163ecff6dce714192470cdf5b2a01a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 226d9bc3d..8be995031 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From cde1a0f2eac0274039e46131dd84146d25333d27 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 27 Apr 2024 16:45:38 +0000 Subject: [PATCH 0921/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c07858ddbccbc620689361f29cb051120883c89f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8be995031..1be1784a9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 12d09c409165a0cec21d212119f545b3a7731e70 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 28 Apr 2024 12:37:12 +0000 Subject: [PATCH 0922/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1a3cc50da8f5c2a7d15438896c70977ace78ea7e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 1be1784a9..a26112a37 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 409319d7de9265e1e9257116ad45cbb0646dff03 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 29 Apr 2024 12:11:28 +0000 Subject: [PATCH 0923/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@913f273deffb4fbe051692d474762e5d7e2f5dca?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a26112a37..ec9d3537f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 8a7a9145314cf0dd9183bb30689fa460a8e8d3f6 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 29 Apr 2024 15:20:21 +0000 Subject: [PATCH 0924/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@eaeab1417252b5b1f7ce985ba9985954d26e0df5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ec9d3537f..bed19fe80 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From c7285d709766e8adc88e52194e8a3f80d70531ac Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 29 Apr 2024 16:05:58 +0000 Subject: [PATCH 0925/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@20f87c3447220d61542e0a2212ad6de3ffd0d1b6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bed19fe80..5e6434b47 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 781fcd8c0b9a79e2f246006cfc4c039599a138aa Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 29 Apr 2024 16:38:43 +0000 Subject: [PATCH 0926/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d12cea79cc841900860af35f57e1a1547e7f2d71?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5e6434b47..751830a9e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 9b8a530446100db0bec298b5ffe51fd0d7105fc7 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 29 Apr 2024 17:35:05 +0000 Subject: [PATCH 0927/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ab3b20723e032e0841c0f081c5e082233f230d88?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 751830a9e..65be56184 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f2cc746f529c5c34afe1a07b2783532acee6bd16 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 30 Apr 2024 10:10:40 +0000 Subject: [PATCH 0928/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@81f53551f2a6a899fdd3c704f81200d0a6f8bb03?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 65be56184..814a041a4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From a5d10c524798b1929ac284d79594e4a0f213ea82 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 30 Apr 2024 10:44:22 +0000 Subject: [PATCH 0929/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5089b98deed4e448ce0ba285c9872165e2b4975b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 814a041a4..290ccaf23 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ed63a879a04fb64e8b726bbe6d1978d9947665db Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 30 Apr 2024 13:38:26 +0000 Subject: [PATCH 0930/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4fd355887c3b38ed61580dad861b6685452cfe91?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 290ccaf23..d6558eb76 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 69bfbe7bcd8d3eda35f9e917a2d303d245dc43cc Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 1 May 2024 10:36:07 +0000 Subject: [PATCH 0931/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@620f385bb2c5a5ee2379648a390528815513e061?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d6558eb76..19c6cccc9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From b15e320b52e68902eb82fb657c1aee08af44bb5f Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 1 May 2024 12:18:39 +0000 Subject: [PATCH 0932/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b3fc52ae60758b2328b5bb840008cf2dacf6cab1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 19c6cccc9..0d78d9203 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 9c1b40bf17a25ad7a6d58e0dbdb0dfa0d781f69e Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 1 May 2024 15:56:44 +0000 Subject: [PATCH 0933/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@24be670fbbc08858314387decec7d2e94f6c621a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0d78d9203..e422dcc72 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f4af899baa777ec3a3678692d76b6eaa3be3ecf2 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 2 May 2024 09:49:46 +0000 Subject: [PATCH 0934/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@85a11c8a8c5b819763a5df59aa3612a3498e2360?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e422dcc72..91f7b9c04 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 891a6af335cc750865c4316537f01981e0e6c6bb Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 2 May 2024 10:42:08 +0000 Subject: [PATCH 0935/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@791c7e34757ae8b45f6abf4e73866559a39dfd1e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 91f7b9c04..efea1755a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ba9b99b6871cffca71c3970202ac26161806cd2e Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 2 May 2024 22:22:30 +0000 Subject: [PATCH 0936/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@187e17724b8a84f31c44ffbb0af8e160574d1dbe?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index efea1755a..76dd70c6f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From aa5c47a4f05b333592e0f4c562cb73d05710fb4b Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 3 May 2024 09:30:24 +0000 Subject: [PATCH 0937/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@67c43460cc961ca5aebaabca1bfbddc1d38b3e9b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 76dd70c6f..c9fd4d0e6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 1b75016507da1eb0eda832bd9440de870ee523ce Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 3 May 2024 11:23:35 +0000 Subject: [PATCH 0938/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c41d59d4fb69fa08da95b097da0ac185ea0a36d2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c9fd4d0e6..471f0281a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From adb812bda9e0860367293b94587f35466c71b84c Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 3 May 2024 12:55:34 +0000 Subject: [PATCH 0939/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@76adc53fddf6e33e48f87996aeb496016ee799d2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 471f0281a..869a2595f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d5e7fd9a897d1786749f3afa11cdada6e975b3bb Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 3 May 2024 13:13:22 +0000 Subject: [PATCH 0940/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@85f361eba32a4d8258e7e3ea29c1529c4af7f954?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 869a2595f..ae2268ee2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ef2be25031ab4adce272b9727539edc105533214 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 3 May 2024 14:08:12 +0000 Subject: [PATCH 0941/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@896a0a1502ad0666be62c1b27fe2cea7b4113368?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ae2268ee2..184e092da 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From de2c1aeecf2ebc8e6a6d4979dc59e06136efcb1b Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 3 May 2024 14:21:38 +0000 Subject: [PATCH 0942/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@49cac8dac0437ae013e69ffb59478c4ef08ef876?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 184e092da..8af2db0ff 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 9de6a3a8b105086d98a2bdf925d168cad505dd08 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 3 May 2024 14:28:46 +0000 Subject: [PATCH 0943/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7b11e5e64069dbfda6bcb8348aee5423df000ec2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8af2db0ff..c2e87cccc 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 2bb1dabd3f9f234504c766a96c22b8418d9c0b12 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 3 May 2024 17:30:22 +0000 Subject: [PATCH 0944/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b2ba5f717e8dcff8cc85714036527b5c7c57dc0c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c2e87cccc..4a3b707ec 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d2ead61b8c5fb01791fbf4a6435410d0dbc64fd0 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 4 May 2024 09:20:27 +0000 Subject: [PATCH 0945/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6a43164dc14a1489a7dfc61766710435a8023555?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4a3b707ec..7e119dca5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d5c4797b440cbc2e9e70256748e0eb420c453732 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 4 May 2024 17:03:33 +0000 Subject: [PATCH 0946/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@01629f7ec8f239e3156109b35e0ec4b2c2f8aad9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7e119dca5..c7cd80876 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From cb11796565338a334775fc30f0cd1cbccea2b3a6 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 5 May 2024 10:00:40 +0000 Subject: [PATCH 0947/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@935ab6743230a18863fb0541168aa39d3a16e306?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c7cd80876..79d59f69a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 4d7e4cccb9a963869d1dea43569b688ace3a5adf Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 5 May 2024 10:23:21 +0000 Subject: [PATCH 0948/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ee26c3903a58dc02ba2c56677d6184c66c0ae244?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 79d59f69a..d5d7814bb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ddbd95008e62531f576c7d923c0c0b2824402f81 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 5 May 2024 11:33:52 +0000 Subject: [PATCH 0949/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@0306e4e57e08c0ab5a8850a6e79bb2aaa27faca0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d5d7814bb..abd6cf846 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 110949c8c29b2ec2d14ac41f313674c37cbe3ac9 Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 5 May 2024 11:47:45 +0000 Subject: [PATCH 0950/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@53c1da179bb57a255e419cc6dd946f54b6eb1065?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index abd6cf846..57377a6ad 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From a0f7b1ecd54e47f453b605dc3aaecd300e8c201f Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 5 May 2024 15:31:40 +0000 Subject: [PATCH 0951/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d03f31100ef3eed38e6ec85642a081971b954070?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 57377a6ad..d98686391 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 263830524261bd512570c5a96598fde5b0a1c3db Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 6 May 2024 09:38:58 +0000 Subject: [PATCH 0952/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4edc71584724df0b5da290b9b3028e159349f6fe?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d98686391..344fbbc34 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d72c15416c7fdbc5d9e070149f8f5b58a266c467 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 6 May 2024 15:08:07 +0000 Subject: [PATCH 0953/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5a37f281af162d166b94f65cccff9449892c6f3b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 344fbbc34..64d4ddd18 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 2d3ff9191f408e301e0911c7b41b8fb1e3d4dff5 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 6 May 2024 15:27:17 +0000 Subject: [PATCH 0954/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ca3fa1a1291270cd4dfb39943e774a2c1913ee63?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 64d4ddd18..5d69c7604 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 4891c74ba2ce3d6c1485f086eb7e12d47a1932cf Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 7 May 2024 10:16:19 +0000 Subject: [PATCH 0955/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2594abb4e7c01bc2ff9c843d231e65c5826b3c8d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5d69c7604..153296478 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 91ea0d65eb0d9a818683dd42912aa83b2c3a9313 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 8 May 2024 10:13:16 +0000 Subject: [PATCH 0956/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@98d642511ef9de4b57c597209aab6fd3cfbfa71d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 153296478..189bc086e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 085d9a8b9369e6108ca98da2b294603e5be096b0 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 8 May 2024 18:50:25 +0000 Subject: [PATCH 0957/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@718e02d1e422415ea20678bdc2e50fdf56f238a0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 189bc086e..3d6c494bc 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 0a1b56f09f46e325697dd8dc60b1395f2ee8b3e2 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 9 May 2024 11:27:07 +0000 Subject: [PATCH 0958/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ef80828b220de769c7b5b011d511ae9e21eed9fc?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3d6c494bc..bda4c4338 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From fadcd790e4be1dbf5feac56e13f0dfa1b75d12ea Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 9 May 2024 17:42:08 +0000 Subject: [PATCH 0959/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@71ecc97041176c1281a1be6315446a2052797d9b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bda4c4338..5a13863f3 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From c471f159260d61d7294bd62ac895f15045f59e4b Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 10 May 2024 09:26:05 +0000 Subject: [PATCH 0960/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@3bb5b0366f82f05d063f97d09c0afc9f22b083cf?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5a13863f3..bff7ad675 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 75b143f4e0998cbe72ffeb0efbe335dd57a46c08 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 10 May 2024 09:58:26 +0000 Subject: [PATCH 0961/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@96dc5d624e760c06f7f0d9e57d638de18cc9d320?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bff7ad675..8ea746acb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 41e0a7e570ff7739548009555b1705af7820d9b2 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 11 May 2024 08:58:33 +0000 Subject: [PATCH 0962/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5ce01cf16b875bd19324a9c22e152e5296d83dd4?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8ea746acb..e3b535b0c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 1ef1efa93afe9c67af39a1b97597fdd964edc30c Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 12 May 2024 09:43:03 +0000 Subject: [PATCH 0963/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5db9bedf55d11bee0593714ede69b6ff365e5f17?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e3b535b0c..050ab7464 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From faa8857dfebb2f3cb55d1f363f7e15628cab7470 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 17 May 2024 06:18:15 +0000 Subject: [PATCH 0964/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@963961c3929929c8888a87bd35fe702b7440e10e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 050ab7464..37349529d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f754ec7dc3b3abc1bcdcccc766d2c065adba9018 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 20 May 2024 00:48:00 +0000 Subject: [PATCH 0965/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7759a175980f091331597efdd5785e05ddb32512?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 37349529d..6b3ffc682 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 02601e497a13a47f6a99f9d89b6077ccc2b3da1a Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 21 May 2024 10:43:32 +0000 Subject: [PATCH 0966/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@800d9790dd44938b2c092fb1789a474309a35542?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6b3ffc682..bd7df3ccd 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 5d2ed047daeeea3cd048cd2c46bc41b56a498737 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 18 Jun 2024 10:54:49 +0000 Subject: [PATCH 0967/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ba9f5e1befd2df19291fee57faeb3a0f09473e2e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bd7df3ccd..d4885ac72 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 9ea9d7ef0845f3c59381a5e3a4dcf3ed715d06d5 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 18 Jun 2024 11:13:02 +0000 Subject: [PATCH 0968/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@26fc81acadb27fc9eb7d1226fda58e5f4a67bf1b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d4885ac72..28a63d8fd 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 4e9b19fd18fc99592bf2d84e23f4daf027fd47a6 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 17 Jul 2024 12:29:15 +0000 Subject: [PATCH 0969/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@884ce48822a1eb366292fc58ccaa1de61a24d527?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 28a63d8fd..c64716b45 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From c32a594d6241c084d784c5ce8e463a51fee53f31 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 17 Jul 2024 13:02:03 +0000 Subject: [PATCH 0970/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@57a584402abe802b774401e6444d122dc6dfa0bf?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c64716b45..3e7eeef88 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 9a42ccc8893ada84aa86e7640b7eda7a7c54878e Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 17 Jul 2024 15:37:33 +0000 Subject: [PATCH 0971/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@975c766d1c0b185830f35bc3f64caba766bbd907?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3e7eeef88..00491978d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 87b5d64b547e085efdf0bf54ab39ef855902c270 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 25 Jul 2024 09:34:59 +0000 Subject: [PATCH 0972/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1220ddb5aeea5faaa4d563dfc596c5a344e95cd1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 00491978d..96594ff28 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 635706a1c45bc0fed8da8abad946fddc674f0f81 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 30 Jul 2024 11:39:42 +0000 Subject: [PATCH 0973/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cd41ee917e67ad1507e865a0dbd1179d63dfed05?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 96594ff28..949939a9f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f734a3fc1bf82fa0c7ac6e68543256077b339092 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 30 Jul 2024 11:46:40 +0000 Subject: [PATCH 0974/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@239be3e3bf92b26f5306c4bdd1fa21a2b735e695?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 949939a9f..b160ea221 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 46f9b629b777a79a489fa3d1217678473b421b0c Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 30 Jul 2024 14:53:15 +0000 Subject: [PATCH 0975/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@29552cefdb1371aca0e966b12e6cb73be41d5e3b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b160ea221..ce9def1d4 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 3ff34331ecb049c2fdcb23b0d5ea5fbee35db363 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 30 Jul 2024 18:45:55 +0000 Subject: [PATCH 0976/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f4272c8caa71fbfce04c3a72165a0875e2aacef3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ce9def1d4..125930513 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 05675041c18d71f11f653b429cc2f5a5147c4465 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 31 Jul 2024 17:03:46 +0000 Subject: [PATCH 0977/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5f3b5ec6e32ae3ea24b7eae19a42b38dee2e81d5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 125930513..e924d5e4e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From e12ef84212a91d0a5f755b163e75ee7c2fe20d56 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 1 Aug 2024 15:12:10 +0000 Subject: [PATCH 0978/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@53a88e3e3ec2b9d4d732a0c15fe80d7fe8c5c8dd?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e924d5e4e..21f72251d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From c4abfbda01e52439b6cce75cbdf3b22b1330a2c2 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 1 Aug 2024 17:26:53 +0000 Subject: [PATCH 0979/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@619281a70280bea060f844c39fbd9f63fc27b634?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 21f72251d..d389f9f13 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From eb27bd924d34a2a5ca40db1b28ce5555d4ebe5a3 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 2 Aug 2024 08:40:44 +0000 Subject: [PATCH 0980/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@a5cb3799049dbe873aaea0e69562b8dba8b0427a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d389f9f13..2e49f3969 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 902edafb1ebca4b9763bd979941371506a1eabb4 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 3 Aug 2024 13:04:17 +0000 Subject: [PATCH 0981/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@312fb0317b9b4a9e1741efcf6f5838d23b3f9ded?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 2e49f3969..622521738 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From a83b8216fc736b0871e51e55a0843328d00463ca Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 5 Aug 2024 08:12:25 +0000 Subject: [PATCH 0982/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bfc4205e6cc16d8846af813143a76b5817348c90?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 622521738..7a37ac4cd 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 3bed92d849092a8b0de202b9894f436a34cc0744 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 5 Aug 2024 10:51:15 +0000 Subject: [PATCH 0983/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c6448cf2360c05a66796bdcd6988effd09bd6f19?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7a37ac4cd..e69c41ee6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 998e6d5204dc9c0a6b4acd2729338156418f4578 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 5 Aug 2024 11:47:27 +0000 Subject: [PATCH 0984/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@bc2c3c88a73f12bfc2bc41a35656cd1e624be842?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e69c41ee6..6e37d38c7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 55efda2aa7d334e6429fb388dfb64ea236242ea1 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 5 Aug 2024 14:56:21 +0000 Subject: [PATCH 0985/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@247dcf9d1e8fda381caf89b8d489fcce997368a2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6e37d38c7..e8e2b878d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f8e9ce453223173cac06155ad25bcdbf388a68c3 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 6 Aug 2024 10:01:48 +0000 Subject: [PATCH 0986/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1db61cb5cd54d93a44792cc7150a80b1d262467a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index e8e2b878d..db30a4b45 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 73ae621b811a41e3e263925349d50f8d2df24e3d Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 6 Aug 2024 10:11:00 +0000 Subject: [PATCH 0987/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b767c73f20cbec6a978442c527ffe584bb703690?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index db30a4b45..0679ed8b5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 92288379c90c018e6520a82dd28c9ce89b6ebe49 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 6 Aug 2024 14:20:51 +0000 Subject: [PATCH 0988/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b99d1fe732f79dcb8bca13205faac9488d0ad03c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0679ed8b5..4ca052d79 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 503eadfef520a0d9192b86362fdb65586cd4f340 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 7 Aug 2024 09:16:22 +0000 Subject: [PATCH 0989/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d347b20f980b16b7d96ac831ca020c6696284099?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4ca052d79..8170f8d2b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From da28ef2782a9294e8a7cf717e87f820ab9bb5518 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 7 Aug 2024 10:30:34 +0000 Subject: [PATCH 0990/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1930ed212ade4dce0bda7b622357ff696917fcb2?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8170f8d2b..fad27dc30 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From f7aa8e628daad05c10c598431b1a7e6e8b60f8d3 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 7 Aug 2024 14:47:21 +0000 Subject: [PATCH 0991/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c25be446a44147f91cc538ab1471a4e363aa472f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fad27dc30..5485d7260 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 895614b2829ed132f11981df7f0074ad03b13136 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 8 Aug 2024 17:35:02 +0000 Subject: [PATCH 0992/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@5a2e4f2b75a1e5c2b483d163b4ed683075da8c8a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5485d7260..5da5370d5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ed64058c103d17ce4a84dc01406a169e624f05a7 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 9 Aug 2024 11:05:04 +0000 Subject: [PATCH 0993/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1df58376dd8a5832c92f9b9b7a2df6546cc81329?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5da5370d5..d65f8b83e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From a0afd4aac104b8eaa1ef26e9d4d6b2beb05f3700 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 10 Aug 2024 09:46:11 +0000 Subject: [PATCH 0994/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d6fa4a8953459bbdc6374cf54ac2e9b6cc9137d8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d65f8b83e..4ac35d3c0 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 682a7b8496914acdfabc071b9688dd5737af32c4 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 10 Aug 2024 15:05:03 +0000 Subject: [PATCH 0995/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@fadb5a2ac58a4dff5e03b94cd058c6a81e378a63?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4ac35d3c0..a8c531f8d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From dfa3735dd119fb8a74e5b9e69051f26b7a394504 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 12 Aug 2024 08:07:30 +0000 Subject: [PATCH 0996/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ab27a1dcca80afa36d7703b0611e9e8a00f220c5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index a8c531f8d..01faccc3a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 0067b365000c0b548eb36eb4763446c11b596276 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 12 Aug 2024 14:52:27 +0000 Subject: [PATCH 0997/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7dc5971bac3c4ab46c7ef562c053108d094da8b1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 01faccc3a..d83873469 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 9e50946fd73e4a80bc0de89f9960ff3b51844f89 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 12 Aug 2024 17:20:13 +0000 Subject: [PATCH 0998/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cc0741c454f44264f31a526abe5ec891b889a3c8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d83873469..8d6c29ebb 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 8f492f64524b3a8beb07dab8ec81a491d4012aea Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 13 Aug 2024 11:01:40 +0000 Subject: [PATCH 0999/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@332e5a2e9fbcf8b33f161ed594e1ab5efa4f3d95?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8d6c29ebb..49d49cfe2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 8cd454e66af0ba4cecbd033b77e9552543ebe8f8 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 13 Aug 2024 11:20:54 +0000 Subject: [PATCH 1000/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b0de5d7d469344c1235972ff40adf08f52a5a66a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 49d49cfe2..6cab09669 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 41ac6ff6de1c516e28c655d7a215a62d3feb62f3 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 13 Aug 2024 14:21:57 +0000 Subject: [PATCH 1001/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d79b538122c085a4bc57692ed956e40495a9b8f3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6cab09669..22e90a46d 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From cbe49536b05b2e3353186b3bd44ab5ea04c368f4 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 14 Aug 2024 14:54:57 +0000 Subject: [PATCH 1002/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@01293fcbe587cb03df744fdb40623c277f4a4000?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 22e90a46d..9bb4f0e27 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 08a9edf215672dee2316de3b118a5760711ae729 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 15 Aug 2024 09:02:09 +0000 Subject: [PATCH 1003/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@ad2bc33e68988daa594f30b761f49822992f0f6b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9bb4f0e27..b5954cfac 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 37a657c223099ed265c64e729d1b4acfaf8057a5 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 15 Aug 2024 11:23:41 +0000 Subject: [PATCH 1004/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@08aa9c2f00603d2d00a4149e316a19bca6a17a2e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b5954cfac..6cc171dc2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 89819051cc45db4fb61756ab7c28cfb42c2fe902 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 15 Aug 2024 18:28:23 +0000 Subject: [PATCH 1005/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@edf1351041f1a0dc406fd8da8c52354197dce475?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 6cc171dc2..d1a2b84a7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From b3bbc0cfeba49bcddfe98467140f8d8220f17489 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 16 Aug 2024 08:51:19 +0000 Subject: [PATCH 1006/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@604eceec53bcdc770defe73724402e8129e15957?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d1a2b84a7..3cbd64f61 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From b1eacc83d6d85d69d37fc6bfb85302c4156ea8b2 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 16 Aug 2024 17:21:54 +0000 Subject: [PATCH 1007/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c636af413d8c51edec9f0dcd0599dc3df464d91e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 3cbd64f61..20cca9097 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 9f5576bd2666125d90736579535b8c82c07c9758 Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 16 Aug 2024 18:01:18 +0000 Subject: [PATCH 1008/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4fdfb679650e03e73520aa4416795a466e2862c8?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 20cca9097..b96a346c7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From a3fcda3b638a3a4e8e35f550f2e6e02396b072ab Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 19 Aug 2024 09:08:49 +0000 Subject: [PATCH 1009/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@2f2825b07cfadbd4c535614da1d884330209555b?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b96a346c7..b1d9c2640 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From e8198a467163899d7969d2d8482ae3d2256f6c6f Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 19 Aug 2024 20:14:28 +0000 Subject: [PATCH 1010/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@35b819b0d073f2b6dc4d8c827b663160cba315b0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b1d9c2640..4c33ae93f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 1fd750923aebe4a9f42face066a19fb966c9bcd6 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 19 Aug 2024 20:31:28 +0000 Subject: [PATCH 1011/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d78b5304e7c88b23a2811418a9eed688781aebd1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4c33ae93f..89909a66b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 8367b5319ac777775a9c2c47479c6b8433dbf64b Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 20 Aug 2024 07:56:49 +0000 Subject: [PATCH 1012/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@582c5cae340427ce95dd5980420bd39c5aa38984?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 89909a66b..fca1c27fc 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 11d83d40e456c59c9c1ecc940b2918ae0244c25d Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 20 Aug 2024 23:40:16 +0000 Subject: [PATCH 1013/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@1e837cd702b8da57b502908353afbbef725ea5a5?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index fca1c27fc..b51edc6b1 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 33b2cf35563469a718b810fab626e9949a18380c Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 21 Aug 2024 10:11:29 +0000 Subject: [PATCH 1014/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@687aac0b3e9b1a0a0f071a46e43d354fd3644d49?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b51edc6b1..265b5f479 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 17fa8dc6d877d8eba00beb7ee41521efb42d2e1f Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 21 Aug 2024 15:21:45 +0000 Subject: [PATCH 1015/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7cf41821135f80be69bc16fce3be6bce332f047c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 265b5f479..5f5479772 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From ec028051429b98c58150a2e1597b410703488efb Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 21 Aug 2024 16:35:38 +0000 Subject: [PATCH 1016/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@fdee403934bbba7d8e2774d0c05baffd5828b46e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5f5479772..dfd99c378 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 3efb8f5a437bdae55f8757d542b052e52bbaf567 Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 21 Aug 2024 23:39:53 +0000 Subject: [PATCH 1017/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@32c26897b7c15cedb707a091b66cc659d3c1ca0e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index dfd99c378..c54e71066 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 4cf33076311bda0b872fb94d43b44196799464e9 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 22 Aug 2024 11:03:35 +0000 Subject: [PATCH 1018/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d0b4d983f4e0af703dc11bb13921557e0f97cb05?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c54e71066..7a425348f 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From a44f03f84a55253e7261b0c8d402dd5aa602bba8 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 22 Aug 2024 12:30:51 +0000 Subject: [PATCH 1019/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@b457c7ef2227d56f86391b6733e8a0554d4b1236?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 7a425348f..d2cd9cba2 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 4b584b264bd669d2695bf8501231b1e4d4a573ef Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 22 Aug 2024 15:43:23 +0000 Subject: [PATCH 1020/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@cc34f9bc78a03765800092be4e82e522570871fd?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index d2cd9cba2..584c46247 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From e5f8ad6b9726e0aaa714377bfee67075360f1f06 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 22 Aug 2024 15:46:47 +0000 Subject: [PATCH 1021/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@46c70a30fe13f1c59a5ee93fe1e1de04969ac88d?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 584c46247..5220fe3a5 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 0e2572194f19696b478551ce4d9d4f5f82ac4689 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 22 Aug 2024 17:43:25 +0000 Subject: [PATCH 1022/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f8753ad0c51904798d169ac6c60b0b22d03b9bf7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5220fe3a5..f4c03fc2c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From bf0e8bf9f5471edad325122478c4e6e6520d44cc Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 23 Aug 2024 10:02:27 +0000 Subject: [PATCH 1023/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@28b4acb1d671d5044f5a5061c91f0baff6772a3e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f4c03fc2c..4e9f1353a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 376cec3ce06a0d332436d8316638bc730507597b Mon Sep 17 00:00:00 2001 From: mikera Date: Fri, 23 Aug 2024 11:55:00 +0000 Subject: [PATCH 1024/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@50862d780e867b105548db3cf07736a10c8b6d8c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 4e9f1353a..ddb06ee4c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 927dd5f41afb2476ddf75142751b4d9bfac9434e Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 24 Aug 2024 10:44:27 +0000 Subject: [PATCH 1025/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f82e7a86596ae833891a220fec25a1753083a596?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index ddb06ee4c..c07b6944b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 233ea9a7d5feda2ffbc9877c497df8b378c92c3c Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 24 Aug 2024 21:51:37 +0000 Subject: [PATCH 1026/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@063927dbae2534cd41456a0f9c93c9179b6fdd85?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index c07b6944b..493b8bd0c 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 625c94ce504cf48ba806ee896592c0d0ffcd215d Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 25 Aug 2024 10:14:43 +0000 Subject: [PATCH 1027/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@695bb7d2f1eb95a009daada7d6b9f2dbb7237cb0?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 493b8bd0c..45b071f76 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From c949e61cec4e2a2cff4edefc95ab0210452c1e8a Mon Sep 17 00:00:00 2001 From: mikera Date: Sun, 25 Aug 2024 14:29:47 +0000 Subject: [PATCH 1028/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f7f19de6c660113034985913b059e967732dc8b3?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 45b071f76..5635626b9 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From b3666bcacb6fa461502bce237862a88c6878df92 Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 26 Aug 2024 08:48:41 +0000 Subject: [PATCH 1029/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@8e82fb1def4093b60e5d816c9e9f188266e9481f?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 5635626b9..8012624c7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From a8ec6b96d89585af9278a22690818db832459f5a Mon Sep 17 00:00:00 2001 From: mikera Date: Mon, 26 Aug 2024 20:09:41 +0000 Subject: [PATCH 1030/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4ff3c6f5bddc9978071fcbc4e41415f3d09e9227?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8012624c7..b56a0941e 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From b193b0bfb0a1f5e60d9ecd81510b5d1b051e3bb6 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 27 Aug 2024 06:42:26 +0000 Subject: [PATCH 1031/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@c914a3c25e317f922dbaafb01a4a77d68327ea8e?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index b56a0941e..f386fb473 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 69e430fac62d44d0e51afd76ba19271eda16d314 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 27 Aug 2024 07:04:03 +0000 Subject: [PATCH 1032/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@4c6d6916776922a12296d416430898bdc9dcdea7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index f386fb473..afcda1922 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 862894ae504cecec0c373965f771e2bd726a4082 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 27 Aug 2024 07:33:53 +0000 Subject: [PATCH 1033/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@aed18c1670ec7682daf4717b56927d059bd18af7?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index afcda1922..9d3788c83 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 193c8bd68b926f9d81d11f3c387ba652b1fc5c63 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 27 Aug 2024 07:50:37 +0000 Subject: [PATCH 1034/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@16faf97e7a88575437a472236db4d4d0fcd26605?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 9d3788c83..bff921447 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 616c444a05b1f6e035bbd0351c183b0e3868a9b3 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 27 Aug 2024 10:29:56 +0000 Subject: [PATCH 1035/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@df54f1bd8850315590b8342b31a01832c0f0c5d1?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index bff921447..38ecba248 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 834a75679d551966f62ae3b68cc84bbdbffee83b Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 27 Aug 2024 10:52:15 +0000 Subject: [PATCH 1036/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@7c047e898f680e698f26223f2c0e82dc4aa96ac9?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 38ecba248..0130e419b 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 368b79730e268b00c5b7057a986d5f57aa2d0c21 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 27 Aug 2024 11:56:11 +0000 Subject: [PATCH 1037/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@f2c6663ceedd031540e561619bf30e720f70c9fb?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 0130e419b..384abda1a 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 17e4d13df2b2c7ae2b1bd9d149691f195c1cce86 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 27 Aug 2024 14:04:07 +0000 Subject: [PATCH 1038/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@16d10125b2a37b933c9d758a876560e1448e2221?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 384abda1a..53e8203b7 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From d6bb28dcd0cb7d401fa77c961e97f31bf6d459d1 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 27 Aug 2024 17:49:03 +0000 Subject: [PATCH 1039/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@538310c5869033909eb0a2298a3d4196d3b0fe1c?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 53e8203b7..17f1b2867 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From aecd7f736d36bd9e64c9740d3a8dc7fc5cd4f473 Mon Sep 17 00:00:00 2001 From: mikera Date: Tue, 10 Sep 2024 11:43:03 +0000 Subject: [PATCH 1040/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@6ce3036a5347fc3affb38cf37eb1cf3e3fe34407?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 17f1b2867..8736f52a6 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -2234,7 +2234,7 @@

    Transactions

    From 46d5edc558780e7e5b54ac01111216ab64aedd31 Mon Sep 17 00:00:00 2001 From: mikera Date: Thu, 12 Sep 2024 16:28:17 +0000 Subject: [PATCH 1041/1041] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@?= =?UTF-8?q?=20Convex-Dev/convex@d66653c34af992dff644241b7c9f8926e4016ab6?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex-cli/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/convex-cli/index.html b/convex-cli/index.html index 8736f52a6..093316264 100644 --- a/convex-cli/index.html +++ b/convex-cli/index.html @@ -1615,7 +1615,7 @@

    Starting the peer with convex.worl
    -
    ./convex peer start --public-key=dfbb22 --password=my-password --address=<address> --port=8088 --url=<my-ip>:8088 --peer=convex.world:18888
    +
    ./convex peer start --public-key=dfbb22 --password=my-password --address=<address> --port=80888 --url=<my-ip>:8088 --peer=convex.world:18888
    @@ -2234,7 +2234,7 @@

    Transactions