├── LICENSE ├── README.md ├── banner.png └── contracts ├── .env.template ├── .gitignore ├── EOSPixels ├── EOSPixels.cpp ├── EOSPixels.hpp ├── Makefile ├── catch.hpp ├── config.hpp ├── eosio.hpp ├── fuzz.cpp ├── memo.hpp ├── memo.test.cpp ├── printu128.hpp ├── test.cpp ├── test_helper.hpp └── types.hpp ├── README.md ├── actions ├── autoend.js ├── changedur.js ├── clearAccts.js ├── clearCanvases.js ├── clearPixels.js ├── createAcct.js ├── createpxrs.js ├── end.js ├── init.js ├── refresh.js ├── resetquota.js └── withdraw.js ├── config.js ├── package.json ├── scripts ├── _update_auth.js ├── buy_ram.js ├── deploy.js ├── dump_tables.js ├── init.js └── update_auth.js ├── utils.js └── yarn.lock /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 EOS Asia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EOS Pixels - Paint pixels, earn tokens! 2 | 3 | The first collaborative artwork on blockchain. 4 | 5 | ![The first collaborative artwork on blockchain.](/banner.png) 6 | 7 | ## Setup 8 | 9 | Go to contracts folder: 10 | 11 | ``` 12 | cd contracts 13 | ``` 14 | 15 | Install project dependencies: 16 | 17 | ``` 18 | yarn 19 | ``` 20 | 21 | ### Local EOS test network extra steps 22 | 23 | #### Run nodeos in docker: 24 | 25 | ```bash 26 | # cd to path-to-repo/contracts frist 27 | yarn nodeos:docker 28 | 29 | # alias cleos 30 | alias cleos='docker exec -it eosio /opt/eosio/bin/cleos -u http://0.0.0.0:8888 --wallet-url http://0.0.0.0:8888' 31 | ``` 32 | 33 | #### Initialize wallet if not already 34 | 35 | ```sh 36 | cleos wallet create 37 | # Remember to save the password 38 | cleos wallet import --private-key 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 39 | ``` 40 | 41 | #### Create EOS token if not already 42 | 43 | Create eosio.token account: 44 | 45 | ``` 46 | cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV 47 | ``` 48 | 49 | Deploy eosio.token contract: 50 | 51 | ``` 52 | cleos set contract eosio.token contracts/eosio.token -p eosio.token@active 53 | ``` 54 | 55 | Create EOS token: 56 | 57 | ``` 58 | cleos push action eosio.token create '[ "eosio", "1000000000.0000 EOS"]' -p eosio.token 59 | ``` 60 | 61 | ## Compile & Deploy 62 | 63 | > Note: For [Kylin Testnet](https://www.cryptokylin.io/) or [Jungle Testnet](http://jungle.cryptolions.io/), you need to create the contract account on the corresponding website first, and then you also need to buy some ram using faucet supply tokens. 64 | 65 | Copy the `.env.template` to `.env` and add your private key there and edit your contract name and network node infomations. 66 | 67 | [Kylin Testnet](https://www.cryptokylin.io/) example configuration: 68 | 69 | ``` 70 | EOS_CONTRACT_NAME=myeospixels1 71 | CONTRACT_PRIVATE_KEY=5...... 72 | EOS_NETWORK_PROTOCOL=https 73 | EOS_NETWORK_HOST=api-kylin.eosasia.one 74 | EOS_NETWORK_PORT=443 75 | EOS_NETWORK_CHAINID=5fff1dae8dc8e2fc4d5b23b2c7665c97f9e9d8edf2b6485a86ba311c25639191 76 | ``` 77 | 78 | Compile and deploy contracts: 79 | 80 | ```sh 81 | # Local EOS test network 82 | yarn start:docker 83 | 84 | # yarn start 85 | ``` 86 | 87 | ## Scripts 88 | 89 | > Tip: for local eos testnet, you won't get error detail from api respond, use `docker logs --tail 10 -f eosio` to trace the chain logs. 90 | 91 | Clear docker data: 92 | 93 | ```sh 94 | # stop and remove container 95 | docker stop eosio 96 | 97 | # remove data 98 | yarn clear:docker 99 | ``` 100 | 101 | Update contract (recompile and deploy): 102 | 103 | ```sh 104 | # Local EOS test network 105 | yarn update-contract:docker 106 | 107 | # yarn update-contract 108 | ``` 109 | 110 | Create new canvas after current canvas is done: 111 | 112 | ``` 113 | yarn @end 114 | ``` 115 | 116 | Create new canvas automatically: 117 | 118 | ``` 119 | yarn @autoend 120 | ``` 121 | 122 | Withdraw using tester account: 123 | 124 | ```sh 125 | yarn @withdraw '0.0010 EOS' 126 | ``` 127 | 128 | Inspecting the contract's tables: 129 | 130 | ``` 131 | yarn dump-tables 132 | ``` 133 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eosasia/eospixels/df81bbbfd7d995479bfcbe9b3bf959a5a452e5af/banner.png -------------------------------------------------------------------------------- /contracts/.env.template: -------------------------------------------------------------------------------- 1 | EOS_CONTRACT_NAME=eospixels 2 | CONTRACT_PRIVATE_KEY=5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 3 | EOS_NETWORK_PROTOCOL=http 4 | EOS_NETWORK_HOST=127.0.0.1 5 | EOS_NETWORK_PORT=8888 6 | EOS_NETWORK_CHAINID=cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f 7 | TESTER_NAME=tester 8 | TESTER_PRIVATE_KEY=5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 9 | -------------------------------------------------------------------------------- /contracts/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tmp/ 3 | *.wasm 4 | *.wast 5 | *.abi 6 | 7 | 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | .env 15 | .*.env 16 | .env.* 17 | !.env.template 18 | .envrc 19 | 20 | node_modules/ 21 | .cache-loader/ 22 | .vscode/ 23 | 24 | *.css.d.ts 25 | 26 | *.wasm 27 | *.wast 28 | *.abi 29 | 30 | build -------------------------------------------------------------------------------- /contracts/EOSPixels/EOSPixels.cpp: -------------------------------------------------------------------------------- 1 | #include "EOSPixels.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "memo.hpp" 8 | #include "types.hpp" 9 | 10 | using namespace eosio; 11 | using namespace std; 12 | 13 | template 14 | void clear_table(multi_index *table, uint16_t limit) { 15 | auto it = table->begin(); 16 | uint16_t count = 0; 17 | while (it != table->end() && count < limit) { 18 | it = table->erase(it); 19 | count++; 20 | } 21 | } 22 | 23 | // template 24 | // void clear_table_r(multi_index *table, uint16_t limit) { 25 | // auto it = table->end(); 26 | // uint16_t count = 0; 27 | // while (it != table->begin() && count < limit) { 28 | // it--; 29 | // it = table->erase(it); 30 | // count++; 31 | // } 32 | // } 33 | 34 | void eospixels::clearpixels(uint16_t count, uint16_t nonce) { 35 | require_auth(TEAM_ACCOUNT); 36 | 37 | auto itr = canvases.begin(); 38 | eosio_assert(itr != canvases.end(), "no canvas exists"); 39 | 40 | pixel_store pixels(_self, itr->id); 41 | clear_table(&pixels, count); 42 | } 43 | 44 | // void eospixels::clearpixelsr(uint16_t count, uint16_t nonce) { 45 | // require_auth(TEAM_ACCOUNT); 46 | 47 | // auto itr = canvases.begin(); 48 | // eosio_assert(itr != canvases.end(), "no canvas exists"); 49 | 50 | // pixel_store pixels(_self, itr->id); 51 | // clear_table_r(&pixels, count); 52 | // } 53 | 54 | void eospixels::clearaccts(uint16_t count, uint16_t nonce) { 55 | require_auth(TEAM_ACCOUNT); 56 | 57 | clear_table(&accounts, count); 58 | } 59 | 60 | void eospixels::clearcanvs(uint16_t count, uint16_t nonce) { 61 | require_auth(TEAM_ACCOUNT); 62 | 63 | clear_table(&canvases, count); 64 | } 65 | 66 | void eospixels::resetquota() { 67 | require_auth(TEAM_ACCOUNT); 68 | 69 | auto guardItr = guards.begin(); 70 | if (guardItr == guards.end()) { 71 | guards.emplace(_self, [&](guard &grd) { 72 | grd.id = 0; 73 | grd.quota = WITHDRAW_QUOTA; 74 | }); 75 | } else { 76 | guards.modify(guardItr, 0, [&](guard &grd) { grd.quota = WITHDRAW_QUOTA; }); 77 | } 78 | } 79 | 80 | // FIXME change allPixels to a reference? 81 | void eospixels::drawPixel(pixel_store &allPixels, 82 | const st_pixelOrder &pixelOrder, 83 | st_transferContext &ctx) { 84 | auto loc = pixelOrder.location(); 85 | 86 | auto pixelRowItr = allPixels.find(loc.row); 87 | 88 | // TODO extract this into its own method 89 | // Emplace & initialize empty row if it doesn't already exist 90 | bool hasRow = pixelRowItr != allPixels.end(); 91 | if (!hasRow) { 92 | pixelRowItr = allPixels.emplace(_self, [&](pixel_row &pixelRow) { 93 | pixelRow.row = loc.row; 94 | pixelRow.initialize_empty_pixels(); 95 | }); 96 | } 97 | 98 | auto pixels = pixelRowItr->pixels; 99 | auto pixel = pixels[loc.col]; 100 | 101 | auto result = ctx.purchase(pixel, pixelOrder); 102 | if (result.isSkipped) { 103 | return; 104 | } 105 | 106 | allPixels.modify(pixelRowItr, 0, [&](pixel_row &pixelRow) { 107 | pixelRow.pixels[loc.col] = {pixelOrder.color, pixel.nextPriceCounter(), 108 | ctx.purchaser}; 109 | }); 110 | 111 | if (!result.isFirstBuyer) { 112 | deposit(pixel.owner, result.ownerEarningScaled); 113 | } 114 | } 115 | 116 | bool eospixels::isValidReferrer(account_name name) { 117 | auto it = accounts.find(name); 118 | 119 | if (it == accounts.end()) { 120 | return false; 121 | } 122 | 123 | // referrer must have painted at least one pixel 124 | return it->pixelsDrawn > 0; 125 | } 126 | 127 | void eospixels::onTransfer(const currency::transfer &transfer) { 128 | if (transfer.to != _self) return; 129 | 130 | auto canvasItr = canvases.begin(); 131 | eosio_assert(canvasItr != canvases.end(), "game not started"); 132 | auto canvas = *canvasItr; 133 | eosio_assert(!canvas.isEnded(), "game ended"); 134 | 135 | auto from = transfer.from; 136 | auto accountItr = accounts.find(from); 137 | eosio_assert(accountItr != accounts.end(), 138 | "account not registered to the game"); 139 | 140 | pixel_store allPixels(_self, canvas.id); 141 | 142 | auto memo = TransferMemo(); 143 | memo.parse(transfer.memo); 144 | 145 | auto ctx = st_transferContext(); 146 | ctx.amountLeft = transfer.quantity.amount; 147 | ctx.purchaser = transfer.from; 148 | ctx.referrer = memo.referrer; 149 | 150 | // Remove referrer if it is invalid 151 | if (ctx.referrer != 0 && 152 | (ctx.referrer == from || !isValidReferrer(ctx.referrer))) { 153 | ctx.referrer = 0; 154 | } 155 | 156 | // Every pixel has a "fee". For IPO the fee is the whole pixel price. For 157 | // takeover, the fee is a percentage of the price increase. 158 | 159 | for (auto &pixelOrder : memo.pixelOrders) { 160 | drawPixel(allPixels, pixelOrder, ctx); 161 | } 162 | 163 | size_t paintSuccessPercent = 164 | ctx.paintedPixelCount * 100 / memo.pixelOrders.size(); 165 | eosio_assert(paintSuccessPercent >= 80, "Too many pixels did not paint."); 166 | 167 | if (ctx.amountLeft > 0) { 168 | // Refund user with whatever is left over 169 | deposit(from, ctx.amountLeftScaled()); 170 | } 171 | 172 | ctx.updateFeesDistribution(); 173 | 174 | canvases.modify(canvasItr, 0, [&](auto &cv) { 175 | cv.lastPaintedAt = now(); 176 | cv.lastPainter = from; 177 | 178 | ctx.updateCanvas(cv); 179 | }); 180 | 181 | accounts.modify(accountItr, 0, 182 | [&](account &acct) { ctx.updatePurchaserAccount(acct); }); 183 | 184 | if (ctx.hasReferrer()) { 185 | deposit(ctx.referrer, ctx.referralEarningScaled); 186 | } 187 | } 188 | 189 | void eospixels::end() { 190 | // anyone can create new canvas 191 | auto itr = canvases.begin(); 192 | eosio_assert(itr != canvases.end(), "no canvas exists"); 193 | 194 | auto c = *itr; 195 | eosio_assert(c.isEnded(), "canvas still has time left"); 196 | 197 | // reclaim memory 198 | canvases.erase(itr); 199 | 200 | // create new canvas 201 | canvases.emplace(_self, [&](canvas &newCanvas) { 202 | newCanvas.id = c.id + 1; 203 | newCanvas.lastPaintedAt = now(); 204 | newCanvas.duration = CANVAS_DURATION; 205 | }); 206 | } 207 | 208 | void eospixels::refreshLastPaintedAt() { 209 | auto itr = canvases.begin(); 210 | eosio_assert(itr != canvases.end(), "no canvas exists"); 211 | 212 | canvases.modify(itr, 0, 213 | [&](canvas &newCanvas) { newCanvas.lastPaintedAt = now(); }); 214 | } 215 | 216 | void eospixels::refresh() { 217 | require_auth(TEAM_ACCOUNT); 218 | 219 | refreshLastPaintedAt(); 220 | } 221 | 222 | void eospixels::changedur(time duration) { 223 | require_auth(TEAM_ACCOUNT); 224 | 225 | auto itr = canvases.begin(); 226 | eosio_assert(itr != canvases.end(), "no canvas exists"); 227 | 228 | canvases.modify(itr, 0, 229 | [&](canvas &newCanvas) { newCanvas.duration = duration; }); 230 | } 231 | 232 | void eospixels::createacct(const account_name account) { 233 | require_auth(account); 234 | 235 | auto itr = accounts.find(account); 236 | eosio_assert(itr == accounts.end(), "account already exist"); 237 | 238 | accounts.emplace(account, [&](auto &acct) { acct.owner = account; }); 239 | } 240 | 241 | void eospixels::init() { 242 | require_auth(_self); 243 | // make sure table records is empty 244 | eosio_assert(canvases.begin() == canvases.end(), "already initialized"); 245 | 246 | canvases.emplace(_self, [&](canvas &newCanvas) { 247 | newCanvas.id = 0; 248 | newCanvas.lastPaintedAt = now(); 249 | newCanvas.duration = CANVAS_DURATION; 250 | }); 251 | } 252 | 253 | // void eospixels::createpxrs(uint16_t start, uint16_t end) { 254 | // require_auth(TEAM_ACCOUNT); 255 | 256 | // auto itr = canvases.begin(); 257 | // eosio_assert(itr != canvases.end(), "no canvas exists"); 258 | 259 | // pixel_store pixels(_self, itr->id); 260 | // for (uint16_t i = start; i < end; i++) { 261 | // pixels.emplace( 262 | // _self, [&](pixel_row &pixelRow) { pixelRow.row = i; }); 263 | // } 264 | // } 265 | 266 | void eospixels::withdraw(const account_name to) { 267 | require_auth(to); 268 | 269 | auto canvasItr = canvases.begin(); 270 | eosio_assert(canvasItr != canvases.end(), "no canvas exists"); 271 | 272 | auto canvas = *canvasItr; 273 | eosio_assert(canvas.pixelsDrawn >= WITHDRAW_PIXELS_THRESHOLD, 274 | "canvas still in game initialization"); 275 | 276 | auto acctItr = accounts.find(to); 277 | eosio_assert(acctItr != accounts.end(), "unknown account"); 278 | 279 | auto guardItr = guards.begin(); 280 | eosio_assert(guardItr != guards.end(), "no withdraw guard exists"); 281 | 282 | auto player = *acctItr; 283 | auto grd = *guardItr; 284 | 285 | uint64_t withdrawAmount = calculateWithdrawalAndUpdate(canvas, player, grd); 286 | 287 | guards.modify(guardItr, 0, [&](guard &g) { g.quota = grd.quota; }); 288 | 289 | accounts.modify(acctItr, 0, [&](account &acct) { 290 | acct.balanceScaled = player.balanceScaled; 291 | acct.maskScaled = player.maskScaled; 292 | }); 293 | 294 | auto quantity = asset(withdrawAmount, EOS_SYMBOL); 295 | action(permission_level{_self, N(active)}, N(eosio.token), N(transfer), 296 | std::make_tuple(_self, to, quantity, 297 | std::string("Withdraw from EOS Pixels"))) 298 | .send(); 299 | } 300 | 301 | void eospixels::deposit(const account_name user, 302 | const uint128_t quantityScaled) { 303 | eosio_assert(quantityScaled > 0, "must deposit positive quantity"); 304 | 305 | auto itr = accounts.find(user); 306 | 307 | accounts.modify(itr, 0, 308 | [&](auto &acct) { acct.balanceScaled += quantityScaled; }); 309 | } 310 | 311 | void eospixels::apply(account_name contract, action_name act) { 312 | if (contract == N(eosio.token) && act == N(transfer)) { 313 | // React to transfer notification. 314 | // DANGER: All methods MUST check whethe token symbol is acceptable. 315 | 316 | auto transfer = unpack_action_data(); 317 | eosio_assert(transfer.quantity.symbol == EOS_SYMBOL, 318 | "must pay with EOS token"); 319 | onTransfer(transfer); 320 | return; 321 | } 322 | 323 | if (contract != _self) return; 324 | 325 | // needed for EOSIO_API macro 326 | auto &thiscontract = *this; 327 | switch (act) { 328 | // first argument is name of CPP class, not contract 329 | EOSIO_API(eospixels, (init)(refresh)(changedur)(end)(createacct)(withdraw)( 330 | clearpixels)(clearaccts)(clearcanvs)(resetquota)) 331 | }; 332 | } 333 | 334 | extern "C" { 335 | [[noreturn]] void apply(uint64_t receiver, uint64_t code, uint64_t action) { 336 | eospixels pixels(receiver); 337 | pixels.apply(code, action); 338 | eosio_exit(0); 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /contracts/EOSPixels/EOSPixels.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "config.hpp" 6 | #include "types.hpp" 7 | 8 | #define EOS_SYMBOL S(4, EOS) // MainNet and TestNet use EOS 9 | 10 | using namespace eosio; 11 | 12 | class eospixels : public contract { 13 | public: 14 | eospixels(account_name self) 15 | : contract(self), 16 | canvases(self, self), 17 | accounts(self, self), 18 | guards(self, self) {} 19 | 20 | // the first argument of multi_index must be the name of the table 21 | // in the ABI! 22 | typedef multi_index canvas_store; 23 | typedef multi_index pixel_store; 24 | typedef multi_index account_store; 25 | typedef multi_index guard_store; 26 | 27 | void onTransfer(const currency::transfer& transfer); 28 | /// @abi action 29 | void init(); 30 | /// @abi action 31 | void refresh(); 32 | /// @abi action 33 | void changedur(time duration); 34 | /// @abi action 35 | void end(); 36 | /// @abi action 37 | void createacct(const account_name account); 38 | /// @abi action 39 | void withdraw(const account_name to); 40 | /// @abi action 41 | void clearpixels(uint16_t count, uint16_t nonce); 42 | /// @abi action 43 | void clearaccts(uint16_t count, uint16_t nonce); 44 | /// @abi action 45 | void clearcanvs(uint16_t count, uint16_t nonce); 46 | /// @abi action 47 | void resetquota(); 48 | 49 | void apply(account_name contract, action_name act); 50 | 51 | private: 52 | canvas_store canvases; 53 | account_store accounts; 54 | guard_store guards; 55 | 56 | bool isValidReferrer(account_name name); 57 | 58 | void deposit(const account_name user, const uint128_t quantityScaled); 59 | 60 | void drawPixel(pixel_store& allPixels, const st_pixelOrder& pixelOrder, 61 | st_transferContext& ctx); 62 | void refreshLastPaintedAt(); 63 | }; 64 | -------------------------------------------------------------------------------- /contracts/EOSPixels/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test fuzz 2 | 3 | test: test.bin 4 | ./test.bin 5 | 6 | test.bin: *.hpp *.cpp 7 | g++ -DGCC -std=c++14 -o test.bin test.cpp 8 | 9 | fuzz: fuzz.bin 10 | fuzz.bin 11 | 12 | fuzz.bin: fuzz.cpp 13 | g++ -DGCC -std=c++14 -o fuzz.bin fuzz.cpp 14 | -------------------------------------------------------------------------------- /contracts/EOSPixels/config.hpp: -------------------------------------------------------------------------------- 1 | #ifndef _PIXELS_CONFIG_HPP 2 | 3 | #define DEFAULT_PRICE 500 4 | #define FEE_PERCENTAGE 25 5 | 6 | #define PRICE_MULTIPLIER 1.35 7 | #define CANVAS_DURATION 60 * 60 * 24 8 | #define MAX_COORDINATE_X_PLUS_ONE 1000 9 | #define MAX_COORDINATE_Y_PLUS_ONE 1000 10 | #define PIXELS_PER_ROW 50 11 | 12 | #define PATRON_BONUS_PERCENTAGE_POINTS 40 13 | #define POT_PERCENTAGE_POINTS 25 14 | 15 | // number of pixels sold anything may be withdrawn 16 | #define WITHDRAW_PIXELS_THRESHOLD 150000 17 | 18 | #define REFERRER_PERCENTAGE_POINTS 8 19 | // TEAM_PERCENTAGE == 27 20 | #define PRECISION_BASE 1e16 21 | #define TEAM_ACCOUNT N(CHANGE_THIS) 22 | 23 | #define WITHDRAW_QUOTA 10000000 // 1000 EOS 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /contracts/EOSPixels/eosio.hpp: -------------------------------------------------------------------------------- 1 | // poorman's eoslib. minimal definitions to allow some files to compile both as 2 | // smart contract and as normal cpp program 3 | 4 | #ifndef _PIXEL_EOSIO 5 | #define _PIXEL_EOSIO 6 | 7 | #ifdef GCC 8 | #include 9 | #define eosio_assert(A, B) assert((A) && B) 10 | 11 | /** 12 | * @brief Name of an account 13 | * @details Name of an account 14 | */ 15 | typedef uint64_t account_name; 16 | 17 | /** 18 | * Converts a base32 symbol into its binary representation, used by 19 | * string_to_name() 20 | * 21 | * @brief Converts a base32 symbol into its binary representation, used by 22 | * string_to_name() 23 | * @param c - Character to be converted 24 | * @return constexpr char - Converted character 25 | * @ingroup types 26 | */ 27 | static constexpr char char_to_symbol(char c) { 28 | if (c >= 'a' && c <= 'z') return (c - 'a') + 6; 29 | if (c >= '1' && c <= '5') return (c - '1') + 1; 30 | return 0; 31 | } 32 | 33 | /** 34 | * Converts a base32 string to a uint64_t. This is a constexpr so that 35 | * this method can be used in template arguments as well. 36 | * 37 | * @brief Converts a base32 string to a uint64_t. 38 | * @param str - String representation of the name 39 | * @return constexpr uint64_t - 64-bit unsigned integer representation of the 40 | * name 41 | * @ingroup types 42 | */ 43 | static constexpr uint64_t string_to_name(const char* str) { 44 | uint32_t len = 0; 45 | while (str[len]) ++len; 46 | 47 | uint64_t value = 0; 48 | 49 | for (uint32_t i = 0; i <= 12; ++i) { 50 | uint64_t c = 0; 51 | if (i < len && i <= 12) c = uint64_t(char_to_symbol(str[i])); 52 | 53 | if (i < 12) { 54 | c &= 0x1f; 55 | c <<= 64 - 5 * (i + 1); 56 | } else { 57 | c &= 0x0f; 58 | } 59 | 60 | value |= c; 61 | } 62 | 63 | return value; 64 | } 65 | 66 | // This EOS definition conflcits with std::time 67 | // typedef uint32_t time; 68 | uint32_t now() { return 0; } 69 | 70 | #define N(X) string_to_name(#X) 71 | #define EOSLIB_SERIALIZE(A, B) 72 | #else 73 | #include 74 | #endif 75 | 76 | #endif 77 | -------------------------------------------------------------------------------- /contracts/EOSPixels/fuzz.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "config.hpp" 4 | #include "types.hpp" 5 | 6 | #include "printu128.hpp" 7 | 8 | uint64_t totalPaid = 0; 9 | uint128_t totalPaidToOwnerScaled = 0; 10 | 11 | void buyNonBlankPixels(size_t npixels, canvas& cnv, account& player) { 12 | auto ctx = st_transferContext{}; 13 | 14 | // a blank pixel 15 | auto pxl = pixel{}; 16 | pxl.owner = N(somebody); 17 | pxl.color = 0xff0000ff; 18 | pxl.priceCounter = 0; 19 | 20 | uint64_t purchasePrice = pxl.nextPrice(); 21 | uint64_t purchaseAmount = npixels * purchasePrice; 22 | totalPaid += purchaseAmount; 23 | ctx.amountLeft = purchaseAmount; 24 | ctx.purchaser = player.owner; 25 | 26 | // buying a non-blank pixel (first take over) 27 | auto pxlOrder = st_pixelOrder{}; 28 | pxlOrder.color = 0x00ff00ff; 29 | pxlOrder.priceCounter = 1; 30 | 31 | // buy n blank pixels 32 | for (size_t i = 0; i < npixels; i++) { 33 | auto result = ctx.purchase(pxl, pxlOrder); 34 | totalPaidToOwnerScaled += result.ownerEarningScaled; 35 | } 36 | 37 | ctx.updateFeesDistribution(); 38 | ctx.updateCanvas(cnv); 39 | ctx.updatePurchaserAccount(player); 40 | } 41 | 42 | void buyBlankPixels(size_t npixels, canvas& cnv, account& player) { 43 | auto ctx = st_transferContext{}; 44 | 45 | uint64_t purchaseAmount = npixels * DEFAULT_PRICE; 46 | totalPaid += purchaseAmount; 47 | ctx.amountLeft = purchaseAmount; 48 | ctx.purchaser = player.owner; 49 | 50 | // a blank pixel 51 | auto pxl = pixel{}; 52 | 53 | // buying a blank pixel 54 | auto pxlOrder = st_pixelOrder{}; 55 | pxlOrder.color = 0xff0000ff; 56 | 57 | // buy n blank pixels 58 | for (size_t i = 0; i < npixels; i++) { 59 | ctx.purchase(pxl, pxlOrder); 60 | } 61 | 62 | ctx.updateFeesDistribution(); 63 | 64 | // printf("ctx.paintedPixelCount: %llu\n", ctx.paintedPixelCount); 65 | 66 | // printf("ctx.totalFeesScaled: "); 67 | // print_u128(ctx.totalFeesScaled); 68 | // printf("\n"); 69 | 70 | // printf("ctx patronBonuses scaled: "); 71 | // print_u128(ctx.patronBonusesScaled); 72 | // printf("\n"); 73 | 74 | ctx.updateCanvas(cnv); 75 | ctx.updatePurchaserAccount(player); 76 | } 77 | 78 | double eos(uint64_t value) { return (double)value / 1e4; } 79 | 80 | int main() { 81 | auto cnv = canvas{}; 82 | 83 | auto player1 = account{}; 84 | player1.owner = N(player1); 85 | 86 | auto player2 = account{}; 87 | player2.owner = N(player2); 88 | 89 | auto player3 = account{}; 90 | player3.owner = N(player3); 91 | 92 | auto player4 = account{}; 93 | player4.owner = N(player4); 94 | 95 | auto player5 = account{}; 96 | player5.owner = N(player5); 97 | 98 | // buyBlankPixels(100, cnv, player1); 99 | // buyBlankPixels(100, cnv, player2); 100 | // buyBlankPixels(1000, cnv, player2); 101 | 102 | // initial layer 103 | // for (size_t i = 0; i < 10; i++) { 104 | // buyBlankPixels(100, cnv, player1); 105 | // } 106 | 107 | // if i buy 1000 blank pixels (50 EOS), how many more pixels bought till i 108 | // break even? 109 | 110 | // first guy to buy 1000 pixels, make it back from patronBonuses when 2000 more 111 | // pixels sold 112 | // for (size_t i = 0; i < 10; i++) { 113 | // buyBlankPixels(100, cnv, player1); 114 | // // breaks even: ~3000 shares 115 | // } 116 | 117 | // for (size_t i = 0; i < 10; i++) { 118 | // buyBlankPixels(100, cnv, player2); 119 | // // breaks even: ~6000 shares 120 | // } 121 | 122 | // for (size_t i = 0; i < 10; i++) { 123 | // buyBlankPixels(100, cnv, player3); 124 | // // breaks even: ~12000 shares 125 | // } 126 | 127 | // for (size_t i = 0; i < 10; i++) { 128 | // buyBlankPixels(100, cnv, player4); 129 | // // breaks even: ~12000 shares 130 | // } 131 | 132 | for (size_t i = 0; i < 1e6; i += 100) { 133 | buyBlankPixels(100, cnv, player5); 134 | } 135 | 136 | // buyBlankPixels(100, cnv, player1); 137 | // buyBlankPixels(1000, cnv, player2); 138 | // buyBlankPixels(1000, cnv, player3); 139 | 140 | // buyBlankPixels(1000, cnv, player2); 141 | // buyBlankPixels(1000, cnv, player3); 142 | // buyBlankPixels(1000, cnv, player3); 143 | 144 | // second layer 145 | // buyNonBlankPixels(1000, cnv, player2); 146 | 147 | // first paint over 148 | // for (size_t i = 0; i < 100; i++) { 149 | // buyNonBlankPixels(100, cnv, player2); 150 | // } 151 | 152 | printf("canvas shares: %llu\n", cnv.shares); 153 | 154 | printf("player1 patronBonuses: "); 155 | print_u128(eos(cnv.playerpatronBonusesScaled(player1) / PRECISION_BASE)); 156 | printf("\n"); 157 | 158 | printf("player2 patronBonuses: "); 159 | print_u128(eos(cnv.playerpatronBonusesScaled(player2) / PRECISION_BASE)); 160 | printf("\n"); 161 | 162 | printf("player3 patronBonuses: "); 163 | print_u128(eos(cnv.playerpatronBonusesScaled(player3) / PRECISION_BASE)); 164 | printf("\n"); 165 | 166 | printf("player4 patronBonuses: "); 167 | print_u128(eos(cnv.playerpatronBonusesScaled(player4) / PRECISION_BASE)); 168 | printf("\n"); 169 | 170 | // everybody buys 20 EOS (400 pixels) 171 | // 250 players 172 | // how much does everyone make? 173 | 174 | // printf("player5 patronBonuses: "); 175 | // print_u128(eos(cnv.playerpatronBonusesScaled(player5) / PRECISION_BASE)); 176 | // printf("\n"); 177 | 178 | // printf("total patronBonuses : "); 179 | // print_u128(eos(cnv.maskScaled * cnv.shares / PRECISION_BASE)); 180 | // printf("\n"); 181 | 182 | printf("team earning: "); 183 | print_u128(eos(cnv.teamScaled / PRECISION_BASE)); 184 | printf("\n"); 185 | 186 | printf("pot: "); 187 | print_u128(eos(cnv.potScaled / PRECISION_BASE)); 188 | printf("\n"); 189 | 190 | printf("paid to owners (EOS): %.4f\n", 191 | eos(totalPaidToOwnerScaled / PRECISION_BASE)); 192 | 193 | uint128_t allPayouts = 194 | cnv.playerpatronBonusesScaled(player1) + cnv.playerpatronBonusesScaled(player2) + 195 | cnv.playerpatronBonusesScaled(player3) + cnv.playerpatronBonusesScaled(player4) + 196 | cnv.playerpatronBonusesScaled(player5) + totalPaidToOwnerScaled + 197 | cnv.teamScaled + cnv.potScaled; 198 | 199 | printf("all payouts (EOS): %.4f\n", eos(allPayouts / PRECISION_BASE)); 200 | 201 | printf("total paid (EOS): %.4f\n", eos(totalPaid)); 202 | 203 | // printf("painted pixels: "); 204 | // print_u128(ctx.paintedPixelCount); 205 | // printf("\n"); 206 | 207 | // printf("amount left: "); 208 | // printf("%llu", ctx.amountLeft); 209 | // printf("\n"); 210 | 211 | // printf("total fees: "); 212 | // print_u128(ctx.totalFeesScaled / PRECISION_BASE); 213 | // printf("\n"); 214 | 215 | // std::cout << "hello" << std::endl; 216 | // uint128_t a = 2; 217 | 218 | // for (size_t i = 0; i < 130; i++) { 219 | // a *= 2; 220 | // print_u128(a); 221 | // printf("\n"); 222 | // } 223 | // // std::cout << a << std::endl; 224 | // print_u128(a); 225 | } 226 | -------------------------------------------------------------------------------- /contracts/EOSPixels/memo.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "eosio.hpp" 5 | #include "types.hpp" 6 | 7 | void splitMemo(std::vector& results, const std::string& memo, 8 | char separator) { 9 | auto start = memo.cbegin(); 10 | auto end = memo.cend(); 11 | 12 | for (auto it = start; it != end; ++it) { 13 | if (*it == separator) { 14 | results.emplace_back(start, it); 15 | start = it + 1; 16 | } 17 | } 18 | if (start != end) results.emplace_back(start, end); 19 | } 20 | 21 | class TransferMemo { 22 | public: 23 | std::vector pixelOrders; 24 | account_name referrer; 25 | 26 | void parse(const std::string& memo) { 27 | std::vector memoParts; 28 | splitMemo(memoParts, memo, ';'); 29 | 30 | if (memoParts.size() == 2) { 31 | referrer = string_to_name(memoParts[1].c_str()); 32 | } else { 33 | referrer = 0; 34 | } 35 | 36 | const auto& ordersStr = memoParts[0]; 37 | 38 | std::vector pixelOrderParts; 39 | splitMemo(pixelOrderParts, ordersStr, ','); 40 | 41 | pixelOrders = std::vector(pixelOrderParts.size()); 42 | 43 | size_t i = 0; 44 | for (auto& pixelOrderPart : pixelOrderParts) { 45 | auto& order = pixelOrders[i]; 46 | order.parse(pixelOrderPart); 47 | i++; 48 | } 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /contracts/EOSPixels/memo.test.cpp: -------------------------------------------------------------------------------- 1 | #include "catch.hpp" 2 | 3 | #include "memo.hpp" 4 | 5 | unsigned int Factorial(unsigned int number) { 6 | return number <= 1 ? number : Factorial(number - 1) * number; 7 | } 8 | 9 | TEST_CASE("Can parse memo referrer", "[memo]") { 10 | auto memo = TransferMemo(); 11 | REQUIRE(memo.referrer == 0); 12 | 13 | memo.parse("0,0;howard"); 14 | REQUIRE(memo.referrer == N(howard)); 15 | 16 | memo.parse("0,0"); 17 | REQUIRE(memo.referrer == 0); 18 | } 19 | 20 | TEST_CASE("Can parse memo pixel orders", "[memo]") { 21 | auto memoStr = 22 | "k5k36s7pxb,k5k55tbrwf,k5k74uftvj,k5k93vjvun,k5kb2wnxtr,k5kd1xrzsv," 23 | "k5kf0yw1rz,k5kh0003r3"; 24 | 25 | auto memo = TransferMemo(); 26 | memo.parse(memoStr); 27 | 28 | REQUIRE(memo.pixelOrders.size() == 8); 29 | 30 | size_t i = 0; 31 | for (auto& order : memo.pixelOrders) { 32 | REQUIRE(order.priceCounter == 0); 33 | REQUIRE(order.y == 465); 34 | REQUIRE(order.x == 416 + i); 35 | REQUIRE(order.color == 0x222222ff); 36 | i++; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /contracts/EOSPixels/printu128.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #define P10_UINT64 10000000000000000000ULL /* 19 zeroes */ 5 | #define E10_UINT64 19 6 | 7 | #define STRINGIZER(x) #x 8 | #define TO_STRING(x) STRINGIZER(x) 9 | 10 | int print_u128(uint128_t u128) { 11 | int rc; 12 | if (u128 > UINT64_MAX) { 13 | uint128_t leading = u128 / P10_UINT64; 14 | uint64_t trailing = u128 % P10_UINT64; 15 | rc = print_u128(leading); 16 | rc += printf("%." TO_STRING(E10_UINT64) PRIu64, trailing); 17 | } else { 18 | uint64_t u64 = u128; 19 | rc = printf("%" PRIu64, u64); 20 | } 21 | return rc; 22 | } 23 | -------------------------------------------------------------------------------- /contracts/EOSPixels/test.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include "catch.hpp" 3 | #include "test_helper.hpp" 4 | 5 | #include "memo.test.cpp" 6 | 7 | SCENARIO("game economics") { 8 | WHEN("a player buys 1000 blank pixels") { 9 | auto tt = TestContext{1}; 10 | 11 | auto& cnv = tt.cnv; 12 | auto& player1 = tt.accounts[0]; 13 | 14 | tt.buyPixels(tt, 1000, 0, player1); 15 | 16 | REQUIRE(tt.cnv.pixelsDrawn == 1000); 17 | REQUIRE(tt.totalPaid == 500000); 18 | 19 | REQUIRE(cnv.potScaled / PRECISION_BASE == 165000); 20 | REQUIRE(cnv.teamScaled / PRECISION_BASE == 135000); 21 | REQUIRE(tt.totalReferralPaidScaled == 0); 22 | tt.validatePayouts(); 23 | } 24 | 25 | WHEN("a player buys pixels with referral") { 26 | auto tt = TestContext{1}; 27 | 28 | auto& cnv = tt.cnv; 29 | auto& player1 = tt.accounts[0]; 30 | 31 | tt.buyPixels(tt, 1000, 0, player1, N(aReferrer)); 32 | 33 | REQUIRE((uint64_t)(tt.totalReferralPaidScaled / PRECISION_BASE) == 40000); 34 | REQUIRE(cnv.potScaled / PRECISION_BASE == 125000); 35 | REQUIRE(cnv.teamScaled / PRECISION_BASE == 135000); 36 | } 37 | 38 | WHEN("many players buy 150k blank pixels") { 39 | auto tt = TestContext{250}; 40 | auto& cnv = tt.cnv; 41 | 42 | auto& accounts = tt.accounts; 43 | 44 | for (auto& account : accounts) { 45 | size_t npixels = 600; 46 | for (size_t i = 0; i < npixels; i += 50) tt.buyPixels(tt, 50, 0, account); 47 | } 48 | 49 | REQUIRE(tt.cnv.pixelsDrawn == 150e3); 50 | REQUIRE(tt.totalPaid == DEFAULT_PRICE * 150e3); 51 | tt.validatePayouts(); 52 | 53 | THEN("early players should earn more patronBonuses") { 54 | for (size_t i = 1; i < accounts.size(); i++) { 55 | auto& accountA = accounts[i - 1]; 56 | auto& accountB = accounts[i]; 57 | 58 | REQUIRE(cnv.patronBonusScaled(accountA) != 0); 59 | REQUIRE(cnv.patronBonusScaled(accountA) > 60 | cnv.patronBonusScaled(accountB)); 61 | } 62 | } 63 | } 64 | 65 | WHEN("players buy 1 pixel at a time") { 66 | auto tt = TestContext{2}; 67 | 68 | auto& player1 = tt.accounts[0]; 69 | auto& player2 = tt.accounts[1]; 70 | 71 | for (size_t i = 0; i < 500e3; i++) { 72 | tt.buyPixels(tt, 1, 0, player1); 73 | } 74 | 75 | for (size_t i = 0; i < 500e3; i++) { 76 | tt.buyPixels(tt, 1, 0, player2); 77 | } 78 | 79 | REQUIRE(tt.totalPaid == 1e6 * DEFAULT_PRICE); 80 | tt.validatePayouts(); 81 | } 82 | 83 | WHEN("player withdraw patronBonuses and balance") { 84 | auto tt = TestContext{2}; 85 | auto grd = guard{}; 86 | 87 | uint64_t quotaStart = 1000 * 1e4; 88 | grd.quota = quotaStart; 89 | 90 | uint128_t initialBalanceScaled = 1000 * PRECISION_BASE; 91 | 92 | auto& player1 = tt.accounts[0]; 93 | player1.balanceScaled = initialBalanceScaled; 94 | 95 | tt.buyPixels(tt, 10e3, 0, player1); 96 | 97 | auto patronBonusesScaled = tt.cnv.patronBonusScaled(player1); 98 | 99 | uint64_t withdrawAmount = 100 | calculateWithdrawalAndUpdate(tt.cnv, player1, grd); 101 | 102 | REQUIRE( 103 | (uint64_t)(withdrawAmount - (patronBonusesScaled + initialBalanceScaled) / 104 | PRECISION_BASE) == 0); 105 | 106 | REQUIRE(tt.cnv.patronBonusScaled(player1) == 0); 107 | REQUIRE(player1.balanceScaled == 0); 108 | REQUIRE(quotaStart - withdrawAmount == grd.quota); 109 | } 110 | 111 | WHEN("player bids up pixel price to very high") { 112 | auto tt = TestContext{2}; 113 | 114 | auto& player1 = tt.accounts[0]; 115 | 116 | for (size_t i = 0; i < 25; i++) { 117 | tt.buyPixels(tt, 1, i, player1); 118 | } 119 | 120 | // printf("total paid: %llu\n", tt.totalPaid / (uint64_t)1e4); 121 | // printf("total paid owner: %llu\n", 122 | // (uint64_t)(tt.totalPaidToOwnerScaled / PRECISION_BASE / 1e4)); 123 | REQUIRE(tt.totalPaidToOwnerScaled > 0); 124 | tt.validatePayouts(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /contracts/EOSPixels/test_helper.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "config.hpp" 4 | #include "types.hpp" 5 | 6 | class TestContext { 7 | public: 8 | uint64_t totalPaid; 9 | uint128_t totalPaidToOwnerScaled; 10 | uint128_t totalReferralPaidScaled; 11 | 12 | canvas cnv; 13 | std::vector accounts; 14 | 15 | TestContext(size_t naccounts) 16 | : totalPaid(0), totalPaidToOwnerScaled(0), cnv(), accounts(naccounts) {} 17 | 18 | uint128_t payoutsScaled() { 19 | uint128_t allPatronBonus = 0; 20 | 21 | for (auto& account : accounts) { 22 | allPatronBonus += cnv.patronBonusScaled(account); 23 | } 24 | 25 | return cnv.potScaled + cnv.teamScaled + allPatronBonus + 26 | totalPaidToOwnerScaled; 27 | } 28 | 29 | void validatePayouts(uint64_t tolerance = 10) { 30 | uint64_t totalPayouts = payoutsScaled() / PRECISION_BASE; 31 | REQUIRE(totalPaid >= totalPayouts); 32 | REQUIRE(totalPaid - totalPayouts <= tolerance); 33 | } 34 | 35 | void buyPixels(TestContext& tt, size_t npixels, size_t gen, account& player, 36 | account_name referrer = 0) { 37 | auto ctx = st_transferContext{}; 38 | ctx.referrer = referrer; 39 | 40 | // a blank pixel 41 | auto pxl = pixel{}; 42 | if (gen != 0) { 43 | pxl.owner = N(somebody); 44 | pxl.priceCounter = gen - 1; 45 | } else { 46 | pxl.owner = 0; 47 | pxl.priceCounter = 0; 48 | } 49 | pxl.color = 0xff0000ff; 50 | 51 | uint64_t purchasePrice = pxl.nextPrice(); 52 | uint64_t purchaseAmount = npixels * purchasePrice; 53 | tt.totalPaid += purchaseAmount; 54 | ctx.amountLeft = purchaseAmount; 55 | ctx.purchaser = player.owner; 56 | 57 | // buying a non-blank pixel (first take over) 58 | auto pxlOrder = st_pixelOrder{}; 59 | pxlOrder.color = 0x00ff00ff; 60 | pxlOrder.priceCounter = gen; 61 | 62 | // buy n blank pixels 63 | for (size_t i = 0; i < npixels; i++) { 64 | auto result = ctx.purchase(pxl, pxlOrder); 65 | 66 | tt.totalPaidToOwnerScaled += result.ownerEarningScaled; 67 | } 68 | 69 | ctx.updateFeesDistribution(); 70 | 71 | totalReferralPaidScaled = ctx.referralEarningScaled; 72 | 73 | ctx.updateCanvas(cnv); 74 | ctx.updatePurchaserAccount(player); 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /contracts/EOSPixels/types.hpp: -------------------------------------------------------------------------------- 1 | #ifndef _PIXELS_TYPES_HPP 2 | #define _PIXELS_TYPES_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "config.hpp" 10 | #include "eosio.hpp" 11 | 12 | typedef uint32_t eostime; 13 | // https://gcc.gnu.org/onlinedocs/gcc/_005f_005fint128.html 14 | typedef unsigned __int128 uint128_t; 15 | 16 | struct st_pixelOrder; 17 | struct st_transferContext; 18 | struct st_buyPixel_result; 19 | struct st_pixelLocation; 20 | 21 | struct pixel { 22 | uint32_t color; 23 | uint8_t priceCounter; 24 | account_name owner; 25 | 26 | inline bool isBlank() const { return owner == 0; } 27 | 28 | uint64_t currentPrice() const { 29 | if (isBlank()) { 30 | return 0; 31 | } 32 | 33 | return DEFAULT_PRICE * pow(PRICE_MULTIPLIER, priceCounter); 34 | } 35 | 36 | uint64_t nextPrice() const { 37 | if (isBlank()) { 38 | return DEFAULT_PRICE; 39 | } 40 | 41 | // Put a ceiling price on pixel. Prevents weird overflow. 42 | // FIXME: Unconfirmed bug. From the frontend integration test, it's possible 43 | // to bid up a pixel, and the total payout of the whole game is slightly 44 | // more. 45 | // 46 | // At 100+ EOS per pixel, the excess payout is ~0.006 EOS. But the team gets 47 | // paid ~14 EOS, so we can cover that excess. Attacker cannot make money 48 | // this way tho. 49 | // 50 | // Just to maintain sanity, we artificially limit the ceiling of a pixel 51 | // price to below 90 EOS. 52 | eosio_assert(priceCounter <= 25, "price for pixel is too high"); 53 | 54 | return currentPrice() * PRICE_MULTIPLIER; 55 | } 56 | 57 | uint8_t nextPriceCounter() const { 58 | if (isBlank()) { 59 | return 0; 60 | } 61 | 62 | auto c = priceCounter + 1; 63 | eosio_assert(c > 0, "price counter overflow"); 64 | 65 | return c; 66 | } 67 | }; 68 | 69 | //@abi table account i64 70 | struct account { 71 | account_name owner; 72 | 73 | // Including refunds, earnings, and referral fees (scaled) 74 | uint128_t balanceScaled; 75 | 76 | uint64_t pixelsDrawn; 77 | uint128_t maskScaled; 78 | 79 | uint64_t primary_key() const { return owner; } 80 | // asset eos_balance() const { 81 | // return asset(balance / PRECISION_BASE, EOS_SYMBOL); 82 | // } 83 | 84 | EOSLIB_SERIALIZE(account, (owner)(balanceScaled)(pixelsDrawn)(maskScaled)) 85 | }; 86 | 87 | // @abi table guard i64 88 | struct guard { 89 | uint64_t id; 90 | // Admin needs to refill this quota to enable more withdrwal 91 | uint64_t quota; 92 | 93 | uint64_t primary_key() const { return id; } 94 | 95 | EOSLIB_SERIALIZE(guard, (id)(quota)) 96 | }; 97 | 98 | // @abi table canvases i64 99 | struct canvas { 100 | uint64_t id; 101 | eostime lastPaintedAt; 102 | eostime duration; 103 | account_name lastPainter; 104 | uint64_t pixelsDrawn; 105 | 106 | uint128_t maskScaled; 107 | uint128_t potScaled; 108 | uint128_t teamScaled; 109 | 110 | uint64_t primary_key() const { return id; } 111 | 112 | bool isEnded() { return now() > lastPaintedAt + duration; } 113 | 114 | uint128_t patronBonusScaled(const account &player) const { 115 | return maskScaled * player.pixelsDrawn - player.maskScaled; 116 | } 117 | 118 | EOSLIB_SERIALIZE(canvas, (id)(lastPaintedAt)(duration)(lastPainter)( 119 | pixelsDrawn)(maskScaled)(potScaled)(teamScaled)) 120 | }; 121 | 122 | //@abi table pixels i64 123 | struct pixel_row { 124 | uint64_t row; 125 | std::vector pixels; 126 | 127 | uint64_t primary_key() const { return row; } 128 | 129 | void initialize_empty_pixels() { 130 | std::vector v(PIXELS_PER_ROW); 131 | pixels = v; 132 | } 133 | 134 | EOSLIB_SERIALIZE(pixel_row, (row)(pixels)) 135 | }; 136 | 137 | uint64_t calculateWithdrawalAndUpdate(const canvas &cnv, account &player, 138 | guard &grd) { 139 | int64_t patronBonus = cnv.patronBonusScaled(player) / PRECISION_BASE; 140 | 141 | int64_t balance = player.balanceScaled / PRECISION_BASE; 142 | 143 | int64_t withdrawAmount = patronBonus + balance; 144 | eosio_assert(withdrawAmount > 0, "Balance too small for withdrawal"); 145 | 146 | eosio_assert(grd.quota >= withdrawAmount, 147 | "Contract withdrawal quota exceeded"); 148 | 149 | grd.quota -= withdrawAmount; 150 | 151 | // Find the actual truncated values, and use those to update the records 152 | uint128_t withdrawnBonusScaled = patronBonus * PRECISION_BASE; 153 | uint128_t withdrawnBalanceScaled = balance * PRECISION_BASE; 154 | 155 | // Due to precision issues in PRECISION_BASE, withdrawnBalanceScaled may cause overflow. 156 | if (withdrawnBalanceScaled >= player.balanceScaled) { 157 | player.balanceScaled = 0; 158 | } else { 159 | player.balanceScaled -= withdrawnBalanceScaled; 160 | } 161 | player.maskScaled += withdrawnBonusScaled; 162 | 163 | return withdrawAmount; 164 | }; 165 | 166 | // The location of the pixel in table. 167 | struct st_pixelLocation { 168 | // index into the table 169 | uint16_t row; 170 | // index into the pixels array of the row 171 | uint16_t col; 172 | 173 | st_pixelLocation(uint32_t coordinate) { 174 | row = coordinate / PIXELS_PER_ROW; 175 | col = coordinate % PIXELS_PER_ROW; 176 | } 177 | }; 178 | 179 | struct st_pixelOrder { 180 | uint32_t coordinate; 181 | uint32_t color; 182 | uint8_t priceCounter; 183 | 184 | uint32_t x; 185 | uint32_t y; 186 | 187 | void parse(const std::string &memo) { 188 | uint64_t memoInt = stoull(memo, 0, 36); 189 | priceCounter = memoInt >> 52; 190 | 191 | coordinate = memoInt >> 32 & 0xFFFFF; 192 | color = memoInt & 0xFFFFFFFF; 193 | 194 | x = coordinate & 0x3ff; 195 | y = coordinate >> 10; 196 | 197 | eosio_assert(y < MAX_COORDINATE_Y_PLUS_ONE, "invalid y"); 198 | eosio_assert(x < MAX_COORDINATE_X_PLUS_ONE, "invalid x"); 199 | } 200 | 201 | st_pixelLocation location() const { return st_pixelLocation(coordinate); } 202 | }; 203 | 204 | struct st_buyPixel_result { 205 | // Whether the buy was skipped for some reason. 206 | bool isSkipped; 207 | // Whether the buy was a blank pixel 208 | bool isFirstBuyer; 209 | 210 | // Fee (scaled) paid to the contract. Going to community pot, patron bonus, 211 | // and team. 212 | uint128_t feeScaled; 213 | 214 | // Value (scaled) that goes to the previous pixel owner. 0, if pixel was 215 | // blank. 216 | uint128_t ownerEarningScaled; 217 | }; 218 | 219 | // Used to bookeep various money calculations 220 | struct st_transferContext { 221 | account_name purchaser; 222 | account_name referrer; 223 | 224 | // Fund available for purchasing 225 | uint64_t amountLeft; // 1 EOS = 10,000 226 | 227 | // Total fees collected (scaled) 228 | uint128_t totalFeesScaled; 229 | 230 | // How much is paid to the previous owners 231 | // uint128_t totalPaidToPreviousOwnersScaled; 232 | 233 | // How many pixels are painted 234 | uint64_t paintedPixelCount; 235 | 236 | uint128_t amountLeftScaled() { return amountLeft * PRECISION_BASE; } 237 | 238 | st_buyPixel_result purchase(const pixel &pixel, 239 | const st_pixelOrder &pixelOrder) { 240 | auto result = st_buyPixel_result(); 241 | 242 | auto isBlank = pixel.isBlank(); 243 | result.isFirstBuyer = isBlank; 244 | 245 | if (!isBlank && pixelOrder.color == pixel.color) { 246 | // Pixel already the same color. Skip. 247 | result.isSkipped = true; 248 | return result; 249 | } 250 | 251 | if (!isBlank && pixelOrder.priceCounter < pixel.nextPriceCounter()) { 252 | // Payment too low for this pixel, maybe price had increased (somebody 253 | // else drew first). Skip. 254 | result.isSkipped = true; 255 | return result; 256 | } 257 | 258 | uint64_t nextPrice = pixel.nextPrice(); 259 | eosio_assert(amountLeft >= nextPrice, "insufficient fund to buy pixel"); 260 | 261 | if (isBlank) { 262 | // buying blank. The fee is the entire buy price. 263 | result.feeScaled = nextPrice * PRECISION_BASE; 264 | } else { 265 | // buying from another player. The fee is a percentage of the gain. 266 | uint64_t currentPrice = pixel.currentPrice(); 267 | uint128_t priceIncreaseScaled = 268 | (nextPrice - currentPrice) * PRECISION_BASE; 269 | 270 | result.feeScaled = priceIncreaseScaled * FEE_PERCENTAGE / 100; 271 | result.ownerEarningScaled = nextPrice * PRECISION_BASE - result.feeScaled; 272 | } 273 | 274 | // bookkeeping for multiple purchases 275 | amountLeft -= nextPrice; 276 | paintedPixelCount++; 277 | totalFeesScaled += result.feeScaled; 278 | 279 | return result; 280 | } 281 | 282 | uint128_t patronBonusScaled; 283 | uint128_t potScaled; 284 | uint128_t teamScaled; 285 | uint128_t referralEarningScaled; 286 | 287 | bool hasReferrer() { return referrer != 0; } 288 | 289 | void updateFeesDistribution() { 290 | patronBonusScaled = totalFeesScaled * PATRON_BONUS_PERCENTAGE_POINTS / 100; 291 | 292 | potScaled = totalFeesScaled * POT_PERCENTAGE_POINTS / 100; 293 | 294 | referralEarningScaled = totalFeesScaled * REFERRER_PERCENTAGE_POINTS / 100; 295 | if (referrer == 0) { 296 | // if no referrer, pay the pot. 297 | potScaled += referralEarningScaled; 298 | referralEarningScaled = 0; 299 | } 300 | 301 | teamScaled = 302 | totalFeesScaled - patronBonusScaled - potScaled - referralEarningScaled; 303 | } 304 | 305 | uint128_t canvasMaskScaled; 306 | uint128_t bonusPerPixelScaled; 307 | 308 | void updateCanvas(canvas &cv) { 309 | cv.potScaled += potScaled; 310 | cv.teamScaled += teamScaled; 311 | cv.pixelsDrawn += paintedPixelCount; 312 | 313 | bonusPerPixelScaled = patronBonusScaled / cv.pixelsDrawn; 314 | eosio_assert(bonusPerPixelScaled > 0, "bonus is 0"); 315 | 316 | cv.maskScaled += bonusPerPixelScaled; 317 | eosio_assert(cv.maskScaled >= bonusPerPixelScaled, "canvas mask overflow"); 318 | 319 | canvasMaskScaled = cv.maskScaled; 320 | } 321 | 322 | void updatePurchaserAccount(account &acct) { 323 | if (amountLeft) { 324 | acct.balanceScaled += amountLeft * PRECISION_BASE; 325 | } 326 | 327 | uint128_t patronBonusScaled = bonusPerPixelScaled * paintedPixelCount; 328 | 329 | // FIXME: give the truncated to the team? 330 | 331 | uint128_t maskUpdate = 332 | canvasMaskScaled * paintedPixelCount - patronBonusScaled; 333 | acct.maskScaled += maskUpdate; 334 | 335 | eosio_assert(acct.maskScaled >= maskUpdate, "player mask overflow"); 336 | acct.pixelsDrawn += paintedPixelCount; 337 | } 338 | }; 339 | 340 | #endif 341 | -------------------------------------------------------------------------------- /contracts/README.md: -------------------------------------------------------------------------------- 1 | ## Setup 2 | 3 | Install project dependencies: 4 | 5 | ``` 6 | yarn 7 | ``` 8 | 9 | ### Local EOS test network extra steps 10 | 11 | #### Run nodeos in docker: 12 | 13 | ```bash 14 | # cd to path-to-repo/contracts frist 15 | yarn nodeos:docker 16 | 17 | # alias cleos 18 | alias cleos='docker exec -it eosio /opt/eosio/bin/cleos -u http://0.0.0.0:8888 --wallet-url http://0.0.0.0:8888' 19 | ``` 20 | 21 | #### Initialize wallet if not already 22 | 23 | ```sh 24 | cleos wallet create 25 | # Remember to save the password 26 | cleos wallet import --private-key 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 27 | ``` 28 | 29 | #### Create EOS token if not already 30 | 31 | Create eosio.token account: 32 | 33 | ``` 34 | cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV 35 | ``` 36 | 37 | Deploy eosio.token contract: 38 | 39 | ``` 40 | cleos set contract eosio.token contracts/eosio.token -p eosio.token@active 41 | ``` 42 | 43 | Create EOS token: 44 | 45 | ``` 46 | cleos push action eosio.token create '[ "eosio", "1000000000.0000 EOS"]' -p eosio.token 47 | ``` 48 | 49 | ## Compile & Deploy 50 | 51 | > Note: For [Kylin Testnet](https://www.cryptokylin.io/) or [Jungle Testnet](http://jungle.cryptolions.io/), you need to create the contract account on the corresponding website first, and then you also need to buy some ram using faucet supply tokens. 52 | 53 | Copy the `.env.template` to `.env` and add your private key there and edit your contract name and network node infomations. 54 | 55 | [Kylin Testnet](https://www.cryptokylin.io/) example configuration: 56 | 57 | ``` 58 | EOS_CONTRACT_NAME=myeospixels1 59 | CONTRACT_PRIVATE_KEY=5...... 60 | EOS_NETWORK_PROTOCOL=https 61 | EOS_NETWORK_HOST=api-kylin.eosasia.one 62 | EOS_NETWORK_PORT=443 63 | EOS_NETWORK_CHAINID=5fff1dae8dc8e2fc4d5b23b2c7665c97f9e9d8edf2b6485a86ba311c25639191 64 | ``` 65 | 66 | Compile and deploy contracts: 67 | 68 | ```sh 69 | # Local EOS test network 70 | yarn start:docker 71 | 72 | # yarn start 73 | ``` 74 | 75 | ## Scripts 76 | 77 | > Tip: for local eos testnet, you won't get error detail from api respond, use `docker logs --tail 10 -f eosio` to trace the chain logs. 78 | 79 | Clear docker data: 80 | 81 | ```sh 82 | # stop and remove container 83 | docker stop eosio 84 | 85 | # remove data 86 | yarn clear:docker 87 | ``` 88 | 89 | Update contract (recompile and deploy): 90 | 91 | ```sh 92 | # Local EOS test network 93 | yarn update-contract:docker 94 | 95 | # yarn update-contract 96 | ``` 97 | 98 | Create new canvas after current canvas is done: 99 | 100 | ``` 101 | yarn @end 102 | ``` 103 | 104 | Create new canvas automatically: 105 | 106 | ``` 107 | yarn @autoend 108 | ``` 109 | 110 | Withdraw using tester account: 111 | 112 | ```sh 113 | yarn @withdraw '0.0010 EOS' 114 | ``` 115 | 116 | Inspecting the contract's tables: 117 | 118 | ``` 119 | yarn dump-tables 120 | ``` 121 | -------------------------------------------------------------------------------- /contracts/actions/autoend.js: -------------------------------------------------------------------------------- 1 | const { eos } = require(`../config`) 2 | const { getErrorDetail } = require(`../utils`) 3 | 4 | const { EOS_CONTRACT_NAME } = process.env 5 | 6 | async function action() { 7 | try { 8 | const contract = await eos.contract(EOS_CONTRACT_NAME) 9 | await contract.end( 10 | { name: EOS_CONTRACT_NAME }, 11 | { authorization: EOS_CONTRACT_NAME }, 12 | ) 13 | console.log('New canvas created') 14 | } catch (error) { 15 | console.log(`FAILED`) 16 | console.error(`${getErrorDetail(error)}`) 17 | } 18 | } 19 | 20 | async function sleep(ms) { 21 | return new Promise((resolve) => { 22 | setTimeout(() => { 23 | resolve() 24 | }, ms) 25 | }) 26 | } 27 | 28 | // try end current round if possible 29 | ;(async () => { 30 | while (true) { 31 | await sleep(60 * 1000) 32 | await action() 33 | } 34 | })() 35 | -------------------------------------------------------------------------------- /contracts/actions/changedur.js: -------------------------------------------------------------------------------- 1 | const { eos } = require(`../config`) 2 | const { getErrorDetail } = require(`../utils`) 3 | 4 | const { EOS_CONTRACT_NAME, TESTER_NAME } = process.env 5 | 6 | async function action() { 7 | try { 8 | const contract = await eos.contract(EOS_CONTRACT_NAME) 9 | await contract.changedur(60 * 60 * 24, { 10 | authorization: TESTER_NAME, 11 | }) 12 | 13 | console.log(`SUCCESS`) 14 | } catch (error) { 15 | console.log(`FAILED`) 16 | console.error(`${getErrorDetail(error)}`) 17 | } 18 | } 19 | 20 | action() 21 | -------------------------------------------------------------------------------- /contracts/actions/clearAccts.js: -------------------------------------------------------------------------------- 1 | const { eos } = require(`../config`) 2 | const { getErrorDetail, sleep } = require(`../utils`) 3 | 4 | const { EOS_CONTRACT_NAME, TESTER_NAME } = process.env 5 | 6 | async function action() { 7 | let total = 100 8 | const perBatch = 100 9 | const perTx = 100 10 | const contract = await eos.contract(EOS_CONTRACT_NAME) 11 | while (total) { 12 | total -= perBatch 13 | let count = perBatch 14 | let nonce = 0 15 | const tx = [] 16 | while (count) { 17 | tx.push( 18 | contract 19 | .clearaccts(perTx, nonce, { 20 | authorization: TESTER_NAME, 21 | expireInSeconds: 15, 22 | }) 23 | .catch((err) => { 24 | console.error(`${getErrorDetail(err)}`) 25 | }), 26 | ) 27 | count -= perTx 28 | nonce++ 29 | } 30 | console.log('SENT') 31 | 32 | await Promise.all(tx) 33 | 34 | console.log(`DONE`) 35 | } 36 | console.log(`ALL DONE`) 37 | } 38 | 39 | action() 40 | -------------------------------------------------------------------------------- /contracts/actions/clearCanvases.js: -------------------------------------------------------------------------------- 1 | const { eos } = require(`../config`) 2 | const { getErrorDetail, sleep } = require(`../utils`) 3 | 4 | const { EOS_CONTRACT_NAME, TESTER_NAME } = process.env 5 | 6 | async function action() { 7 | let total = 1 8 | const perBatch = 1 9 | const perTx = 1 10 | const contract = await eos.contract(EOS_CONTRACT_NAME) 11 | while (total) { 12 | total -= perBatch 13 | let count = perBatch 14 | let nonce = 0 15 | const tx = [] 16 | while (count) { 17 | tx.push( 18 | contract 19 | .clearcanvs(perTx, nonce, { 20 | authorization: TESTER_NAME, 21 | expireInSeconds: 15, 22 | }) 23 | .catch((err) => { 24 | console.error(`${getErrorDetail(err)}`) 25 | }), 26 | ) 27 | count -= perTx 28 | nonce++ 29 | } 30 | console.log('SENT') 31 | 32 | await Promise.all(tx) 33 | 34 | console.log(`DONE`) 35 | } 36 | console.log(`ALL DONE`) 37 | } 38 | 39 | action() 40 | -------------------------------------------------------------------------------- /contracts/actions/clearPixels.js: -------------------------------------------------------------------------------- 1 | const { eos } = require(`../config`) 2 | const { getErrorDetail, sleep } = require(`../utils`) 3 | 4 | const { EOS_CONTRACT_NAME, TESTER_NAME } = process.env 5 | 6 | async function action() { 7 | let total = 100 8 | const perBatch = 100 9 | const perTx = 100 10 | const contract = await eos.contract(EOS_CONTRACT_NAME) 11 | while (total) { 12 | total -= perBatch 13 | let count = perBatch 14 | let nonce = 0 15 | const tx = [] 16 | while (count) { 17 | tx.push( 18 | contract 19 | .clearpixels(perTx, nonce, { 20 | authorization: TESTER_NAME, 21 | expireInSeconds: 15, 22 | }) 23 | .catch((err) => { 24 | console.error(`${getErrorDetail(err)}`) 25 | }), 26 | ) 27 | count -= perTx 28 | nonce++ 29 | } 30 | console.log('SENT') 31 | 32 | await Promise.all(tx) 33 | 34 | console.log(`DONE`) 35 | } 36 | console.log(`ALL DONE`) 37 | } 38 | 39 | action() 40 | -------------------------------------------------------------------------------- /contracts/actions/createAcct.js: -------------------------------------------------------------------------------- 1 | const { eos } = require(`../config`) 2 | const { getErrorDetail } = require(`../utils`) 3 | 4 | const { EOS_CONTRACT_NAME, TESTER_NAME } = process.env 5 | 6 | async function action() { 7 | try { 8 | const contract = await eos.contract(EOS_CONTRACT_NAME) 9 | await contract.createacct(TESTER_NAME, { 10 | authorization: TESTER_NAME, 11 | }) 12 | 13 | console.log(`SUCCESS`) 14 | } catch (error) { 15 | console.log(`FAILED`) 16 | console.error(`${getErrorDetail(error)}`) 17 | } 18 | } 19 | 20 | action() 21 | -------------------------------------------------------------------------------- /contracts/actions/createpxrs.js: -------------------------------------------------------------------------------- 1 | const { eos } = require(`../config`) 2 | const { getErrorDetail } = require(`../utils`) 3 | 4 | const { EOS_CONTRACT_NAME, TESTER_NAME } = process.env 5 | 6 | async function action() { 7 | try { 8 | const contract = await eos.contract(EOS_CONTRACT_NAME) 9 | await contract.createpxrs(0, 64, { 10 | authorization: TESTER_NAME, 11 | }) 12 | 13 | console.log(`SUCCESS`) 14 | } catch (error) { 15 | console.log(`FAILED`) 16 | console.error(`${getErrorDetail(error)}`) 17 | } 18 | } 19 | 20 | action() 21 | -------------------------------------------------------------------------------- /contracts/actions/end.js: -------------------------------------------------------------------------------- 1 | const { eos } = require(`../config`) 2 | const { getErrorDetail } = require(`../utils`) 3 | 4 | const { EOS_CONTRACT_NAME } = process.env 5 | 6 | async function action() { 7 | try { 8 | const contract = await eos.contract(EOS_CONTRACT_NAME) 9 | await contract.end( 10 | { name: EOS_CONTRACT_NAME }, 11 | { authorization: EOS_CONTRACT_NAME }, 12 | ) 13 | console.log(`SUCCESS`) 14 | } catch (error) { 15 | console.log(`FAILED`) 16 | console.error(`${getErrorDetail(error)}`) 17 | } 18 | } 19 | 20 | action() 21 | -------------------------------------------------------------------------------- /contracts/actions/init.js: -------------------------------------------------------------------------------- 1 | const { eos } = require(`../config`) 2 | const { getErrorDetail } = require(`../utils`) 3 | 4 | const { EOS_CONTRACT_NAME } = process.env 5 | 6 | async function action() { 7 | try { 8 | const contract = await eos.contract(EOS_CONTRACT_NAME) 9 | await contract.init( 10 | { name: EOS_CONTRACT_NAME }, 11 | { authorization: EOS_CONTRACT_NAME }, 12 | ) 13 | console.log(`SUCCESS`) 14 | } catch (error) { 15 | console.log(`FAILED`) 16 | console.error(`${getErrorDetail(error)}`) 17 | } 18 | } 19 | 20 | action() 21 | -------------------------------------------------------------------------------- /contracts/actions/refresh.js: -------------------------------------------------------------------------------- 1 | const { eos } = require(`../config`) 2 | const { getErrorDetail } = require(`../utils`) 3 | 4 | const { EOS_CONTRACT_NAME, TESTER_NAME } = process.env 5 | 6 | async function action() { 7 | try { 8 | const contract = await eos.contract(EOS_CONTRACT_NAME) 9 | await contract.refresh({ 10 | authorization: TESTER_NAME, 11 | }) 12 | 13 | console.log(`SUCCESS`) 14 | } catch (error) { 15 | console.log(`FAILED`) 16 | console.error(`${getErrorDetail(error)}`) 17 | } 18 | } 19 | 20 | action() 21 | -------------------------------------------------------------------------------- /contracts/actions/resetquota.js: -------------------------------------------------------------------------------- 1 | const { eos } = require(`../config`) 2 | const { getErrorDetail } = require(`../utils`) 3 | 4 | const { EOS_CONTRACT_NAME, TESTER_NAME } = process.env 5 | 6 | async function action() { 7 | try { 8 | const contract = await eos.contract(EOS_CONTRACT_NAME) 9 | await contract.resetquota({ 10 | authorization: TESTER_NAME, 11 | }) 12 | 13 | console.log(`SUCCESS`) 14 | } catch (error) { 15 | console.log(`FAILED`) 16 | console.error(`${getErrorDetail(error)}`) 17 | } 18 | } 19 | 20 | action() 21 | -------------------------------------------------------------------------------- /contracts/actions/withdraw.js: -------------------------------------------------------------------------------- 1 | const { eos } = require(`../config`) 2 | const { getErrorDetail } = require(`../utils`) 3 | 4 | const { EOS_CONTRACT_NAME, TESTER_NAME } = process.env 5 | 6 | async function action() { 7 | try { 8 | const contract = await eos.contract(EOS_CONTRACT_NAME) 9 | await contract.withdraw(TESTER_NAME, process.argv[2], { 10 | authorization: TESTER_NAME, 11 | }) 12 | 13 | console.log(`SUCCESS`) 14 | } catch (error) { 15 | console.log(`FAILED`) 16 | console.error(`${getErrorDetail(error)}`) 17 | } 18 | } 19 | 20 | action() 21 | -------------------------------------------------------------------------------- /contracts/config.js: -------------------------------------------------------------------------------- 1 | const Eos = require('eosjs') 2 | const { ecc } = Eos.modules 3 | const binaryen = require(`binaryen`) 4 | const dotenv = require(`dotenv`) 5 | 6 | dotenv.config({ 7 | path: process.env.NODE_ENV === `production` ? `.env.production` : `.env`, 8 | }) 9 | // used in dev only 10 | const eosioPrivateKey = `5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3` 11 | 12 | const contractPrivate = process.env.CONTRACT_PRIVATE_KEY 13 | const contractPublicKey = ecc.privateToPublic(contractPrivate) 14 | 15 | const testerPrivate = process.env.TESTER_PRIVATE_KEY 16 | const testerPublicKey = ecc.privateToPublic(testerPrivate) 17 | 18 | const keyProvider = [eosioPrivateKey, contractPrivate, testerPrivate] 19 | const logger = { error: null } 20 | 21 | const httpProtocol = process.env.EOS_NETWORK_PROTOCOL 22 | const host = process.env.EOS_NETWORK_HOST 23 | const port = process.env.EOS_NETWORK_PORT 24 | const chainId = process.env.EOS_NETWORK_CHAINID 25 | 26 | const eos = Eos({ 27 | keyProvider, 28 | binaryen, 29 | logger, 30 | httpEndpoint: `${httpProtocol}://${host}:${port}`, 31 | chainId, 32 | // uncomment next line for debugging 33 | // verbose: true, 34 | }) 35 | 36 | module.exports = { 37 | eos, 38 | contractPublicKey, 39 | testerPublicKey, 40 | } 41 | -------------------------------------------------------------------------------- /contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contracts", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "scripts": { 7 | "nodeos": "nodeos -e -p eosio --plugin eosio::wallet_api_plugin --plugin eosio::chain_api_plugin --plugin eosio::history_api_plugin --contracts-console --access-control-allow-origin=*", 8 | "nodeos:docker": "docker run -d --rm --name eosio -p 8888:8888 -p 9876:9876 -v $(pwd)/EOSPixels:/EOSPixels -v /tmp/work:/work -v /tmp/eosio/data:/mnt/dev/data -v /tmp/eosio/config:/mnt/dev/config eosio/eos-dev@sha256:8f1841f71332109d8281cbd4197751933f2bf4fb5e77db189908bb77782b19df /bin/bash -c 'nodeos -e -p eosio --plugin eosio::wallet_api_plugin --plugin eosio::wallet_plugin --plugin eosio::producer_plugin --plugin eosio::history_plugin --plugin eosio::chain_api_plugin --plugin eosio::history_api_plugin --plugin eosio::http_plugin -d /mnt/dev/data --config-dir /mnt/dev/config --http-server-address=0.0.0.0:8888 --access-control-allow-origin=* --contracts-console --http-validate-host=false'", 9 | "initialize": "node scripts/init.js", 10 | "compile": "eosiocpp -o EOSPixels/EOSPixels.wast EOSPixels/EOSPixels.cpp && eosiocpp -g EOSPixels/EOSPixels.abi EOSPixels/EOSPixels.cpp", 11 | "compile:docker": "docker exec -it eosio /bin/bash -c 'eosiocpp -o EOSPixels/EOSPixels.wast EOSPixels/EOSPixels.cpp && eosiocpp -g EOSPixels/EOSPixels.abi EOSPixels/EOSPixels.cpp'", 12 | "deploy": "node scripts/deploy.js", 13 | "start": "npm run initialize && npm run compile && npm run deploy && npm run @init", 14 | "start:docker": "npm run initialize && npm run compile:docker && npm run deploy && npm run @init", 15 | "update-contract": "npm run compile && npm run deploy", 16 | "update-contract:docker": "npm run compile:docker && npm run deploy", 17 | "dump-tables": "node scripts/dump_tables.js", 18 | "update-auth": "node scripts/update_auth.js", 19 | "buy-ram": "node scripts/buy_ram.js", 20 | "@init": "node actions/init.js", 21 | "@end": "node actions/end.js", 22 | "@autoend": "node actions/autoend.js", 23 | "@withdraw": "node actions/withdraw.js", 24 | "clear:docker": "rm -rf /tmp/work && rm -rf /tmp/eosio" 25 | }, 26 | "license": "MIT", 27 | "dependencies": { 28 | "binaryen": "^48.0.0", 29 | "dotenv": "^6.0.0", 30 | "eosjs": "^16.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /contracts/scripts/_update_auth.js: -------------------------------------------------------------------------------- 1 | const { eos, contractPublicKey } = require(`../config`) 2 | 3 | const { EOS_CONTRACT_NAME } = process.env 4 | 5 | async function updateAuth() { 6 | // inline action required 7 | const auth = { 8 | threshold: 1, 9 | accounts: [ 10 | { 11 | permission: { actor: EOS_CONTRACT_NAME, permission: 'eosio.code' }, 12 | weight: 1, 13 | }, 14 | ], 15 | } 16 | 17 | try { 18 | console.log('Updating contract eosio.code permissions') 19 | await eos.updateauth({ 20 | account: EOS_CONTRACT_NAME, 21 | permission: 'active', 22 | parent: 'owner', 23 | auth: Object.assign({}, auth, { 24 | keys: [{ key: contractPublicKey, weight: 1 }], 25 | }), 26 | }) 27 | console.log(`Updated`) 28 | } catch (err) { 29 | console.error(`Cannot update contract permission`, err) 30 | process.exit(1) 31 | } 32 | } 33 | 34 | module.exports = { 35 | updateAuth, 36 | } 37 | -------------------------------------------------------------------------------- /contracts/scripts/buy_ram.js: -------------------------------------------------------------------------------- 1 | const { eos } = require(`../config`) 2 | const { getErrorDetail } = require(`../utils`) 3 | 4 | const { EOS_CONTRACT_NAME } = process.env 5 | 6 | async function buyRam() { 7 | try { 8 | await eos.transaction((tr) => { 9 | tr.buyrambytes({ 10 | payer: EOS_CONTRACT_NAME, 11 | receiver: EOS_CONTRACT_NAME, 12 | bytes: 4096, 13 | }) 14 | 15 | // tr.delegatebw({ 16 | // from: EOS_CONTRACT_NAME, 17 | // receiver: EOS_CONTRACT_NAME, 18 | // stake_net_quantity: `1.0000 EOS`, 19 | // stake_cpu_quantity: `1.0000 EOS`, 20 | // transfer: 0, 21 | // }) 22 | }) 23 | } catch (error) { 24 | console.error(`${getErrorDetail(error)}`) 25 | } 26 | } 27 | 28 | buyRam() 29 | -------------------------------------------------------------------------------- /contracts/scripts/deploy.js: -------------------------------------------------------------------------------- 1 | const fs = require(`fs`) 2 | const path = require(`path`) 3 | const { eos } = require(`../config`) 4 | 5 | const { EOS_CONTRACT_NAME } = process.env 6 | 7 | const contractDir = `./EOSPixels` 8 | const wasm = fs.readFileSync(path.join(contractDir, `EOSPixels.wasm`)) 9 | const abi = fs.readFileSync(path.join(contractDir, `EOSPixels.abi`)) 10 | 11 | // Publish contract to the blockchain 12 | const codePromise = eos.setcode(EOS_CONTRACT_NAME, 0, 0, wasm) 13 | const abiPromise = eos.setabi(EOS_CONTRACT_NAME, JSON.parse(abi)) 14 | 15 | Promise.all([codePromise, abiPromise]) 16 | .then(() => { 17 | console.log(`Successfully delpoyed`) 18 | }) 19 | .catch(console.log) 20 | -------------------------------------------------------------------------------- /contracts/scripts/dump_tables.js: -------------------------------------------------------------------------------- 1 | const { eos } = require(`../config`) 2 | const { getErrorDetail } = require(`../utils`) 3 | 4 | const { EOS_CONTRACT_NAME } = process.env 5 | const ROWS_LIMIT = 99999 6 | 7 | async function script() { 8 | try { 9 | const [canvases, accounts] = await Promise.all([ 10 | eos.getTableRows({ 11 | json: true, 12 | code: EOS_CONTRACT_NAME, 13 | scope: EOS_CONTRACT_NAME, 14 | table: `canvases`, 15 | lower_bound: 0, 16 | upper_bound: -1, 17 | limit: ROWS_LIMIT, 18 | }), 19 | eos.getTableRows({ 20 | json: true, 21 | code: EOS_CONTRACT_NAME, 22 | scope: EOS_CONTRACT_NAME, 23 | table: `account`, 24 | lower_bound: 0, 25 | upper_bound: -1, 26 | limit: ROWS_LIMIT, 27 | }), 28 | ]) 29 | console.log('Canvases:') 30 | canvases.rows.forEach((row) => console.log(JSON.stringify(row, null, 2))) 31 | console.log('\nAccounts:') 32 | accounts.rows.forEach((row) => console.log(JSON.stringify(row, null, 2))) 33 | console.log('\nPixels:') 34 | const canvas = canvases.rows[0] 35 | if (canvas) { 36 | const pixels = await eos.getTableRows({ 37 | json: true, 38 | code: EOS_CONTRACT_NAME, 39 | scope: `${canvas.id}`, 40 | table: `pixels`, 41 | lower_bound: 0, 42 | upper_bound: -1, 43 | limit: ROWS_LIMIT, 44 | }) 45 | pixels.rows.forEach((row) => console.log(JSON.stringify(row, null, 2))) 46 | } 47 | } catch (error) { 48 | console.error(`${getErrorDetail(error)}`) 49 | } 50 | } 51 | 52 | script() 53 | -------------------------------------------------------------------------------- /contracts/scripts/init.js: -------------------------------------------------------------------------------- 1 | const { eos, contractPublicKey, testerPublicKey } = require(`../config`) 2 | const { getErrorDetail } = require(`../utils`) 3 | const { updateAuth } = require(`./_update_auth`) 4 | 5 | const { EOS_CONTRACT_NAME, TESTER_NAME } = process.env 6 | 7 | async function createAccount(name, publicKey) { 8 | try { 9 | await eos.getAccount(name) 10 | console.log(`"${name}" already exists: ${publicKey}`) 11 | 12 | return 13 | // no error => account already exists 14 | } catch (e) { 15 | // error => account does not exist yet 16 | } 17 | console.log(`Creating "${name}" ...`) 18 | await eos.transaction((tr) => { 19 | tr.newaccount({ 20 | creator: `eosio`, 21 | name, 22 | owner: publicKey, 23 | active: publicKey, 24 | }) 25 | 26 | // tr.buyrambytes({ 27 | // payer: name, 28 | // receiver: name, 29 | // bytes: 8192, 30 | // }) 31 | 32 | // tr.delegatebw({ 33 | // from: name, 34 | // receiver: name, 35 | // stake_net_quantity: `10.0000 EOS`, 36 | // stake_cpu_quantity: `10.0000 EOS`, 37 | // transfer: 0, 38 | // }) 39 | }) 40 | 41 | await eos.issue( 42 | { 43 | to: name, 44 | quantity: `1000.0000 EOS`, 45 | memo: `Happy spending`, 46 | }, 47 | { authorization: 'eosio' }, 48 | ) 49 | console.log(`Created`) 50 | } 51 | 52 | async function init() { 53 | try { 54 | await createAccount(EOS_CONTRACT_NAME, contractPublicKey) 55 | await createAccount(TESTER_NAME, testerPublicKey) 56 | } catch (error) { 57 | console.error( 58 | `Cannot create account ${EOS_CONTRACT_NAME} "${getErrorDetail(error)}"`, 59 | ) 60 | process.exit(1) 61 | } 62 | 63 | await updateAuth() 64 | } 65 | 66 | init() 67 | -------------------------------------------------------------------------------- /contracts/scripts/update_auth.js: -------------------------------------------------------------------------------- 1 | const { updateAuth } = require(`./_update_auth`) 2 | 3 | updateAuth() 4 | -------------------------------------------------------------------------------- /contracts/utils.js: -------------------------------------------------------------------------------- 1 | function getErrorDetail(error) { 2 | try { 3 | const json = 4 | typeof error === 'string' ? JSON.parse(error) : JSON.parse(error.message) 5 | return json.error.details[0].message 6 | } catch (err) { 7 | return error.message 8 | } 9 | } 10 | 11 | function sleep(ms) { 12 | return new Promise((resolve) => { 13 | setTimeout(() => { 14 | resolve() 15 | }, ms) 16 | }) 17 | } 18 | 19 | module.exports = { 20 | getErrorDetail, 21 | sleep, 22 | } 23 | -------------------------------------------------------------------------------- /contracts/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | babel-runtime@^6.26.0: 6 | version "6.26.0" 7 | resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" 8 | dependencies: 9 | core-js "^2.4.0" 10 | regenerator-runtime "^0.11.0" 11 | 12 | base-x@^3.0.2: 13 | version "3.0.4" 14 | resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.4.tgz#94c1788736da065edb1d68808869e357c977fa77" 15 | dependencies: 16 | safe-buffer "^5.0.1" 17 | 18 | bigi@^1.1.0, bigi@^1.4.2: 19 | version "1.4.2" 20 | resolved "https://registry.yarnpkg.com/bigi/-/bigi-1.4.2.tgz#9c665a95f88b8b08fc05cfd731f561859d725825" 21 | 22 | binaryen@^37.0.0: 23 | version "37.0.0" 24 | resolved "https://registry.yarnpkg.com/binaryen/-/binaryen-37.0.0.tgz#c392bc784b6d46da8fc918b1a4ed8257a0cd8b08" 25 | 26 | binaryen@^48.0.0: 27 | version "48.0.0" 28 | resolved "https://registry.yarnpkg.com/binaryen/-/binaryen-48.0.0.tgz#40eeff8ba0393751271dfa217987114dd65b9b59" 29 | 30 | bn.js@^4.11.8: 31 | version "4.11.8" 32 | resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" 33 | 34 | browserify-aes@^1.0.6: 35 | version "1.2.0" 36 | resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" 37 | dependencies: 38 | buffer-xor "^1.0.3" 39 | cipher-base "^1.0.0" 40 | create-hash "^1.1.0" 41 | evp_bytestokey "^1.0.3" 42 | inherits "^2.0.1" 43 | safe-buffer "^5.0.1" 44 | 45 | bs58@^4.0.1: 46 | version "4.0.1" 47 | resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" 48 | dependencies: 49 | base-x "^3.0.2" 50 | 51 | buffer-xor@^1.0.3: 52 | version "1.0.3" 53 | resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" 54 | 55 | bytebuffer@^5.0.1: 56 | version "5.0.1" 57 | resolved "https://registry.yarnpkg.com/bytebuffer/-/bytebuffer-5.0.1.tgz#582eea4b1a873b6d020a48d58df85f0bba6cfddd" 58 | dependencies: 59 | long "~3" 60 | 61 | camel-case@^3.0.0: 62 | version "3.0.0" 63 | resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" 64 | dependencies: 65 | no-case "^2.2.0" 66 | upper-case "^1.1.1" 67 | 68 | cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: 69 | version "1.0.4" 70 | resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" 71 | dependencies: 72 | inherits "^2.0.1" 73 | safe-buffer "^5.0.1" 74 | 75 | core-js@^2.4.0: 76 | version "2.5.7" 77 | resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" 78 | 79 | create-hash@^1.1.0, create-hash@^1.1.3: 80 | version "1.2.0" 81 | resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" 82 | dependencies: 83 | cipher-base "^1.0.1" 84 | inherits "^2.0.1" 85 | md5.js "^1.3.4" 86 | ripemd160 "^2.0.1" 87 | sha.js "^2.4.0" 88 | 89 | create-hmac@^1.1.6: 90 | version "1.1.7" 91 | resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" 92 | dependencies: 93 | cipher-base "^1.0.3" 94 | create-hash "^1.1.0" 95 | inherits "^2.0.1" 96 | ripemd160 "^2.0.0" 97 | safe-buffer "^5.0.1" 98 | sha.js "^2.4.8" 99 | 100 | dotenv@^6.0.0: 101 | version "6.0.0" 102 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.0.0.tgz#24e37c041741c5f4b25324958ebbc34bca965935" 103 | 104 | ecurve@^1.0.5: 105 | version "1.0.6" 106 | resolved "https://registry.yarnpkg.com/ecurve/-/ecurve-1.0.6.tgz#dfdabbb7149f8d8b78816be5a7d5b83fcf6de797" 107 | dependencies: 108 | bigi "^1.1.0" 109 | safe-buffer "^5.0.1" 110 | 111 | encoding@^0.1.11: 112 | version "0.1.12" 113 | resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" 114 | dependencies: 115 | iconv-lite "~0.4.13" 116 | 117 | eosjs-api@7.0.0: 118 | version "7.0.0" 119 | resolved "https://registry.yarnpkg.com/eosjs-api/-/eosjs-api-7.0.0.tgz#4cfc4fef7f25e2c77f8d137bb7b9c75505a272f4" 120 | dependencies: 121 | camel-case "^3.0.0" 122 | isomorphic-fetch "^2.2.1" 123 | 124 | eosjs-ecc@4.0.2: 125 | version "4.0.2" 126 | resolved "https://registry.yarnpkg.com/eosjs-ecc/-/eosjs-ecc-4.0.2.tgz#89b2fc019b2632ef294b94d4f34dbd540da9763c" 127 | dependencies: 128 | bigi "^1.4.2" 129 | browserify-aes "^1.0.6" 130 | bs58 "^4.0.1" 131 | bytebuffer "^5.0.1" 132 | create-hash "^1.1.3" 133 | create-hmac "^1.1.6" 134 | ecurve "^1.0.5" 135 | randombytes "^2.0.5" 136 | 137 | eosjs@^16.0.0: 138 | version "16.0.0" 139 | resolved "https://registry.yarnpkg.com/eosjs/-/eosjs-16.0.0.tgz#d88fd6bd8408882bd0d8a4bc5abe22827a8c9ecb" 140 | dependencies: 141 | babel-runtime "^6.26.0" 142 | binaryen "^37.0.0" 143 | create-hash "^1.1.3" 144 | eosjs-api "7.0.0" 145 | eosjs-ecc "4.0.2" 146 | fcbuffer "2.2.0" 147 | 148 | evp_bytestokey@^1.0.3: 149 | version "1.0.3" 150 | resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" 151 | dependencies: 152 | md5.js "^1.3.4" 153 | safe-buffer "^5.1.1" 154 | 155 | fcbuffer@2.2.0: 156 | version "2.2.0" 157 | resolved "https://registry.yarnpkg.com/fcbuffer/-/fcbuffer-2.2.0.tgz#c85d01485f74bb917d87689a55890dba70fc1de3" 158 | dependencies: 159 | bn.js "^4.11.8" 160 | bytebuffer "^5.0.1" 161 | ieee-float "^0.6.0" 162 | 163 | hash-base@^3.0.0: 164 | version "3.0.4" 165 | resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" 166 | dependencies: 167 | inherits "^2.0.1" 168 | safe-buffer "^5.0.1" 169 | 170 | iconv-lite@~0.4.13: 171 | version "0.4.23" 172 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" 173 | dependencies: 174 | safer-buffer ">= 2.1.2 < 3" 175 | 176 | ieee-float@^0.6.0: 177 | version "0.6.0" 178 | resolved "https://registry.yarnpkg.com/ieee-float/-/ieee-float-0.6.0.tgz#a68a856ba1ef511e7fa0e7e7e155c3a63642a55d" 179 | 180 | inherits@^2.0.1: 181 | version "2.0.3" 182 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 183 | 184 | is-stream@^1.0.1: 185 | version "1.1.0" 186 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 187 | 188 | isomorphic-fetch@^2.2.1: 189 | version "2.2.1" 190 | resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" 191 | dependencies: 192 | node-fetch "^1.0.1" 193 | whatwg-fetch ">=0.10.0" 194 | 195 | long@~3: 196 | version "3.2.0" 197 | resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" 198 | 199 | lower-case@^1.1.1: 200 | version "1.1.4" 201 | resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" 202 | 203 | md5.js@^1.3.4: 204 | version "1.3.4" 205 | resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" 206 | dependencies: 207 | hash-base "^3.0.0" 208 | inherits "^2.0.1" 209 | 210 | no-case@^2.2.0: 211 | version "2.3.2" 212 | resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" 213 | dependencies: 214 | lower-case "^1.1.1" 215 | 216 | node-fetch@^1.0.1: 217 | version "1.7.3" 218 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" 219 | dependencies: 220 | encoding "^0.1.11" 221 | is-stream "^1.0.1" 222 | 223 | randombytes@^2.0.5: 224 | version "2.0.6" 225 | resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" 226 | dependencies: 227 | safe-buffer "^5.1.0" 228 | 229 | regenerator-runtime@^0.11.0: 230 | version "0.11.1" 231 | resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" 232 | 233 | ripemd160@^2.0.0, ripemd160@^2.0.1: 234 | version "2.0.2" 235 | resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" 236 | dependencies: 237 | hash-base "^3.0.0" 238 | inherits "^2.0.1" 239 | 240 | safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1: 241 | version "5.1.2" 242 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 243 | 244 | "safer-buffer@>= 2.1.2 < 3": 245 | version "2.1.2" 246 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 247 | 248 | sha.js@^2.4.0, sha.js@^2.4.8: 249 | version "2.4.11" 250 | resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" 251 | dependencies: 252 | inherits "^2.0.1" 253 | safe-buffer "^5.0.1" 254 | 255 | upper-case@^1.1.1: 256 | version "1.1.3" 257 | resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" 258 | 259 | whatwg-fetch@>=0.10.0: 260 | version "2.0.4" 261 | resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" 262 | --------------------------------------------------------------------------------