├── .cargo └── config ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── build.sh ├── dao ├── README.md ├── dao_deploy_mainnet.sh ├── dao_deploy_testnet.sh ├── dao_proposals_mainnet.sh └── dao_proposals_testnet.sh ├── examples ├── airdrop │ ├── .cargo │ │ └── config │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── charity │ ├── .cargo │ │ └── config │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── counter │ ├── .cargo │ │ └── config │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── cross-contract │ ├── .cargo │ │ └── config │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── views │ ├── .cargo │ └── config │ ├── Cargo.toml │ └── src │ └── lib.rs ├── manager ├── .cargo │ └── config ├── Cargo.toml ├── src │ ├── agent.rs │ ├── lib.rs │ ├── owner.rs │ ├── storage_impl.rs │ ├── tasks.rs │ ├── triggers.rs │ ├── utils.rs │ └── views.rs └── tests │ ├── sim │ ├── main.rs │ └── test_utils.rs │ └── sputnik │ └── sputnikdao2.wasm ├── rewards ├── .cargo │ └── config ├── Cargo.toml └── src │ └── lib.rs ├── rust-toolchain ├── scripts ├── airdrop_bootstrap.sh ├── clear_all.sh ├── create_and_deploy.sh ├── guildnet_deploy.sh ├── mainnet_deploy.sh ├── owner_commands.sh ├── rewards_bootstrap.sh ├── simple_bootstrap.sh ├── testnet_deploy.sh └── triggers_bootstrap.sh └── test.sh /.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-C", "link-args=-s"] -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: push 3 | jobs: 4 | tests: 5 | strategy: 6 | matrix: 7 | platform: [macos-latest] 8 | runs-on: ${{ matrix.platform }} 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | toolchain: stable 14 | target: wasm32-unknown-unknown 15 | - name: Build 16 | env: 17 | IS_GITHUB_ACTION: true 18 | run: cargo +stable build --workspace --target wasm32-unknown-unknown --release 19 | - name: Run tests 20 | env: 21 | IS_GITHUB_ACTION: true 22 | run: cargo test --workspace -- --nocapture -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | target 4 | **/target/ 5 | **/pkg/ 6 | res 7 | 8 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 9 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 10 | Cargo.lock 11 | 12 | # Key pairs generated by the NEAR shell 13 | neardev 14 | 15 | # These are backup files generated by rustfmt 16 | **/*.rs.bk 17 | 18 | .idea 19 | dump.rdb -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | # include a member for each contract 3 | members = [ 4 | "manager", 5 | "rewards", 6 | "examples/airdrop", 7 | "examples/counter", 8 | "examples/charity", 9 | "examples/cross-contract", 10 | "examples/views" 11 | ] 12 | 13 | [profile.release] 14 | codegen-units = 1 15 | # Tell `rustc` to optimize for small code size. 16 | opt-level = "z" 17 | lto = true 18 | debug = true 19 | panic = "abort" 20 | # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 21 | overflow-checks = true 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Cron-Near 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 |
2 |

3 | Cron.near Contracts 4 |

5 |

6 | But i really really wanted to name this repo "crontracts" 7 |

8 |
9 | 10 | ## Building 11 | Run: 12 | ```bash 13 | ./build.sh 14 | ``` 15 | 16 | ## Testing 17 | To test run: 18 | ```bash 19 | cargo test --package manager -- --nocapture 20 | ``` 21 | 22 | ## Scripts 23 | The following scripts automate a lot of the tedious setup for contracts, and allow for quick deployments and setup. These are scripted versions of the example commands below. 24 | 25 | NOTE: See each script to change the main `NEAR_ACCT` to configure to an account you have testnet keys. 26 | 27 | Run: 28 | ```bash 29 | ./scripts/clear_all.sh 30 | ./scripts/create_and_deploy.sh 31 | ./scripts/simple_bootstrap.sh 32 | ``` 33 | 34 | Power user: 35 | ```bash 36 | export NEAR_ACCT=YOU.testnet 37 | ./scripts/clear_all.sh && ./scripts/create_and_deploy.sh && ./scripts/simple_bootstrap.sh 38 | ``` 39 | 40 | ## Create testnet subaccounts 41 | Next, create a NEAR testnet account with [Wallet](https://wallet.testnet.near.org). 42 | 43 | Set an environment variable to use in these examples. For instance, if your test account is `you.testnet` set it like so: 44 | 45 | ```bash 46 | export NEAR_ACCT=you.testnet 47 | ``` 48 | 49 | (**Windows users**: please look into using `set` instead of `export`, surrounding the environment variable in `%` instead of beginning with `$`, and using escaped double-quotes `\"` where necessary instead of the single-quotes provided in these instructions.) 50 | 51 | Create subaccounts: 52 | 53 | ```bash 54 | near create-account cron.$NEAR_ACCT --masterAccount $NEAR_ACCT 55 | near create-account counter.$NEAR_ACCT --masterAccount $NEAR_ACCT 56 | near create-account agent.$NEAR_ACCT --masterAccount $NEAR_ACCT 57 | near create-account user.$NEAR_ACCT --masterAccount $NEAR_ACCT 58 | near create-account crud.$NEAR_ACCT --masterAccount $NEAR_ACCT 59 | ``` 60 | 61 | **Note**: if changes are made to the contract and it needs to be redeployed, it's a good idea to delete and recreate the subaccount like so: 62 | 63 | ```bash 64 | near delete cron.$NEAR_ACCT $NEAR_ACCT && near create-account cron.$NEAR_ACCT --masterAccount $NEAR_ACCT 65 | near delete agent.$NEAR_ACCT $NEAR_ACCT && near create-account agent.$NEAR_ACCT --masterAccount $NEAR_ACCT 66 | near delete crud.$NEAR_ACCT $NEAR_ACCT && near create-account crud.$NEAR_ACCT --masterAccount $NEAR_ACCT 67 | ``` 68 | 69 | ## Contract Interaction 70 | 71 | ``` 72 | # Deploy New 73 | near deploy --wasmFile ./res/manager.wasm --accountId cron.$NEAR_ACCT --initFunction new --initArgs '{}' 74 | near deploy --wasmFile ./res/rust_counter_tutorial.wasm --accountId counter.$NEAR_ACCT 75 | near deploy --wasmFile ./res/cross_contract.wasm --accountId crud.$NEAR_ACCT --initFunction new --initArgs '{"cron": "cron.in.testnet"}' 76 | 77 | # Deploy Migration 78 | near deploy --wasmFile ./res/manager.wasm --accountId cron.$NEAR_ACCT --initFunction migrate_state --initArgs '{}' 79 | 80 | # Schedule "ticks" that help provide in-contract BPS calculation 81 | near call cron.$NEAR_ACCT create_task '{"contract_id": "cron.'$NEAR_ACCT'","function_id": "tick","cadence": "0 0 * * * *","recurring": true,"deposit": "0","gas": 2400000000000}' --accountId cron.$NEAR_ACCT --amount 10 82 | 83 | # Tasks 84 | near call cron.$NEAR_ACCT create_task '{"contract_id": "counter.'$NEAR_ACCT'","function_id": "increment","cadence": "0 */5 * * * *","recurring": true,"deposit": "0","gas": 2400000000000}' --accountId counter.$NEAR_ACCT --amount 10 85 | 86 | near view cron.$NEAR_ACCT get_task '{"task_hash": "r2JvrGPvDkFUuqdF4x1+L93aYKGmgp4GqXT4UAK3AE4="}' 87 | 88 | near call cron.$NEAR_ACCT remove_task '{"task_hash": "r2JvrGPvDkFUuqdF4x1+L93aYKGmgp4GqXT4UAK3AE4="}' --accountId counter.$NEAR_ACCT 89 | 90 | near view cron.$NEAR_ACCT get_tasks '{"offset": 999}' 91 | 92 | near call cron.$NEAR_ACCT proxy_call --accountId agent.$NEAR_ACCT 93 | 94 | near view cron.$NEAR_ACCT get_all_tasks 95 | 96 | # Agents 97 | near call cron.$NEAR_ACCT register_agent '{"payable_account_id": "user.'$NEAR_ACCT'"}' --accountId agent.$NEAR_ACCT 98 | 99 | near call cron.$NEAR_ACCT update_agent '{"payable_account_id": "user.'$NEAR_ACCT'"}' --accountId agent.$NEAR_ACCT 100 | 101 | near call cron.$NEAR_ACCT unregister_agent --accountId agent.$NEAR_ACCT --amount 0.000000000000000000000001 102 | 103 | near view cron.$NEAR_ACCT get_agent '{"pk": "ed25519:AGENT_PUBLIC_KEY"}' 104 | 105 | near call cron.$NEAR_ACCT withdraw_task_balance --accountId agent.$NEAR_ACCT 106 | 107 | # ------------------------------------ 108 | # Counter Interaction 109 | near view counter.$NEAR_ACCT get_num 110 | near call counter.$NEAR_ACCT increment --accountId $NEAR_ACCT 111 | near call counter.$NEAR_ACCT decrement --accountId $NEAR_ACCT 112 | 113 | # ------------------------------------ 114 | # Cross-Contract Interaction 115 | near view crud.$NEAR_ACCT get_series 116 | near view crud.$NEAR_ACCT stats 117 | near call crud.$NEAR_ACCT tick --accountId $NEAR_ACCT 118 | near call crud.$NEAR_ACCT schedule '{ "function_id": "tick", "period": "0 */5 * * * *" }' --accountId crud.$NEAR_ACCT --gas 300000000000000 --amount 5 119 | near call crud.$NEAR_ACCT update '{ "period": "0 0 */1 * * *" }' --accountId crud.$NEAR_ACCT --gas 300000000000000 --amount 5 120 | near call crud.$NEAR_ACCT remove --accountId crud.$NEAR_ACCT 121 | near call crud.$NEAR_ACCT status --accountId crud.$NEAR_ACCT 122 | ``` 123 | 124 | ## Changelog 125 | 126 | ### `0.4.7` 127 | 128 | Adjust old slot agent coverage, max gas assertion on task create 129 | 130 | ### `0.4.6` 131 | 132 | Add refill balance method, fix empty slots, more available_balance coverage 133 | 134 | ### `0.4.5` 135 | 136 | Add validate cadence view method, add owner proxy for sweeping 137 | 138 | ### `0.4.4` 139 | 140 | Mainnet security mitigation 141 | 142 | ### `0.4.0` 143 | 144 | Mainnet preparation, convenience methods, multi-agent support 145 | 146 | ### `0.2.0` 147 | 148 | Audit recommendations implemented, bug fixes. Watch audit here: https://youtu.be/KPAQbFz8RnE 149 | 150 | ### `0.1.0` 151 | 152 | Testnet version stable, gas efficiencies, initial full spec complete 153 | 154 | ### `0.0.1` 155 | 156 | Initial setup -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [ -d "res" ]; then 5 | echo "" 6 | else 7 | mkdir res 8 | fi 9 | 10 | cd "`dirname $0`" 11 | 12 | if [ -z "$KEEP_NAMES" ]; then 13 | export RUSTFLAGS='-C link-arg=-s' 14 | else 15 | export RUSTFLAGS='' 16 | fi 17 | 18 | cargo build --all --target wasm32-unknown-unknown --release 19 | cp target/wasm32-unknown-unknown/release/*.wasm ./res/ -------------------------------------------------------------------------------- /dao/README.md: -------------------------------------------------------------------------------- 1 | # Croncat DAO 2 | Enabling a community to own, grow & maintain the blockchain scheduling utility. 3 | 4 | # Mission 5 | Provide a well balanced group of persons capable of furthering the development of Croncat, maintaining core business objectives that sustain the network & act on community improvement initiatives. 6 | 7 | ### Core Values 8 | * Stability 9 | * Economic Sustainability 10 | * Community Ownership 11 | 12 | # Are you a Croncat? 13 | Croncat is an organism owned & maintained by dedicated people coming from a diverse set of perspectives. 14 | Here are the classifications of what makes up the types of people in Croncat DAO: 15 | * **Founder** - Core contributors that provide vision, implementation & leadership 16 | * **Agent** - The operator executing the tasks & runtime dictated by the DAO 17 | * **Application** - Entities that need scheduled transactions 18 | * **Commander** - Individuals that contribute to initiatives defined by the DAO, receive retainer stipend based on performance (Example: Growth Hacker) 19 | 20 | Becoming part of the Croncat DAO is much different than token based DAOs. It requires real interaction and participation in community development, governance, research & network growth. To maintain a seat on Croncat DAO, you believe in the vision, aspire to further the mission statement, and generally provide your personal perspective to create positive outcomes for the Croncat community as a whole. Maintaining a seat on the DAO requires interactions like voting at minimum 5 times per year. 21 | 22 | # Governance & Operations 23 | ### Council Responsibilities 24 | DAO council will be the sole responsible entity for the development and promotion of croncat. The council will be made up of different types of persons, each bringing a unique viewpoint to the governance process to align and balance the DAO. You can think of the council representing 3 core entities: Founders, Producers, Consumers. The council will have the power to enact proposals, fund development & marketing, provide guidance on integrations and onboard further community members. 25 | 26 | Council members are responsible to the DAO, and have a requirement to on-chain activity periodically to maintain their council seat. By staying active on the chain activities, members not only keep the community inline with their perspective, but also help create a wider base for decisions. Council members are also responsible for doing the research and diligence necessary to make sound decisions for fund allocations. 27 | 28 | ### Role Definitions 29 | 30 | | Role | Capabilities | Definition & Perks | 31 | |---|---|---| 32 | |Founder|Proposal, Voting, Treasury, Core|Maintains council members, directs treasury funds towards development initiatives, maintains upgrades. Can vote on all types of proposals. Receives epoch based retainer stipend of a percentage on earned interest balance.| 33 | |Application|Proposal, Voting|Proposes needs for application integrations, fees or similar. Can vote on operation & cost proposals. Early integration partners receive special swag & NFT.| 34 | |Agent|Proposal, Voting|Proposes needs for agent runtime, reward amount or similar. Can vote on operation & cost proposals. Early adopters receive special swag & rare NFT.| 35 | |Commander|Proposal|Proposes reimbursements, work initiatives, development bounties, marketing initiatives & other types of works created for furthering the growth of cron. Commanders receive differing levels of access to things like social, discussion, development session & more based on longevity and work completed.| 36 | 37 | ### Proposal Types 38 | * **Treasury Proposal**: Items relating to or including fund or token allocation & staking accounting. 39 | * **Operation Cost Proposal**: Items that pay individuals for a certain finalized development or marketing initiative. 40 | * **Custom Operation**: Special contract function calls that can include core runtime settings, interacting with other dApps or upgrading core contracts. 41 | Council Change: Adding/Removing council members only when deemed appropriate by DAO. 42 | 43 | ### Core Operations: 44 | Croncat core contracts have several variables that can be adjusted by the DAO to further align the needs of agents and applications. The purpose was to allow cron to not have a static economic model, but rather adjust to the runtime needs. The following variables define the name, type and intent of each parameter. Note that these settings are only allowed to be adjusted by DAO founders, but can be voted upon by members. 45 | 46 | | Variable | Type | Description | 47 | |---|---|---| 48 | |paused|Boolean|In case of runtime emergency, the contract can be paused| 49 | |owner_id|AccountId|This account represents the active DAO managing the croncat contract| 50 | |agent_fee|Balance|The per-task fee which accrues to the agent for executing the task.| 51 | |gas_price|Balance|This is the gas price, as set by the genesis config of near runtime. In the event that config changes, the setting can be updated to reflect that value.| 52 | |slot_granularity|u64|The total amount of blocks to cluster into a window of execution. Example: If there are 1000 blocks and slot granularity is 100 then there will be 10 “buckets” where tasks will be slotted.| 53 | 54 | ### Core Deployment 55 | Croncat is a living creature, developed by people and autonomously operating on the blockchain. Development will continue to be fluid, where features will be added from time to time. When a new feature is ready to be deployed, the compiled contract code will be staged on-chain, and submitted as an upgrade proposal. Core DAO members will be responsible for testing & ensuring the upgrade will not be malicious, align with all representative parties of cron DAO and meet all coding standards for production contracts. Upon successful approval of upgrade, the croncat contract will utilize a migration function to handle any/all state changes needed. In the event that there are backward incompatibilities, the DAO can decide to launch an entirely new deployed contract. This type of change will need to be communicated among all integration partnerships, publicly disclosed on social and website and maintain the legacy contract until all tasks have been completed. 56 | 57 | # DAO Economic Governance: 58 | The cron DAO will be responsible for appropriately allocating funds towards initiatives that benefit the whole croncat community. The following are possible incentives, each to be approved and potentially implemented by the DAO. Unless otherwise noted, each item will be available to be voted upon by all DAO members. Specific amounts are left out of this document, as they are to be proposed within the DAO. 59 | 60 | ### General Fund Management 61 | Cron DAO will maintain two areas of funds: 62 | 63 | 1. **Treasury**: Funds allocated to treasury will contain collateral provided by tasks, accrued from staking interest, accrued from potential yield initiatives and initially seeded by cron DAO grant(s). The treasury use and allocation for all operations will be only controlled by founder level proposals and voted by founder level votes. Treasury will maintain a budget allocating the majority of funds towards operations and some towards incentives and growth. Treasury will focus on the goal of a fully self-sustaining income based on seeking revenue by accrued interest, developing features for efficiency or further revenues and maintaining the correct ratio of funds to keep ongoing tasks running. 64 | 65 | 2. **Core Operations**: These funds will be automatically managed by the croncat core contracts, and utilized for task deposits, task gas fees, overhead for upcoming task needs and agent reward payout. No funds remaining on the core contracts shall be touched by members of the croncat DAO, unless fee structures are adjusted resulting in a collateral surplus. All changes to fee structures will be actioned directly from the DAO, and surplus or other situations must be handled by cron DAO treasury. 66 | 67 | 68 | ### Treasury Collateral Uses 69 | Core treasury collateral is made up of task fund allocation that will be used at a later time. This means that the majority of the treasury funds will not be available for spending, but rather available for use in the following revenue generation possibilities: 70 | * **Staking**: Majority of funds will be allocated directly to whitelisted staking pools or meta staking, using a cadence based balancing mechanism to keep task funds available. 71 | * **Yield Initiatives**: Token farming, Liquidity Pools 72 | * **Further possibilities**: Lending, Insurance 73 | 74 | Not all of these items will be possible, but are mentioned here as possible DAO decisions and direction for fund allocation. 75 | 76 | 77 | ### Operations Budget 78 | Operations will fund specific needs of the cron DAO that act like traditional business budgets. These needs will be allocated directly to individuals committed to achieving certain goals and tasks, with a set amount monthly or quarterly. These individuals are accepted by the DAO, and are re-evaluated post-acceptance after the completion of 2 calendar months to ensure funds were allocated wisely. Budget funds will be an operating expense, paid by treasury for specific outcomes: 79 | * Retainers: Founders, Developers 80 | * Promotion: Commanders 81 | * Operations: Materials costs for items similar to marketing, swag, publishing, outreach lists or other DAO identified operation needs. 82 | 83 | 84 | ### Incentives 85 | * Bounties - Applied to hackathons & competitions 86 | * Referral rewards - Available to any community member 87 | * Onboarding bonuses: Early application integrations, Application fee waivers, Early agent adoption 88 | * Ongoing bonuses: Application continued use, Agent continued support 89 | 90 | ### DAO Viral Loops 91 | **Application Onboarding** 92 | 93 | Applications running tasks on cron are imperative to the success of croncat. Early integrations using cron should be encouraged by a few initiatives: 94 | 95 | 1. Early integrations will reward both application and ambassador. Applications will receive a set amount of free transactions and free agent fees paid for by cron DAO. See economics section for specific reward amounts. 96 | 97 | 2. All integrations will receive a set amount of tasks that are agent fee free. Agents will still be rewarded for executing these tasks, however the amount will be paid by cron DAO. 98 | 99 | 3. Applications that have continuous tasks running for longer than 3 months or greater than 10,000 tasks will receive cross promotion on cron social and a rare NFT. If possible, the application will also be highlighted as a use case on the cron website. 100 | 101 | **Agent Onboarding** 102 | 103 | Agents keep the lights on for croncat and make the autonomy of cron possible. Agents will be incentivized primarily by rewards per task, but also encouraged in additional ways: 104 | 105 | 1. Promote the use of cron by onboarding new applications or tasks. 106 | 107 | 2. Refer others to become croncat agents. 108 | 109 | 3. Continuously run the croncat agent scripts for 1 year or more with minimal downtime. 110 | 111 | **Outreach, Community Expansion** 112 | 113 | Cron will rely on the community of croncat commanders to grow the adoption of cron and promote awareness. Commanders will be responsible for creating network effects by the following avenues: 114 | 115 | 1. Post promotional materials for onboarding applications, agents and other commanders. 116 | 117 | 2. Produce creative pieces (video, blog, social post) that highlight and encourage cron use cases, potential functionality & more. 118 | 119 | 3. Recruit applications and agents to utilize cron. 120 | -------------------------------------------------------------------------------- /dao/dao_deploy_mainnet.sh: -------------------------------------------------------------------------------- 1 | MASTER_ACC=cron.near 2 | DAO_ROOT_ACC=sputnik-dao.near 3 | DAO_NAME=croncat 4 | DAO_ACCOUNT=$DAO_NAME.$DAO_ROOT_ACC 5 | 6 | ##Change NEAR_ENV between mainnet, testnet and betanet 7 | # export NEAR_ENV=testnet 8 | export NEAR_ENV=mainnet 9 | 10 | FOUNDERS='["tjtc.near", "mike.near", "ozymandius.near", "daobox.near", "bbentley.near"]' 11 | APPLICATIONS='[]' 12 | AGENTS='[]' 13 | COMMANDERS='[]' 14 | 15 | #DAO Policy 16 | export POLICY='{ 17 | "roles": [ 18 | { 19 | "name": "founders", 20 | "kind": { "Group": '$FOUNDERS' }, 21 | "permissions": [ 22 | "*:Finalize", 23 | "*:AddProposal", 24 | "*:VoteApprove", 25 | "*:VoteReject", 26 | "*:VoteRemove" 27 | ], 28 | "vote_policy": { 29 | "Group": { 30 | "weight_kind": "RoleWeight", 31 | "quorum": "0", 32 | "threshold": [1, 5] 33 | } 34 | } 35 | }, 36 | { 37 | "name": "applications", 38 | "kind": { "Group": '$APPLICATIONS' }, 39 | "permissions": [ 40 | "*:AddProposal", 41 | "*:VoteApprove", 42 | "*:VoteReject" 43 | ], 44 | "vote_policy": {} 45 | }, 46 | { 47 | "name": "agents", 48 | "kind": { "Group": '$AGENTS' }, 49 | "permissions": [ 50 | "*:AddProposal", 51 | "*:VoteApprove", 52 | "*:VoteReject" 53 | ], 54 | "vote_policy": {} 55 | }, 56 | { 57 | "name": "commanders", 58 | "kind": { "Group": '$COMMANDERS' }, 59 | "permissions": [ 60 | "*:AddProposal" 61 | ], 62 | "vote_policy": { 63 | "Group": { 64 | "weight_kind": "RoleWeight", 65 | "quorum": "0", 66 | "threshold": [1, 2] 67 | } 68 | } 69 | } 70 | ], 71 | "default_vote_policy": { 72 | "weight_kind": "RoleWeight", 73 | "quorum": "0", 74 | "threshold": [1, 2] 75 | }, 76 | "proposal_bond": "100000000000000000000000", 77 | "proposal_period": "604800000000000", 78 | "bounty_bond": "100000000000000000000000", 79 | "bounty_forgiveness_period": "604800000000000" 80 | }' 81 | 82 | #Args for creating DAO in sputnik-factory2 83 | ARGS=`echo "{\"config\": {\"name\": \"$DAO_NAME\", \"purpose\": \"Enabling a community to own grow and maintain the blockchain scheduling utility\", \"metadata\":\"\"}, \"policy\": $POLICY}" | base64` 84 | FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 85 | 86 | # Call sputnik factory for deploying new dao with custom policy 87 | near call $DAO_ROOT_ACC create "{\"name\": \"$DAO_NAME\", \"args\": \"$FIXED_ARGS\"}" --accountId $MASTER_ACC --amount 5 --gas 150000000000000 88 | near view $DAO_ACCOUNT get_policy 89 | echo "'$NEAR_ENV' Deploy Complete!" -------------------------------------------------------------------------------- /dao/dao_deploy_testnet.sh: -------------------------------------------------------------------------------- 1 | MASTER_ACC=in.testnet 2 | DAO_ROOT_ACC=sputnikv2.testnet 3 | DAO_NAME=croncat_testnet_v3 4 | DAO_ACCOUNT=$DAO_NAME.$DAO_ROOT_ACC 5 | 6 | export NEAR_ENV=testnet 7 | 8 | FOUNDERS='["per.testnet", "in.testnet", "escrow.testnet", "cron.testnet", "ion.testnet"]' 9 | APPLICATIONS='[]' 10 | AGENTS='[]' 11 | COMMANDERS='[]' 12 | 13 | #DAO Policy 14 | export POLICY='{ 15 | "roles": [ 16 | { 17 | "name": "founders", 18 | "kind": { "Group": '$FOUNDERS' }, 19 | "permissions": [ 20 | "*:Finalize", 21 | "*:AddProposal", 22 | "*:VoteApprove", 23 | "*:VoteReject", 24 | "*:VoteRemove" 25 | ], 26 | "vote_policy": { 27 | "Group": { 28 | "weight_kind": "RoleWeight", 29 | "quorum": "0", 30 | "threshold": [1, 5] 31 | } 32 | } 33 | }, 34 | { 35 | "name": "applications", 36 | "kind": { "Group": '$APPLICATIONS' }, 37 | "permissions": [ 38 | "*:AddProposal", 39 | "*:VoteApprove", 40 | "*:VoteReject" 41 | ], 42 | "vote_policy": {} 43 | }, 44 | { 45 | "name": "agents", 46 | "kind": { "Group": '$AGENTS' }, 47 | "permissions": [ 48 | "*:AddProposal", 49 | "*:VoteApprove", 50 | "*:VoteReject" 51 | ], 52 | "vote_policy": {} 53 | }, 54 | { 55 | "name": "commanders", 56 | "kind": { "Group": '$COMMANDERS' }, 57 | "permissions": [ 58 | "*:AddProposal" 59 | ], 60 | "vote_policy": { 61 | "Group": { 62 | "weight_kind": "RoleWeight", 63 | "quorum": "0", 64 | "threshold": [1, 2] 65 | } 66 | } 67 | } 68 | ], 69 | "default_vote_policy": { 70 | "weight_kind": "RoleWeight", 71 | "quorum": "0", 72 | "threshold": [1, 2] 73 | }, 74 | "proposal_bond": "100000000000000000000000", 75 | "proposal_period": "604800000000000", 76 | "bounty_bond": "100000000000000000000000", 77 | "bounty_forgiveness_period": "604800000000000" 78 | }' 79 | 80 | #Args for creating DAO in sputnik-factory2 81 | ARGS=`echo "{\"config\": {\"name\": \"$DAO_NAME\", \"purpose\": \"Enabling a community to own grow and maintain the blockchain scheduling utility\", \"metadata\":\"\"}, \"policy\": $POLICY}" | base64` 82 | FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 83 | 84 | # Call sputnik factory for deploying new dao with custom policy 85 | near call $DAO_ROOT_ACC create "{\"name\": \"$DAO_NAME\", \"args\": \"$FIXED_ARGS\"}" --accountId $MASTER_ACC --amount 5 --gas 150000000000000 86 | near view $DAO_ACCOUNT get_policy 87 | echo "'$NEAR_ENV' Deploy Complete!" -------------------------------------------------------------------------------- /dao/dao_proposals_mainnet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | MASTER_ACC=cron.near 4 | DAO_ROOT_ACC=sputnik-dao.near 5 | DAO_NAME=croncat 6 | DAO_ACCOUNT=$DAO_NAME.$DAO_ROOT_ACC 7 | CRON_ACCOUNT=manager_v1.croncat.near 8 | 9 | export NEAR_ENV=mainnet 10 | 11 | ## CRONCAT Launch proposal (unpause) 12 | # ARGS=`echo "{ \"paused\": false }" | base64` 13 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 14 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Unpause the croncat manager contract to enable cron tasks", "kind": {"FunctionCall": {"receiver_id": "'$CRON_ACCOUNT'", "actions": [{"method_name": "update_settings", "args": "'$FIXED_ARGS'", "deposit": "0", "gas": "50000000000000"}]}}}}' --accountId $MASTER_ACC --amount 0.1 15 | 16 | ## CRONCAT config change proposal 17 | # ARGS=`echo "{ \"agents_eject_threshold\": \"600\" }" | base64` 18 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 19 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Change agent kick length to 10 hours", "kind": {"FunctionCall": {"receiver_id": "'$CRON_ACCOUNT'", "actions": [{"method_name": "update_settings", "args": "'$FIXED_ARGS'", "deposit": "0", "gas": "50000000000000"}]}}}}' --accountId $MASTER_ACC --amount 0.1 20 | 21 | ## CRONCAT Launch proposal: TICK Task 22 | # ARGS=`echo "{\"contract_id\": \"$CRON_ACCOUNT\",\"function_id\": \"tick\",\"cadence\": \"0 0 * * * *\",\"recurring\": true,\"deposit\": \"0\",\"gas\": 2400000000000}" | base64` 23 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 24 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Create cron task to manage TICK method to handle agents every hour for 1 year", "kind": {"FunctionCall": {"receiver_id": "'$CRON_ACCOUNT'", "actions": [{"method_name": "create_task", "args": "'$FIXED_ARGS'", "deposit": "7000000000000000000000000", "gas": "50000000000000"}]}}}}' --accountId $MASTER_ACC --amount 0.1 25 | 26 | ## payout proposal 27 | # PAYOUT_AMT=1000000000000000000000000 28 | # PAYOUT_ACCT=prod.near 29 | # near call $DAO_ACCOUNT add_proposal '{"proposal": { "description": "Payout", "kind": { "Transfer": { "token_id": "", "receiver_id": "'$PAYOUT_ACCT'", "amount": "'$PAYOUT_AMT'" } } } }' --accountId $MASTER_ACC --amount 0.1 30 | 31 | ## add member to one of our roles 32 | # ROLE=founders 33 | # ROLE=applications 34 | # ROLE=agents 35 | # ROLE=commanders 36 | # NEW_MEMBER=prod.near 37 | # near call $DAO_ACCOUNT add_proposal '{ "proposal": { "description": "Welcome '$NEW_MEMBER' to the '$ROLE' team", "kind": { "AddMemberToRole": { "member_id": "'$NEW_MEMBER'", "role": "'$ROLE'" } } } }' --accountId $MASTER_ACC --amount 0.1 38 | # near call $DAO_ACCOUNT add_proposal '{ "proposal": { "description": "Remove '$NEW_MEMBER' from '$ROLE' for non-availability", "kind": { "RemoveMemberFromRole": { "member_id": "'$NEW_MEMBER'", "role": "'$ROLE'" } } } }' --accountId $MASTER_ACC --amount 0.1 39 | 40 | ## CRONCAT Scheduling proposal example 41 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "demo croncat test", "kind": {"FunctionCall": {"receiver_id": "crud.in.testnet", "actions": [{"method_name": "tick", "args": "e30=", "deposit": "0", "gas": "20000000000000"}]}}}}' --accountId $MASTER_ACC --amount 0.1 42 | 43 | # ## CRONCAT Slot Management proposal 44 | # ARGS=`echo "{\"slot\": \"1638307260000000000\"}" | base64` 45 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 46 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Remove a slot that has missing tasks", "kind": {"FunctionCall": {"receiver_id": "'$CRON_ACCOUNT'", "actions": [{"method_name": "remove_slot", "args": "'$FIXED_ARGS'", "deposit": "0", "gas": "50000000000000"}]}}}}' --accountId $MASTER_ACC --amount 0.1 47 | 48 | 49 | 50 | ## ----------------------------------------------- 51 | ## AUTOMATED TREASURY! 52 | ## ----------------------------------------------- 53 | TREASURY_ACCT=treasury.vaultfactory.near 54 | 55 | # Send funds to treasury for auto-management 56 | # near call $DAO_ACCOUNT add_proposal '{"proposal": { "description": "Move near funds to treasury", "kind": { "Transfer": { "token_id": "", "receiver_id": "'$TREASURY_ACCT'", "amount": "1400000000000000000000000000" } } } }' --accountId $MASTER_ACC --amount $BOND_AMOUNT 57 | # TOKEN_ID=token-v3.cheddar.testnet 58 | # near call $DAO_ACCOUNT add_proposal '{"proposal": { "description": "Move token funds to treasury", "kind": { "Transfer": { "token_id": "'$TOKEN_ID'", "receiver_id": "'$TREASURY_ACCT'", "amount": "100000000000000000000000000" } } } }' --accountId $MASTER_ACC --amount $BOND_AMOUNT 59 | 60 | # Staking: Deposit & Stake (Manual) 61 | # ARGS=`echo "{\"pool_account_id\": \"meta-v2.pool.testnet\"}" | base64` 62 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 63 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Send funds to stake with a certain pool", "kind": {"FunctionCall": {"receiver_id": "'$TREASURY_ACCT'", "actions": [{"method_name": "deposit_and_stake", "args": "'$FIXED_ARGS'", "deposit": "100000000000000000000000000", "gas": "180000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 64 | # OR 65 | # ARGS=`echo "{\"pool_account_id\": \"hotones.pool.f863973.m0\"}" | base64` 66 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 67 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Send funds to stake with a certain pool", "kind": {"FunctionCall": {"receiver_id": "'$TREASURY_ACCT'", "actions": [{"method_name": "deposit_and_stake", "args": "'$FIXED_ARGS'", "deposit": "100000000000000000000000000", "gas": "180000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 68 | 69 | # Staking: Liquid Unstake (Manual) 70 | # ARGS=`echo "{\"pool_account_id\": \"meta-v2.pool.testnet\",\"amount\": \"5000000000000000000000000\"}" | base64` 71 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 72 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Request some funds to be immediately released from liquid stake pool", "kind": {"FunctionCall": {"receiver_id": "'$TREASURY_ACCT'", "actions": [{"method_name": "liquid_unstake", "args": "'$FIXED_ARGS'", "deposit": "0", "gas": "180000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 73 | 74 | # Staking: Unstake (Manual + Autowithdraw) 75 | # ARGS=`echo "{\"pool_account_id\": \"hotones.pool.f863973.m0\",\"amount\": \"5000000000000000000000000\"}" | base64` 76 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 77 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Request some funds to be slowly released from vanilla stake pool", "kind": {"FunctionCall": {"receiver_id": "'$TREASURY_ACCT'", "actions": [{"method_name": "unstake", "args": "'$FIXED_ARGS'", "deposit": "0", "gas": "180000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 78 | 79 | # Staking: Auto-Stake Retrieve Balances 80 | # CRONCAT_ARGS=`echo "{\"pool_account_id\": \"meta-v2.pool.testnet\"}" | base64` 81 | # # CRONCAT_ARGS=`echo "{\"pool_account_id\": \"hotones.pool.f863973.m0\"}" | base64` 82 | # CRONCAT_FIXED_ARGS=`echo $CRONCAT_ARGS | tr -d '\r' | tr -d ' '` 83 | # ARGS=`echo "{\"contract_id\": \"$TREASURY_ACCT\",\"function_id\": \"get_staked_balance\",\"cadence\": \"0 0 */1 * * *\",\"recurring\": true,\"deposit\": \"0\",\"gas\": 24000000000000,\"arguments\": \"$CRONCAT_FIXED_ARGS\"}" | base64` 84 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 85 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Create cron task to manage staking balance method every day", "kind": {"FunctionCall": {"receiver_id": "'$CRON_ACCOUNT'", "actions": [{"method_name": "create_task", "args": "'$FIXED_ARGS'", "deposit": "5000000000000000000000000", "gas": "50000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 86 | 87 | ## ----------------------------------------------- 88 | ## ----------------------------------------------- 89 | 90 | 91 | 92 | ## -------------------------------- 93 | ## METAPOOL STAKING 94 | ## -------------------------------- 95 | METAPOOL_ACCT=meta-pool.near 96 | # Stake 97 | # 10 NEAR 98 | # STAKE_AMOUNT_NEAR=10000000000000000000000000 99 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Stake funds from croncat dao to metapool", "kind": {"FunctionCall": {"receiver_id": "'$METAPOOL_ACCT'", "actions": [{"method_name": "deposit_and_stake", "args": "e30=", "deposit": "'$STAKE_AMOUNT_NEAR'", "gas": "20000000000000"}]}}}}' --accountId $MASTER_ACC --amount 0.1 100 | 101 | # # Unstake all (example: 9xVyewMkzxHfRGtx3EyG82mXX8CfPXLJeW4Xo2y6PpXX) 102 | # ARGS=`echo "{ \"amount\": \"10000000000000000000000000\" }" | base64` 103 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 104 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Unstake funds from metapool to croncat dao", "kind": {"FunctionCall": {"receiver_id": "'$METAPOOL_ACCT'", "actions": [{"method_name": "unstake", "args": "'$FIXED_ARGS'", "deposit": "0", "gas": "20000000000000"}]}}}}' --accountId $MASTER_ACC --amount 0.1 105 | 106 | # # Withdraw balance back (example: EKZqArNzsjq9hpYuYt37Y59qU1kmZoxguLwRH2RnDELd) 107 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Withdraw unstaked funds from metapool to croncat dao", "kind": {"FunctionCall": {"receiver_id": "'$METAPOOL_ACCT'", "actions": [{"method_name": "withdraw_unstaked", "args": "e30=", "deposit": "0", "gas": "20000000000000"}]}}}}' --accountId $MASTER_ACC --amount 0.1 108 | 109 | # # Harvest the earned $META token, NOTE: 1 yocto must be attached to perform the right action 110 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Harvest earned META from metapool to croncat dao", "kind": {"FunctionCall": {"receiver_id": "'$METAPOOL_ACCT'", "actions": [{"method_name": "harvest_meta", "args": "e30=", "deposit": "1", "gas": "120000000000000"}]}}}}' --accountId $MASTER_ACC --amount 0.1 111 | 112 | ## -------------------------------- 113 | ## PARAS.ID NFTs 114 | ## -------------------------------- 115 | ## 116 | ## NOTE: To mint a series, first upload artwork via secret account and get the ipfs hash, as there is no way to upload via DAO 117 | PARAS_ACCT=x.paras.near 118 | 119 | # [img, reference] 120 | # https://cdn.paras.id/tr:w-0.8/bafybeigs5r2g3kpz7ucjwtbbadqbif6mwm7a27ga67cou6tme4zj4gvnha 121 | # FOUNDER: {"status":1,"data":["ipfs://bafybeigs5r2g3kpz7ucjwtbbadqbif6mwm7a27ga67cou6tme4zj4gvnha","ipfs://bafybeidy77pjqzurxoanbtuulzinou5tbxoomlbgacumxcxdxht5m2afbq"]} 122 | # COMMANDER: {"status":1,"data":["ipfs://bafybeibzaeouzmifvkjwtpbl5sccp7z3ej5yqyef7yeeb3p5ikpnjtraeu","ipfs://bafybeie5te5ylzgyx76ks2gjolzszb6obfwhd6zply77rfk7yq722xf3nq"]} 123 | # AGENT: {"status":1,"data":["ipfs://bafybeidha5l6fv7jm4mtgg5v6lplp4pmxx4zxxw4jaiq7fxqsx7nnyed3m","ipfs://bafybeibkvkazyu5oed6k6mp7fn7fnm7yv3krvolhfh4zwmciruvt55ardm"]} 124 | # APP: {"status":1,"data":["ipfs://bafybeiae3i55h377miym7yrovyu5b6f75us56zdkkxi3y4gvmaoiotxiva","ipfs://bafybeietne5xe7ad2hsye5ltpdrfri2hpxgheukutv76kuydqki2f4hbam"]} 125 | # CHEFS: {"status":1,"data":["ipfs://bafybeig5bbumicsi6g2hu5gpzcukxke6rcxpcmc7gpgk725t5spxft7i64","ipfs://bafybeiadifgrnym5b6q6ktbd3qh7se2wtu4lcr4ta3fj2waarigifujdva"]} 126 | 127 | # Create the Series! We have 5 Series today: Founders, Agents, Applications, Commanders, Chefs 128 | # nft_create_series 129 | # { 130 | # "creator_id": "croncat.sputnik-dao.near", 131 | # "token_metadata": { 132 | # "title": "Croncat", 133 | # "media": "bafybeid7ytiw7yea...", 134 | # "reference": "bafybeih7jhlqs7g65...", 135 | # "copies": 10 136 | # }, 137 | # "price": null, 138 | # "royalty": { 139 | # "croncat.sputnik-dao.near": 700 140 | # } 141 | # } 142 | # ARGS=`echo "{ \"creator_id\": \"$DAO_ACCOUNT\", \"token_metadata\": { \"title\": \"Croncat DAO Commander\", \"media\": \"bafybeibzaeouzmifvkjwtpbl5sccp7z3ej5yqyef7yeeb3p5ikpnjtraeu\", \"reference\": \"bafybeie5te5ylzgyx76ks2gjolzszb6obfwhd6zply77rfk7yq722xf3nq\", \"copies\": 37 }, \"price\": null, \"royalty\": { \"$DAO_ACCOUNT\": 700 } }" | base64` 143 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 144 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Create NFT series for Commanders of Croncat DAO", "kind": {"FunctionCall": {"receiver_id": "'$PARAS_ACCT'", "actions": [{"method_name": "nft_create_series", "args": "'$FIXED_ARGS'", "deposit": "4500000000000000000000", "gas": "20000000000000"}]}}}}' --accountId $MASTER_ACC --amount 1 145 | 146 | 147 | # Mint an NFT to a user 148 | # Get the token_series_id from the above create series result 149 | # FOUNDER: 40444 150 | # COMMANDER: 40982 151 | # AGENT: 40984 152 | # APP: 40985 153 | # CHEF: 40986 154 | # 155 | # nft_mint 156 | # { 157 | # "token_series_id": "39640", 158 | # "receiver_id": "account.near" 159 | # } 160 | 161 | # SERIES_ID=40444 162 | # RECEIVER=account.near 163 | # ARGS=`echo "{ \"token_series_id\": \"$SERIES_ID\", \"receiver_id\": \"$RECEIVER\" }" | base64` 164 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 165 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Mint NFT for Commander '$RECEIVER'", "kind": {"FunctionCall": {"receiver_id": "'$PARAS_ACCT'", "actions": [{"method_name": "nft_mint", "args": "'$FIXED_ARGS'", "deposit": "7400000000000000000000", "gas": "90000000000000"}]}}}}' --accountId $MASTER_ACC --amount 0.1 166 | 167 | ## -------------------------------- 168 | ## Vote 169 | ## -------------------------------- 170 | ## NOTE: Examples setup as needed, adjust variables for use cases. 171 | # near view $DAO_ACCOUNT get_policy 172 | # near call $DAO_ACCOUNT act_proposal '{"id": 0, "action" :"VoteApprove"}' --accountId $MASTER_ACC --gas 300000000000000 173 | # near call $DAO_ACCOUNT act_proposal '{"id": 0, "action" :"VoteReject"}' --accountId $MASTER_ACC --gas 300000000000000 174 | # near call $DAO_ACCOUNT act_proposal '{"id": 0, "action" :"VoteRemove"}' --accountId $MASTER_ACC --gas 300000000000000 175 | 176 | # # Loop All Action IDs and submit action 177 | # vote_actions=(72 73 74 75 76 77 78 79) 178 | # for (( e=0; e<=${#vote_actions[@]} - 1; e++ )) 179 | # do 180 | # # action="VoteApprove" 181 | # # action="VoteReject" 182 | # action="VoteRemove" 183 | # SUB_ACT_PROPOSAL=`echo "{\"id\": ${vote_actions[e]}, \"action\" :\"${action}\"}"` 184 | # echo "Payload ${SUB_ACT_PROPOSAL}" 185 | 186 | # near call $DAO_ACCOUNT act_proposal '{"id": '${vote_actions[e]}', "action" :"'${action}'"}' --accountId $MASTER_ACC --gas 300000000000000 187 | # done -------------------------------------------------------------------------------- /dao/dao_proposals_testnet.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | MASTER_ACC=in.testnet 3 | DAO_ROOT_ACC=sputnikv2.testnet 4 | DAO_NAME=croncat 5 | DAO_ACCOUNT=$DAO_NAME.$DAO_ROOT_ACC 6 | CRON_ACCOUNT=manager_v1.croncat.testnet 7 | BOND_AMOUNT=0.1 8 | 9 | export NEAR_ENV=testnet 10 | 11 | ## CRONCAT Launch proposal (unpause) 12 | # ARGS=`echo "{ \"paused\": false }" | base64` 13 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 14 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Unpause the croncat manager contract to enable cron tasks", "kind": {"FunctionCall": {"receiver_id": "'$CRON_ACCOUNT'", "actions": [{"method_name": "update_settings", "args": "'$FIXED_ARGS'", "deposit": "0", "gas": "50000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 15 | 16 | ## CRONCAT config change proposal 17 | # ARGS=`echo "{ \"agents_eject_threshold\": \"600\" }" | base64` 18 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 19 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Change agent kick length to 10 hours", "kind": {"FunctionCall": {"receiver_id": "'$CRON_ACCOUNT'", "actions": [{"method_name": "update_settings", "args": "'$FIXED_ARGS'", "deposit": "0", "gas": "50000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 20 | 21 | 22 | # ## CRONCAT Launch proposal: TICK Task 23 | # ARGS=`echo "{\"contract_id\": \"$CRON_ACCOUNT\",\"function_id\": \"tick\",\"cadence\": \"0 0 * * * *\",\"recurring\": true,\"deposit\": \"0\",\"gas\": 9000000000000}" | base64` 24 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 25 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Create cron task to manage TICK method to handle agents every hour", "kind": {"FunctionCall": {"receiver_id": "'$CRON_ACCOUNT'", "actions": [{"method_name": "create_task", "args": "'$FIXED_ARGS'", "deposit": "10000000000000000000000000", "gas": "50000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 26 | 27 | 28 | # ## CRONCAT Slot Management proposal 29 | # ARGS=`echo "{\"slot\": \"1638307260000000000\"}" | base64` 30 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 31 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Remove a slot that has missing tasks", "kind": {"FunctionCall": {"receiver_id": "'$CRON_ACCOUNT'", "actions": [{"method_name": "remove_slot", "args": "'$FIXED_ARGS'", "deposit": "0", "gas": "50000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 32 | 33 | 34 | ## payout proposal 35 | # near call $DAO_ACCOUNT add_proposal '{"proposal": { "description": "", "kind": { "Transfer": { "token_id": "", "receiver_id": "in.testnet", "amount": "1000000000000000000000000" } } } }' --accountId $MASTER_ACC --amount $BOND_AMOUNT 36 | 37 | ## add member to one of our roles 38 | # ROLE=founders 39 | # ROLE=applications 40 | # ROLE=agents 41 | # ROLE=commanders 42 | # NEW_MEMBER=pa.testnet 43 | # near call $DAO_ACCOUNT add_proposal '{ "proposal": { "description": "Welcome '$NEW_MEMBER' to the '$ROLE' team", "kind": { "AddMemberToRole": { "member_id": "'$NEW_MEMBER'", "role": "'$ROLE'" } } } }' --accountId $MASTER_ACC --amount $BOND_AMOUNT 44 | 45 | ## CRONCAT Scheduling proposal example 46 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "demo croncat test", "kind": {"FunctionCall": {"receiver_id": "crud.in.testnet", "actions": [{"method_name": "tick", "args": "e30=", "deposit": "0", "gas": "20000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 47 | 48 | 49 | 50 | ## ----------------------------------------------- 51 | ## AUTOMATED TREASURY! 52 | ## ----------------------------------------------- 53 | TREASURY_ACCT=treasury.vaultfactory.testnet 54 | 55 | # Send funds to treasury for auto-management 56 | # near call $DAO_ACCOUNT add_proposal '{"proposal": { "description": "Move near funds to treasury", "kind": { "Transfer": { "token_id": "", "receiver_id": "'$TREASURY_ACCT'", "amount": "1400000000000000000000000000" } } } }' --accountId $MASTER_ACC --amount $BOND_AMOUNT 57 | # TOKEN_ID=token-v3.cheddar.testnet 58 | # near call $DAO_ACCOUNT add_proposal '{"proposal": { "description": "Move token funds to treasury", "kind": { "Transfer": { "token_id": "'$TOKEN_ID'", "receiver_id": "'$TREASURY_ACCT'", "amount": "100000000000000000000000000" } } } }' --accountId $MASTER_ACC --amount $BOND_AMOUNT 59 | 60 | # Staking: Deposit & Stake (Manual) 61 | # ARGS=`echo "{\"pool_account_id\": \"meta-v2.pool.testnet\"}" | base64` 62 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 63 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Send funds to stake with a certain pool", "kind": {"FunctionCall": {"receiver_id": "'$TREASURY_ACCT'", "actions": [{"method_name": "deposit_and_stake", "args": "'$FIXED_ARGS'", "deposit": "100000000000000000000000000", "gas": "180000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 64 | # OR 65 | # ARGS=`echo "{\"pool_account_id\": \"hotones.pool.f863973.m0\"}" | base64` 66 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 67 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Send funds to stake with a certain pool", "kind": {"FunctionCall": {"receiver_id": "'$TREASURY_ACCT'", "actions": [{"method_name": "deposit_and_stake", "args": "'$FIXED_ARGS'", "deposit": "100000000000000000000000000", "gas": "180000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 68 | 69 | # Staking: Liquid Unstake (Manual) 70 | # ARGS=`echo "{\"pool_account_id\": \"meta-v2.pool.testnet\",\"amount\": \"5000000000000000000000000\"}" | base64` 71 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 72 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Request some funds to be immediately released from liquid stake pool", "kind": {"FunctionCall": {"receiver_id": "'$TREASURY_ACCT'", "actions": [{"method_name": "liquid_unstake", "args": "'$FIXED_ARGS'", "deposit": "0", "gas": "180000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 73 | 74 | # Staking: Unstake (Manual + Autowithdraw) 75 | # ARGS=`echo "{\"pool_account_id\": \"hotones.pool.f863973.m0\",\"amount\": \"5000000000000000000000000\"}" | base64` 76 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 77 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Request some funds to be slowly released from vanilla stake pool", "kind": {"FunctionCall": {"receiver_id": "'$TREASURY_ACCT'", "actions": [{"method_name": "unstake", "args": "'$FIXED_ARGS'", "deposit": "0", "gas": "180000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 78 | 79 | # Staking: Auto-Stake Retrieve Balances 80 | # CRONCAT_ARGS=`echo "{\"pool_account_id\": \"meta-v2.pool.testnet\"}" | base64` 81 | # # CRONCAT_ARGS=`echo "{\"pool_account_id\": \"hotones.pool.f863973.m0\"}" | base64` 82 | # CRONCAT_FIXED_ARGS=`echo $CRONCAT_ARGS | tr -d '\r' | tr -d ' '` 83 | # ARGS=`echo "{\"contract_id\": \"$TREASURY_ACCT\",\"function_id\": \"get_staked_balance\",\"cadence\": \"0 0 */1 * * *\",\"recurring\": true,\"deposit\": \"0\",\"gas\": 24000000000000,\"arguments\": \"$CRONCAT_FIXED_ARGS\"}" | base64` 84 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 85 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Create cron task to manage staking balance method every day", "kind": {"FunctionCall": {"receiver_id": "'$CRON_ACCOUNT'", "actions": [{"method_name": "create_task", "args": "'$FIXED_ARGS'", "deposit": "5000000000000000000000000", "gas": "50000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 86 | 87 | ## ----------------------------------------------- 88 | ## ----------------------------------------------- 89 | 90 | 91 | 92 | # ## METAPOOL STAKING 93 | # METAPOOL_ACCT=meta-v2.pool.testnet 94 | # # Stake (example: 5jh8NP6dwXUELQfTSZSQzKpyaPjuue8btEaxv1Ng1MBT, and https://explorer.testnet.near.org/transactions/4rgrYB9W1UxZVzyVeYRP6sAGsWv18tfsB3zX3KjyYqqF) 95 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Stake some funds from croncat dao to metapool", "kind": {"FunctionCall": {"receiver_id": "'$METAPOOL_ACCT'", "actions": [{"method_name": "deposit_and_stake", "args": "e30=", "deposit": "10000000000000000000000000", "gas": "20000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 96 | 97 | # # Unstake all (example: 9xVyewMkzxHfRGtx3EyG82mXX8CfPXLJeW4Xo2y6PpXX) 98 | # ARGS=`echo "{ \"amount\": \"10000000000000000000000000\" }" | base64` 99 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 100 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Unstake all funds from metapool to croncat dao", "kind": {"FunctionCall": {"receiver_id": "'$METAPOOL_ACCT'", "actions": [{"method_name": "unstake", "args": "'$FIXED_ARGS'", "deposit": "0", "gas": "20000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 101 | 102 | # # Withdraw balance back (example: EKZqArNzsjq9hpYuYt37Y59qU1kmZoxguLwRH2RnDELd) 103 | # near call $DAO_ACCOUNT add_proposal '{"proposal": {"description": "Withdraw unstaked funds from metapool to croncat dao", "kind": {"FunctionCall": {"receiver_id": "'$METAPOOL_ACCT'", "actions": [{"method_name": "withdraw_unstaked", "args": "e30=", "deposit": "0", "gas": "20000000000000"}]}}}}' --accountId $MASTER_ACC --amount $BOND_AMOUNT 104 | 105 | ## NOTE: Examples setup as needed, adjust variables for use cases. 106 | # near view $DAO_ACCOUNT get_policy 107 | near call $DAO_ACCOUNT act_proposal '{"id": 46, "action" :"VoteApprove"}' --accountId $MASTER_ACC --gas 300000000000000 108 | # near call $DAO_ACCOUNT act_proposal '{"id": 0, "action" :"VoteReject"}' --accountId $MASTER_ACC --gas 300000000000000 109 | # near call $DAO_ACCOUNT act_proposal '{"id": 0, "action" :"VoteRemove"}' --accountId $MASTER_ACC --gas 300000000000000 110 | -------------------------------------------------------------------------------- /examples/airdrop/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-C", "link-args=-s"] -------------------------------------------------------------------------------- /examples/airdrop/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airdrop" 3 | version = "0.0.1" 4 | authors = ["Cron.cat", "@trevorjtclarke"] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | near-sdk = "3.1.0" 12 | -------------------------------------------------------------------------------- /examples/airdrop/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use near_sdk::{ 4 | borsh::{self, BorshDeserialize, BorshSerialize}, 5 | collections::UnorderedSet, 6 | env, ext_contract, 7 | json_types::{ValidAccountId, U128}, 8 | log, near_bindgen, 9 | serde::{Deserialize, Serialize}, 10 | AccountId, Balance, BorshStorageKey, Gas, PanicOnDefault, Promise, 11 | }; 12 | 13 | near_sdk::setup_alloc!(); 14 | 15 | pub const MAX_ACCOUNTS: u64 = 100_000; 16 | pub const PAGINATION_SIZE: u128 = 5; 17 | 18 | const BASE_GAS: Gas = 5_000_000_000_000; 19 | const PROMISE_CALL: Gas = 5_000_000_000_000; 20 | const GAS_FOR_FT_TRANSFER: Gas = BASE_GAS + PROMISE_CALL; 21 | const GAS_FOR_NFT_TRANSFER: Gas = BASE_GAS + PROMISE_CALL; 22 | 23 | // const NO_DEPOSIT: Balance = 0; 24 | const ONE_YOCTO: Balance = 1; 25 | 26 | #[derive(BorshStorageKey, BorshSerialize)] 27 | pub enum StorageKeys { 28 | Accounts, 29 | Managers, 30 | NonFungibleTokens, 31 | NonFungibleHoldings, 32 | } 33 | 34 | // #[derive(BorshStorageKey, BorshSerialize)] 35 | #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone, PartialEq, Debug)] 36 | #[serde(crate = "near_sdk::serde")] 37 | pub enum TransferType { 38 | Near, 39 | FungibleToken, 40 | NonFungibleToken, 41 | } 42 | 43 | #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] 44 | pub struct NftToken { 45 | id: u128, 46 | owner_id: AccountId, 47 | } 48 | 49 | #[ext_contract(ext_ft)] 50 | pub trait ExtFungibleToken { 51 | fn ft_transfer(&self, receiver_id: AccountId, amount: U128); 52 | fn ft_balance_of(&self, account_id: AccountId) -> U128; 53 | } 54 | 55 | #[ext_contract(ext_nft)] 56 | pub trait ExtNonFungibleToken { 57 | fn nft_transfer( 58 | &self, 59 | receiver_id: AccountId, 60 | token_id: U128, 61 | approval_id: Option, 62 | memo: Option, 63 | ); 64 | fn nft_token(&self, token_id: U128) -> NftToken; 65 | } 66 | 67 | #[near_bindgen] 68 | #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] 69 | pub struct Airdrop { 70 | accounts: UnorderedSet, 71 | managers: UnorderedSet, 72 | index: u128, 73 | page_size: u128, 74 | 75 | // FT & NFT: 76 | ft_account: AccountId, 77 | nft_account: AccountId, 78 | } 79 | 80 | #[near_bindgen] 81 | impl Airdrop { 82 | /// ```bash 83 | /// near call airdrop.testnet new --accountId airdrop.testnet 84 | /// ``` 85 | #[init] 86 | pub fn new( 87 | ft_account_id: Option, 88 | nft_account_id: Option, 89 | ) -> Self { 90 | let default_ft_account = 91 | ValidAccountId::try_from(env::current_account_id().as_str()).unwrap(); 92 | let default_nft_account = 93 | ValidAccountId::try_from(env::current_account_id().as_str()).unwrap(); 94 | Airdrop { 95 | accounts: UnorderedSet::new(StorageKeys::Accounts), 96 | managers: UnorderedSet::new(StorageKeys::Managers), 97 | index: 0, 98 | page_size: PAGINATION_SIZE, 99 | ft_account: ft_account_id.unwrap_or(default_ft_account).into(), 100 | nft_account: nft_account_id.unwrap_or(default_nft_account).into(), 101 | } 102 | } 103 | 104 | /// Add An Approved Manager 105 | /// 106 | /// ```bash 107 | /// near call airdrop.testnet add_manager '{"account_id":"manager.testnet"}' 108 | /// ``` 109 | #[private] 110 | pub fn add_manager(&mut self, account_id: AccountId) { 111 | self.managers.insert(&account_id); 112 | } 113 | 114 | /// Remove An Account 115 | /// 116 | /// ```bash 117 | /// near call airdrop.testnet remove_manager '{"account_id":"manager.testnet"}' 118 | /// ``` 119 | #[private] 120 | pub fn remove_manager(&mut self, account_id: AccountId) { 121 | self.managers.remove(&account_id); 122 | } 123 | 124 | /// Add An Account that will receive an airdrop 125 | /// 126 | /// ```bash 127 | /// near call airdrop.testnet add_account '{"account_id":"friend.testnet"}' 128 | /// ``` 129 | pub fn add_account(&mut self, account_id: AccountId) { 130 | assert!( 131 | self.managers.contains(&env::predecessor_account_id()), 132 | "Must be manager" 133 | ); 134 | assert!(self.accounts.len() < MAX_ACCOUNTS, "Max accounts stored"); 135 | assert!( 136 | !self.managers.contains(&account_id), 137 | "Account already added" 138 | ); 139 | self.accounts.insert(&account_id); 140 | } 141 | 142 | /// Remove An Account 143 | /// 144 | /// ```bash 145 | /// near call airdrop.testnet remove_account '{"account_id":"friend.testnet"}' 146 | /// ``` 147 | pub fn remove_account(&mut self, account_id: AccountId) { 148 | assert!( 149 | self.managers.contains(&env::predecessor_account_id()), 150 | "Must be manager" 151 | ); 152 | self.accounts.remove(&account_id); 153 | } 154 | 155 | /// Reset known accounts 156 | /// 157 | /// ```bash 158 | /// near call airdrop.testnet reset 159 | /// ``` 160 | pub fn reset(&mut self) { 161 | assert!( 162 | self.managers.contains(&env::predecessor_account_id()), 163 | "Must be manager" 164 | ); 165 | self.accounts.clear(); 166 | log!("Removed all accounts"); 167 | } 168 | 169 | /// Reset known accounts 170 | /// 171 | /// ```bash 172 | /// near call airdrop.testnet reset_index 173 | /// ``` 174 | pub fn reset_index(&mut self) { 175 | assert!( 176 | self.managers.contains(&env::predecessor_account_id()), 177 | "Must be manager" 178 | ); 179 | self.index = 0; 180 | log!("Reset index to 0"); 181 | } 182 | 183 | /// Stats about the contract 184 | /// 185 | /// ```bash 186 | /// near view airdrop.testnet stats 187 | /// ``` 188 | pub fn stats(&self) -> (u128, u128, u64, u64) { 189 | ( 190 | self.index, 191 | self.page_size, 192 | self.managers.len(), 193 | self.accounts.len(), 194 | ) 195 | } 196 | 197 | /// Send airdrop to paginated accounts! 198 | /// NOTE:s 199 | /// - TransferType is how you can use the same method for diff promises to distribute across accounts 200 | /// - Amount is the units being transfered to EACH account, so either a FT amount or NFT ID 201 | /// - FT/NFT account only accept 1, but can be extended to support multiple if desired. 202 | /// - If used in conjunction with croncat, amount is optional so the internal contract can decide on variable token amounts 203 | /// 204 | /// TODO: Pop-remove style too, so the accounts list gets smaller 205 | /// 206 | /// ```bash 207 | /// near call airdrop.testnet multisend '{"transfer_type": "FungibleToken", "amount": "1234567890000000"}' --amount 1 208 | /// ``` 209 | #[payable] 210 | pub fn multisend(&mut self, transfer_type: TransferType, amount: Option) { 211 | assert!(self.accounts.len() > 0, "No accounts"); 212 | let token_amount = amount.unwrap_or(U128::from(0)); 213 | assert!(token_amount.0 > 0, "Nothing to send"); 214 | 215 | let start = self.index; 216 | let end_index = u128::max(self.index.saturating_add(self.page_size), 0); 217 | let end = u128::min(end_index, self.accounts.len() as u128); 218 | log!( 219 | "start {:?}, end {:?} -- index {:?}, total {:?}", 220 | &start, 221 | &end, 222 | self.index, 223 | self.accounts.len() 224 | ); 225 | 226 | // Check current index 227 | // Stop if index has run out of accounts 228 | // Get max index and see if we exceeded 229 | assert_ne!(start, end, "No items to paginate"); 230 | assert!(self.index < end, "Index has reached end"); 231 | 232 | // Return all tasks within range 233 | // loop and transfer funds to each account 234 | let keys = self.accounts.as_vector(); 235 | for i in start..end { 236 | if let Some(acct) = keys.get(i as u64) { 237 | match transfer_type { 238 | TransferType::Near => { 239 | Promise::new(acct).transfer(token_amount.into()); 240 | } 241 | TransferType::FungibleToken => { 242 | ext_ft::ft_transfer( 243 | acct, 244 | token_amount, 245 | &self.ft_account, 246 | ONE_YOCTO, 247 | GAS_FOR_FT_TRANSFER, 248 | ); 249 | } 250 | TransferType::NonFungibleToken => { 251 | ext_nft::nft_transfer( 252 | acct, 253 | token_amount, 254 | // TODO: Could support approval_id & memo 255 | None, 256 | None, 257 | &self.nft_account, 258 | ONE_YOCTO, 259 | GAS_FOR_NFT_TRANSFER, 260 | ); 261 | } 262 | } 263 | } 264 | } 265 | 266 | // increment index upon completion 267 | self.index = self.index.saturating_add(self.page_size); 268 | } 269 | } 270 | 271 | // NOTE: Im sorry, i didnt have time for adding tests. 272 | // DO YOU? If so, get a bounty reward: https://github.com/Cron-Near/bounties 273 | // 274 | // // use the attribute below for unit tests 275 | // #[cfg(test)] 276 | // mod tests { 277 | // use super::*; 278 | // use near_sdk::MockedBlockchain; 279 | // use near_sdk::{testing_env, VMContext}; 280 | // } 281 | -------------------------------------------------------------------------------- /examples/charity/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-C", "link-args=-s"] -------------------------------------------------------------------------------- /examples/charity/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "charity" 3 | version = "0.0.1" 4 | authors = ["Cron.cat", "@trevorjtclarke"] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | near-sdk = "3.1.0" 12 | -------------------------------------------------------------------------------- /examples/charity/src/lib.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::{ 2 | borsh::{self, BorshDeserialize, BorshSerialize}, 3 | collections::UnorderedSet, 4 | env, log, near_bindgen, AccountId, BorshStorageKey, PanicOnDefault, Promise, 5 | }; 6 | 7 | near_sdk::setup_alloc!(); 8 | 9 | #[derive(BorshStorageKey, BorshSerialize)] 10 | pub enum StorageKeys { 11 | Accounts, 12 | } 13 | 14 | #[near_bindgen] 15 | #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] 16 | pub struct Donations { 17 | beneficiaries: UnorderedSet, 18 | total: u128, 19 | paid: u128, 20 | } 21 | 22 | #[near_bindgen] 23 | impl Donations { 24 | /// ```bash 25 | /// near call donations.testnet new --accountId donations.testnet 26 | /// ``` 27 | #[init] 28 | pub fn new() -> Self { 29 | Donations { 30 | beneficiaries: UnorderedSet::new(StorageKeys::Accounts), 31 | total: 0, 32 | paid: 0, 33 | } 34 | } 35 | 36 | /// Add A Beneficiary 37 | /// 38 | /// ```bash 39 | /// near call donations.testnet add_account '{"account_id":"friend.testnet"}' 40 | /// ``` 41 | pub fn add_account(&mut self, account_id: AccountId) { 42 | assert!(self.beneficiaries.len() < 10, "Max beneficiaries stored"); 43 | self.beneficiaries.insert(&account_id); 44 | } 45 | 46 | /// Remove A Beneficiary 47 | /// 48 | /// ```bash 49 | /// near call donations.testnet remove_account '{"account_id":"friend.testnet"}' 50 | /// ``` 51 | pub fn remove_account(&mut self, account_id: AccountId) { 52 | self.beneficiaries.remove(&account_id); 53 | } 54 | 55 | /// Reset known beneficiaries 56 | /// 57 | /// ```bash 58 | /// near call donations.testnet reset 59 | /// ``` 60 | pub fn reset(&mut self) { 61 | self.beneficiaries.clear(); 62 | log!("Removed all beneficiaries"); 63 | } 64 | 65 | /// Stats about the contract 66 | /// 67 | /// ```bash 68 | /// near view donations.testnet stats 69 | /// ``` 70 | pub fn stats(&self) -> (u128, u128) { 71 | (self.total, self.paid) 72 | } 73 | 74 | /// Contribution of donations to all beneficiaries! 75 | /// 76 | /// ```bash 77 | /// near call donations.testnet donate --amount 10 78 | /// ``` 79 | #[payable] 80 | pub fn donate(&mut self) { 81 | assert!(self.beneficiaries.len() > 0, "No beneficiaries"); 82 | assert!( 83 | env::attached_deposit() > 0, 84 | "Must include amount to be paid to all beneficiaries" 85 | ); 86 | assert!( 87 | env::attached_deposit() / u128::from(self.beneficiaries.len()) > 1_000_000_000, 88 | "Minimum amount not met to cover transfers" 89 | ); 90 | let donation = env::attached_deposit() / u128::from(self.beneficiaries.len()); 91 | 92 | // update stats 93 | self.paid += env::attached_deposit(); 94 | 95 | // loop and transfer funds to each account 96 | for acct in self.beneficiaries.iter() { 97 | Promise::new(acct).transfer(donation); 98 | self.total += 1; 99 | } 100 | } 101 | } 102 | 103 | #[cfg(all(test, not(target_arch = "wasm32")))] 104 | mod tests { 105 | use super::*; 106 | use near_sdk::json_types::ValidAccountId; 107 | use near_sdk::test_utils::{accounts, VMContextBuilder}; 108 | use near_sdk::{testing_env, MockedBlockchain}; 109 | 110 | fn get_context(predecessor_account_id: ValidAccountId) -> VMContextBuilder { 111 | let mut builder = VMContextBuilder::new(); 112 | builder 113 | .current_account_id(accounts(0)) 114 | .signer_account_id(predecessor_account_id.clone()) 115 | .signer_account_pk(b"ed25519:4ZhGmuKTfQn9ZpHCQVRwEr4JnutL8Uu3kArfxEqksfVM".to_vec()) 116 | .predecessor_account_id(predecessor_account_id) 117 | .block_index(1234) 118 | .block_timestamp(1_600_000_000_000_000_000); 119 | builder 120 | } 121 | 122 | #[test] 123 | fn test_contract_new() { 124 | let mut context = get_context(accounts(1)); 125 | testing_env!(context.build()); 126 | let contract = Donations::new(); 127 | testing_env!(context.is_view(true).build()); 128 | assert_eq!(contract.stats().0, 0, "Stats is not empty"); 129 | } 130 | 131 | #[test] 132 | fn test_add_beneficiaries() { 133 | let mut context = get_context(accounts(1)); 134 | testing_env!(context.is_view(false).build()); 135 | let mut contract = Donations::new(); 136 | contract.add_account(accounts(2).to_string()); 137 | testing_env!(context.is_view(true).build()); 138 | assert_eq!(contract.beneficiaries.len(), 1, "Wrong number of accounts"); 139 | } 140 | 141 | #[test] 142 | fn test_remove_beneficiaries() { 143 | let mut context = get_context(accounts(1)); 144 | testing_env!(context.is_view(false).build()); 145 | let mut contract = Donations::new(); 146 | contract.add_account(accounts(2).to_string()); 147 | assert_eq!(contract.beneficiaries.len(), 1, "Wrong number of accounts"); 148 | contract.remove_account(accounts(2).to_string()); 149 | testing_env!(context.is_view(true).build()); 150 | assert_eq!(contract.beneficiaries.len(), 0, "Wrong number of accounts"); 151 | } 152 | 153 | #[test] 154 | fn test_reset_beneficiaries() { 155 | let mut context = get_context(accounts(1)); 156 | testing_env!(context.is_view(false).build()); 157 | let mut contract = Donations::new(); 158 | contract.add_account(accounts(2).to_string()); 159 | contract.add_account(accounts(3).to_string()); 160 | contract.add_account(accounts(4).to_string()); 161 | contract.add_account(accounts(5).to_string()); 162 | assert_eq!(contract.beneficiaries.len(), 4, "Wrong number of accounts"); 163 | contract.reset(); 164 | testing_env!(context.is_view(true).build()); 165 | assert_eq!(contract.beneficiaries.len(), 0, "Wrong number of accounts"); 166 | } 167 | 168 | #[test] 169 | fn test_donation() { 170 | let mut context = get_context(accounts(1)); 171 | testing_env!(context.is_view(false).build()); 172 | let mut contract = Donations::new(); 173 | contract.add_account(accounts(2).to_string()); 174 | contract.add_account(accounts(3).to_string()); 175 | assert_eq!(contract.beneficiaries.len(), 2, "Wrong number of accounts"); 176 | testing_env!(context 177 | .is_view(false) 178 | .attached_deposit(10_000_000_000_000_000_000_000_000) 179 | .build()); 180 | contract.donate(); 181 | testing_env!(context.is_view(true).build()); 182 | println!("contract.stats() {:?}", contract.stats()); 183 | assert_eq!( 184 | contract.stats().0, 185 | u128::from(contract.beneficiaries.len()), 186 | "Payments increased" 187 | ); 188 | assert_eq!( 189 | contract.stats().1, 190 | 10_000_000_000_000_000_000_000_000, 191 | "Payment amount increased" 192 | ); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /examples/counter/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-C", "link-args=-s"] -------------------------------------------------------------------------------- /examples/counter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-counter-tutorial" 3 | version = "0.1.0" 4 | authors = ["Near Inc "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | near-sdk = "2.0.0" 12 | -------------------------------------------------------------------------------- /examples/counter/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This contract implements simple counter backed by storage on blockchain. 2 | //! 3 | //! The contract provides methods to [increment] / [decrement] counter and 4 | //! [get it's current value][get_num] or [reset]. 5 | //! 6 | //! [increment]: struct.Counter.html#method.increment 7 | //! [decrement]: struct.Counter.html#method.decrement 8 | //! [get_num]: struct.Counter.html#method.get_num 9 | //! [reset]: struct.Counter.html#method.reset 10 | 11 | use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; 12 | use near_sdk::{env, near_bindgen}; 13 | 14 | #[global_allocator] 15 | static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT; 16 | 17 | // add the following attributes to prepare your code for serialization and invocation on the blockchain 18 | // More built-in Rust attributes here: https://doc.rust-lang.org/reference/attributes.html#built-in-attributes-index 19 | #[near_bindgen] 20 | #[derive(Default, BorshDeserialize, BorshSerialize)] 21 | pub struct Counter { 22 | // See more data types at https://doc.rust-lang.org/book/ch03-02-data-types.html 23 | val: i128, // changed to allow maximum calls before overflow 24 | } 25 | 26 | #[near_bindgen] 27 | impl Counter { 28 | /// Returns 8-bit signed integer of the counter value. 29 | /// 30 | /// This must match the type from our struct's 'val' defined above. 31 | /// 32 | /// Note, the parameter is `&self` (without being mutable) meaning it doesn't modify state. 33 | /// In the frontend (/src/main.js) this is added to the "viewMethods" array 34 | /// using near-cli we can call this by: 35 | /// 36 | /// ```bash 37 | /// near view counter.YOU.testnet get_num 38 | /// ``` 39 | pub fn get_num(&self) -> i128 { 40 | return self.val; 41 | } 42 | 43 | /// Increment the counter. 44 | /// 45 | /// Note, the parameter is "&mut self" as this function modifies state. 46 | /// In the frontend (/src/main.js) this is added to the "changeMethods" array 47 | /// using near-cli we can call this by: 48 | /// 49 | /// ```bash 50 | /// near call counter.YOU.testnet increment --accountId donation.YOU.testnet 51 | /// ``` 52 | pub fn increment(&mut self) { 53 | // note: adding one like this is an easy way to accidentally overflow 54 | // real smart contracts will want to have safety checks 55 | self.val += 1; 56 | let log_message = format!("Increased number to {}", self.val); 57 | env::log(log_message.as_bytes()); 58 | after_counter_change(); 59 | } 60 | 61 | /// Decrement (subtract from) the counter. 62 | /// 63 | /// In (/src/main.js) this is also added to the "changeMethods" array 64 | /// using near-cli we can call this by: 65 | /// 66 | /// ```bash 67 | /// near call counter.YOU.testnet decrement --accountId donation.YOU.testnet 68 | /// ``` 69 | pub fn decrement(&mut self) { 70 | // note: subtracting one like this is an easy way to accidentally overflow 71 | // real smart contracts will want to have safety checks 72 | self.val -= 1; 73 | let log_message = format!("Decreased number to {}", self.val); 74 | env::log(log_message.as_bytes()); 75 | after_counter_change(); 76 | } 77 | 78 | /// Reset to zero. 79 | pub fn reset(&mut self) { 80 | self.val = 0; 81 | // Another way to log is to cast a string into bytes, hence "b" below: 82 | env::log(b"Reset counter to zero"); 83 | } 84 | } 85 | 86 | // unlike the struct's functions above, this function cannot use attributes #[derive(…)] or #[near_bindgen] 87 | // any attempts will throw helpful warnings upon 'cargo build' 88 | // while this function cannot be invoked directly on the blockchain, it can be called from an invoked function 89 | fn after_counter_change() { 90 | // show helpful warning that i8 (8-bit signed integer) will overflow above 127 or below -128 91 | env::log("Make sure you don't overflow, my friend.".as_bytes()); 92 | } 93 | 94 | /* 95 | * the rest of this file sets up unit tests 96 | * to run these, the command will be: 97 | * cargo test --package rust-counter-tutorial -- --nocapture 98 | * Note: 'rust-counter-tutorial' comes from cargo.toml's 'name' key 99 | */ 100 | 101 | // use the attribute below for unit tests 102 | #[cfg(test)] 103 | mod tests { 104 | use super::*; 105 | use near_sdk::MockedBlockchain; 106 | use near_sdk::{testing_env, VMContext}; 107 | 108 | // part of writing unit tests is setting up a mock context 109 | // in this example, this is only needed for env::log in the contract 110 | // this is also a useful list to peek at when wondering what's available in env::* 111 | fn get_context(input: Vec, is_view: bool) -> VMContext { 112 | VMContext { 113 | current_account_id: "alice.testnet".to_string(), 114 | signer_account_id: "robert.testnet".to_string(), 115 | signer_account_pk: vec![0, 1, 2], 116 | predecessor_account_id: "jane.testnet".to_string(), 117 | input, 118 | block_index: 0, 119 | block_timestamp: 0, 120 | account_balance: 0, 121 | account_locked_balance: 0, 122 | storage_usage: 0, 123 | attached_deposit: 0, 124 | prepaid_gas: 10u64.pow(18), 125 | random_seed: vec![0, 1, 2], 126 | is_view, 127 | output_data_receivers: vec![], 128 | epoch_height: 19, 129 | } 130 | } 131 | 132 | // mark individual unit tests with #[test] for them to be registered and fired 133 | #[test] 134 | fn increment() { 135 | // set up the mock context into the testing environment 136 | let context = get_context(vec![], false); 137 | testing_env!(context); 138 | // instantiate a contract variable with the counter at zero 139 | let mut contract = Counter { val: 0 }; 140 | contract.increment(); 141 | println!("Value after increment: {}", contract.get_num()); 142 | // confirm that we received 1 when calling get_num 143 | assert_eq!(1, contract.get_num()); 144 | } 145 | 146 | #[test] 147 | fn decrement() { 148 | let context = get_context(vec![], false); 149 | testing_env!(context); 150 | let mut contract = Counter { val: 0 }; 151 | contract.decrement(); 152 | println!("Value after decrement: {}", contract.get_num()); 153 | // confirm that we received -1 when calling get_num 154 | assert_eq!(-1, contract.get_num()); 155 | } 156 | 157 | #[test] 158 | fn increment_and_reset() { 159 | let context = get_context(vec![], false); 160 | testing_env!(context); 161 | let mut contract = Counter { val: 0 }; 162 | contract.increment(); 163 | contract.reset(); 164 | println!("Value after reset: {}", contract.get_num()); 165 | // confirm that we received -1 when calling get_num 166 | assert_eq!(0, contract.get_num()); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /examples/cross-contract/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-C", "link-args=-s"] -------------------------------------------------------------------------------- /examples/cross-contract/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cross-contract" 3 | version = "0.1.0" 4 | authors = ["Cron.cat", "@trevorjtclarke"] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | near-sdk = "3.1.0" 12 | -------------------------------------------------------------------------------- /examples/cross-contract/src/lib.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::{ 2 | borsh::{self, BorshDeserialize, BorshSerialize}, 3 | collections::Vector, 4 | env, ext_contract, 5 | json_types::{Base64VecU8, U128, U64}, 6 | log, near_bindgen, 7 | serde::{Deserialize, Serialize}, 8 | serde_json, AccountId, BorshStorageKey, Gas, PanicOnDefault, Promise, 9 | }; 10 | 11 | near_sdk::setup_alloc!(); 12 | 13 | /// Basic configs 14 | pub const ONE_NEAR: u128 = 1_000_000_000_000_000_000_000_000; 15 | pub const NANOS: u64 = 1_000_000; 16 | pub const MILLISECONDS_IN_MINUTE: u64 = 60_000; 17 | pub const MILLISECONDS_IN_HOUR: u64 = 3_600_000; 18 | pub const MILLISECONDS_IN_DAY: u64 = 86_400_000; 19 | 20 | /// Gas & Balance Configs 21 | pub const NO_DEPOSIT: u128 = 0; 22 | pub const GAS_FOR_COMPUTE_CALL: Gas = 70_000_000_000_000; 23 | pub const GAS_FOR_COMPUTE_CALLBACK: Gas = 40_000_000_000_000; 24 | pub const GAS_FOR_SCHEDULE_CALL: Gas = 25_000_000_000_000; 25 | pub const GAS_FOR_SCHEDULE_CALLBACK: Gas = 5_000_000_000_000; 26 | pub const GAS_FOR_UPDATE_CALL: Gas = 15_000_000_000_000; 27 | pub const GAS_FOR_REMOVE_CALL: Gas = 20_000_000_000_000; 28 | pub const GAS_FOR_STATUS_CALL: Gas = 25_000_000_000_000; 29 | pub const GAS_FOR_STATUS_CALLBACK: Gas = 25_000_000_000_000; 30 | 31 | /// Error messages 32 | const ERR_ONLY_OWNER: &str = "Must be called by owner"; 33 | const ERR_NO_CRON_CONFIGURED: &str = "No cron account configured, cannot schedule"; 34 | const ERR_NO_TASK_CONFIGURED: &str = 35 | "No task hash found, need to schedule a cron task to set and get it."; 36 | 37 | #[derive(BorshDeserialize, BorshSerialize, Debug, Serialize, Deserialize, PartialEq)] 38 | #[serde(crate = "near_sdk::serde")] 39 | pub struct Task { 40 | pub contract_id: AccountId, 41 | pub function_id: String, 42 | pub cadence: String, 43 | pub recurring: bool, 44 | pub deposit: U128, 45 | pub gas: Gas, 46 | pub arguments: Vec, 47 | } 48 | 49 | #[ext_contract(ext_croncat)] 50 | pub trait ExtCroncat { 51 | fn get_slot_tasks(&self, offset: Option) -> (Vec, U128); 52 | fn get_tasks( 53 | &self, 54 | slot: Option, 55 | from_index: Option, 56 | limit: Option, 57 | ) -> Vec; 58 | // fn get_task(&self, task_hash: Base64VecU8) -> Task; 59 | fn get_task(&self, task_hash: String) -> Task; 60 | fn create_task( 61 | &mut self, 62 | contract_id: String, 63 | function_id: String, 64 | cadence: String, 65 | recurring: Option, 66 | deposit: Option, 67 | gas: Option, 68 | arguments: Option>, 69 | ) -> Base64VecU8; 70 | fn remove_task(&mut self, task_hash: Base64VecU8); 71 | fn proxy_call(&mut self); 72 | fn get_info( 73 | &mut self, 74 | ) -> ( 75 | bool, 76 | AccountId, 77 | U64, 78 | U64, 79 | [u64; 2], 80 | U128, 81 | U64, 82 | U64, 83 | U128, 84 | U128, 85 | U128, 86 | U128, 87 | U64, 88 | U64, 89 | U64, 90 | U128, 91 | ); 92 | } 93 | 94 | #[ext_contract(ext)] 95 | pub trait ExtCrossContract { 96 | fn schedule_callback( 97 | &mut self, 98 | #[callback] 99 | #[serializer(borsh)] 100 | task_hash: Base64VecU8, 101 | ); 102 | fn status_callback( 103 | &self, 104 | #[callback] 105 | #[serializer(borsh)] 106 | task: Option, 107 | ); 108 | fn compute_callback( 109 | &self, 110 | #[callback] 111 | #[serializer(borsh)] 112 | info: ( 113 | bool, 114 | AccountId, 115 | U64, 116 | U64, 117 | [u64; 2], 118 | U128, 119 | U64, 120 | U64, 121 | U128, 122 | U128, 123 | U128, 124 | U128, 125 | U64, 126 | U64, 127 | U64, 128 | U128, 129 | ), 130 | ); 131 | } 132 | 133 | // GOALs: 134 | // create a contract the has full cron CRUD operations managed within this contract 135 | // contract utility is sample idea of an indexer: keep track of info numbers in a "timeseries" 136 | 137 | // NOTE: The series could be updated to support OHLCV, Sums, MACD, etc... 138 | 139 | #[derive(BorshStorageKey, BorshSerialize)] 140 | pub enum StorageKeys { 141 | HourlyBalanceSeries, 142 | HourlyQueueSeries, 143 | HourlySlotsSeries, 144 | DailyBalanceSeries, 145 | DailyQueueSeries, 146 | DailySlotsSeries, 147 | } 148 | 149 | #[derive(Default, BorshDeserialize, BorshSerialize, Debug, Serialize, Deserialize)] 150 | #[serde(crate = "near_sdk::serde")] 151 | pub struct TickItem { 152 | t: u64, // point in time 153 | x: Option, // value at time 154 | y: Option, // value at time 155 | z: Option, // value at time 156 | } 157 | 158 | #[near_bindgen] 159 | #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] 160 | pub struct CrudContract { 161 | // tick: sums over 1hr of data, holding 30 days of hourly items 162 | hourly_balances: Vector, 163 | hourly_queues: Vector, 164 | hourly_slots: Vector, 165 | // tick: sums over 1 day of data, holding 1 year of daily items 166 | daily_balances: Vector, 167 | daily_queues: Vector, 168 | daily_slots: Vector, 169 | // Cron task hash, default will be running at the hourly scale 170 | task_hash: Option, 171 | // Cron manager account (manager_v1.croncat.near) 172 | cron: Option, 173 | } 174 | 175 | #[near_bindgen] 176 | impl CrudContract { 177 | /// ```bash 178 | /// near deploy --wasmFile ./res/cross_contract.wasm --accountId crosscontract.testnet --initFunction new --initArgs '{"cron": "cron.testnet"}' 179 | /// ``` 180 | #[init] 181 | pub fn new(cron: Option) -> Self { 182 | assert!(!env::state_exists(), "The contract is already initialized"); 183 | assert_eq!( 184 | env::current_account_id(), 185 | env::predecessor_account_id(), 186 | "{}", 187 | ERR_ONLY_OWNER 188 | ); 189 | 190 | CrudContract { 191 | hourly_balances: Vector::new(StorageKeys::HourlyBalanceSeries), 192 | hourly_queues: Vector::new(StorageKeys::HourlyQueueSeries), 193 | hourly_slots: Vector::new(StorageKeys::HourlySlotsSeries), 194 | daily_balances: Vector::new(StorageKeys::HourlyBalanceSeries), 195 | daily_queues: Vector::new(StorageKeys::HourlyQueueSeries), 196 | daily_slots: Vector::new(StorageKeys::HourlySlotsSeries), 197 | task_hash: None, 198 | cron, 199 | } 200 | } 201 | 202 | /// Returns the time series of data for hourly, daily 203 | /// 204 | /// ```bash 205 | /// near view crosscontract.testnet get_series 206 | /// ``` 207 | pub fn get_series( 208 | &self, 209 | ) -> ( 210 | Vec, 211 | Vec, 212 | Vec, 213 | Vec, 214 | Vec, 215 | Vec, 216 | ) { 217 | ( 218 | self.hourly_balances.to_vec(), 219 | self.hourly_queues.to_vec(), 220 | self.hourly_slots.to_vec(), 221 | self.daily_balances.to_vec(), 222 | self.daily_queues.to_vec(), 223 | self.daily_slots.to_vec(), 224 | ) 225 | } 226 | 227 | /// Compute: CrudContract Heartbeat 228 | /// Used to compute this time periods hourly/daily 229 | /// This fn can be called a varying intervals to compute rolling window time series data. 230 | /// 231 | /// ```bash 232 | /// near call crosscontract.testnet compute '{}' --accountId YOUR_ACCOUNT.testnet 233 | /// ``` 234 | pub fn compute(&mut self) -> Promise { 235 | ext_croncat::get_info( 236 | &self.cron.clone().expect(ERR_NO_CRON_CONFIGURED), 237 | env::attached_deposit(), 238 | GAS_FOR_SCHEDULE_CALL, 239 | ) 240 | .then(ext::compute_callback( 241 | &env::current_account_id(), 242 | NO_DEPOSIT, 243 | GAS_FOR_COMPUTE_CALLBACK, 244 | )) 245 | } 246 | 247 | /// Get the task hash, and store in state 248 | /// NOTE: This method helps contract understand remaining task balance, in case more is needed to continue running. 249 | /// NOTE: This could handle things about the task, or have logic about changing the task in some way. 250 | #[private] 251 | pub fn compute_callback( 252 | &mut self, 253 | #[callback] info: ( 254 | bool, 255 | AccountId, 256 | U64, 257 | U64, 258 | [u64; 2], 259 | U128, 260 | U64, 261 | U64, 262 | U128, 263 | U128, 264 | U128, 265 | U128, 266 | U64, 267 | U64, 268 | U64, 269 | U128, 270 | ), 271 | ) { 272 | // compute the current intervals 273 | let block_ts = env::block_timestamp(); 274 | let rem_threshold = 60_000; 275 | let rem_hour = core::cmp::max(block_ts % MILLISECONDS_IN_HOUR, 1); 276 | let rem_day = core::cmp::max(block_ts % MILLISECONDS_IN_DAY, 1); 277 | log!("REMS: {:?} {:?}", rem_hour, rem_day); 278 | log!( 279 | "LENS: {:?} {:?} {:?} {:?} {:?} {:?}", 280 | self.hourly_balances.len(), 281 | self.hourly_queues.len(), 282 | self.hourly_slots.len(), 283 | self.daily_balances.len(), 284 | self.daily_queues.len(), 285 | self.daily_slots.len(), 286 | ); 287 | 288 | // Le stuff frem le responsi 289 | let ( 290 | _, 291 | _, 292 | agent_active_queue, 293 | agent_pending_queue, 294 | _, 295 | _, 296 | slots, 297 | tasks, 298 | available_balance, 299 | staked_balance, 300 | _, 301 | _, 302 | _, 303 | slot_granularity, 304 | _, 305 | balance, 306 | ) = info; 307 | 308 | // get some data value, at a point in time 309 | // I chose a stupid value, but one that changes over time. This can be changed to account balances, token prices, anything that changes over time. 310 | let hour_balance = TickItem { 311 | t: block_ts / NANOS, 312 | x: Some(balance.0), 313 | y: Some(available_balance.0), 314 | z: Some(staked_balance.0), 315 | }; 316 | log!("New HR Balance: {:?}", hour_balance); 317 | 318 | // More ticks 319 | let hour_queue = TickItem { 320 | t: block_ts / NANOS, 321 | x: Some(agent_active_queue.0 as u128), 322 | y: Some(agent_pending_queue.0 as u128), 323 | z: None, 324 | }; 325 | let hour_slots = TickItem { 326 | t: block_ts / NANOS, 327 | x: Some(slots.0 as u128), 328 | y: Some(tasks.0 as u128), 329 | z: Some(slot_granularity.0 as u128), 330 | }; 331 | 332 | // compute for each interval match, made a small buffer window to make sure the computed value doesnt get computed too far out of range 333 | self.hourly_balances.push(&hour_balance); 334 | self.hourly_queues.push(&hour_queue); 335 | self.hourly_slots.push(&hour_slots); 336 | 337 | // trim to max 338 | if self.hourly_balances.len() > 744 { 339 | // 31 days of hours (24*31) 340 | // TODO: Change this to unshift lol 341 | self.hourly_balances.pop(); 342 | self.hourly_queues.pop(); 343 | self.hourly_slots.pop(); 344 | } 345 | 346 | // daily average across last 1hr of data including NEW 347 | if rem_day <= rem_threshold { 348 | // 86_400_000 349 | let total_day_ticks: u64 = 24; 350 | let end_index = self.daily_balances.len(); 351 | let start_index = end_index - total_day_ticks; 352 | let mut hour_balance_tick = TickItem { 353 | t: block_ts / NANOS, 354 | x: Some(0), 355 | y: Some(0), 356 | z: Some(0), 357 | }; 358 | let mut hour_queue_tick = TickItem { 359 | t: block_ts / NANOS, 360 | x: Some(0), 361 | y: Some(0), 362 | z: None, 363 | }; 364 | let mut hour_slots_tick = TickItem { 365 | t: block_ts / NANOS, 366 | x: Some(0), 367 | y: Some(0), 368 | z: Some(0), 369 | }; 370 | 371 | // minus 1 for current number above 372 | for i in start_index..end_index { 373 | if let Some(tick) = self.daily_balances.get(i) { 374 | // Aggregate tick numbers 375 | hour_balance_tick.x = if tick.x.is_some() { 376 | Some(hour_balance_tick.x.unwrap_or(0) + tick.x.unwrap_or(0)) 377 | } else { 378 | hour_balance_tick.x 379 | }; 380 | hour_balance_tick.y = if tick.y.is_some() { 381 | Some(hour_balance_tick.y.unwrap_or(0) + tick.y.unwrap_or(0)) 382 | } else { 383 | hour_balance_tick.y 384 | }; 385 | hour_balance_tick.z = if tick.z.is_some() { 386 | Some(hour_balance_tick.z.unwrap_or(0) + tick.z.unwrap_or(0)) 387 | } else { 388 | hour_balance_tick.z 389 | }; 390 | }; 391 | if let Some(tick) = self.hourly_queues.get(i) { 392 | // Aggregate tick numbers 393 | hour_queue_tick.x = if tick.x.is_some() { 394 | Some(hour_queue_tick.x.unwrap_or(0) + tick.x.unwrap_or(0)) 395 | } else { 396 | hour_queue_tick.x 397 | }; 398 | hour_queue_tick.y = if tick.y.is_some() { 399 | Some(hour_queue_tick.y.unwrap_or(0) + tick.y.unwrap_or(0)) 400 | } else { 401 | hour_queue_tick.y 402 | }; 403 | }; 404 | if let Some(tick) = self.hourly_slots.get(i) { 405 | // Aggregate tick numbers 406 | hour_slots_tick.x = if tick.x.is_some() { 407 | Some(hour_slots_tick.x.unwrap_or(0) + tick.x.unwrap_or(0)) 408 | } else { 409 | hour_slots_tick.x 410 | }; 411 | hour_slots_tick.y = if tick.y.is_some() { 412 | Some(hour_slots_tick.y.unwrap_or(0) + tick.y.unwrap_or(0)) 413 | } else { 414 | hour_slots_tick.y 415 | }; 416 | hour_slots_tick.z = if tick.z.is_some() { 417 | Some(hour_slots_tick.z.unwrap_or(0) + tick.z.unwrap_or(0)) 418 | } else { 419 | hour_slots_tick.z 420 | }; 421 | }; 422 | } 423 | 424 | self.daily_balances.push(&hour_balance_tick); 425 | self.daily_balances.push(&hour_queue_tick); 426 | self.daily_balances.push(&hour_slots_tick); 427 | 428 | // trim to max 429 | if end_index > 1825 { 430 | // 5 years of days (365*5) 431 | self.daily_balances.pop(); 432 | } 433 | } 434 | } 435 | 436 | /// Create a new scheduled task, registering the "compute" method with croncat 437 | /// 438 | /// ```bash 439 | /// near call crosscontract.testnet schedule '{ "function_id": "compute", "period": "0 0 * * * *" }' --accountId YOUR_ACCOUNT.testnet 440 | /// ``` 441 | #[payable] 442 | pub fn schedule(&mut self, function_id: String, period: String) -> Promise { 443 | assert_eq!( 444 | env::current_account_id(), 445 | env::predecessor_account_id(), 446 | "{}", 447 | ERR_ONLY_OWNER 448 | ); 449 | // NOTE: Could check that the balance supplied is enough to cover XX task calls. 450 | 451 | ext_croncat::create_task( 452 | env::current_account_id(), 453 | function_id, 454 | period, 455 | Some(true), 456 | Some(U128::from(NO_DEPOSIT)), 457 | Some(GAS_FOR_COMPUTE_CALL), // 30 Tgas 458 | None, 459 | &self.cron.clone().expect(ERR_NO_CRON_CONFIGURED), 460 | env::attached_deposit(), 461 | GAS_FOR_SCHEDULE_CALL, 462 | ) 463 | .then(ext::schedule_callback( 464 | &env::current_account_id(), 465 | NO_DEPOSIT, 466 | GAS_FOR_SCHEDULE_CALLBACK, 467 | )) 468 | } 469 | 470 | /// Get the task hash, and store in state 471 | #[private] 472 | pub fn schedule_callback(&mut self, #[callback] task_hash: Base64VecU8) { 473 | log!("schedule_callback task_hash {:?}", &task_hash); 474 | self.task_hash = Some(task_hash); 475 | } 476 | 477 | /// Remove a scheduled task using a known hash. MUST be owner! 478 | /// 479 | /// ```bash 480 | /// near call crosscontract.testnet remove '{}' --accountId YOUR_ACCOUNT.testnet 481 | /// ``` 482 | pub fn remove(&mut self) -> Promise { 483 | assert_eq!( 484 | env::current_account_id(), 485 | env::predecessor_account_id(), 486 | "{}", 487 | ERR_ONLY_OWNER 488 | ); 489 | let task_hash = self.task_hash.clone().expect(ERR_NO_TASK_CONFIGURED); 490 | self.task_hash = None; 491 | 492 | ext_croncat::remove_task( 493 | task_hash, 494 | &self.cron.clone().expect(ERR_NO_CRON_CONFIGURED), 495 | NO_DEPOSIT, 496 | GAS_FOR_REMOVE_CALL, 497 | ) 498 | } 499 | 500 | /// Get the task status, including remaining balance & etc. 501 | /// Useful for automated on-chain task management! This method could be scheduled as well, and manage re-funding tasks or changing tasks on new data. 502 | /// 503 | /// ```bash 504 | /// near call crosscontract.testnet status 505 | /// ``` 506 | pub fn status(&self) -> Promise { 507 | // NOTE: fix this! serialization is not working 508 | let hash = self.task_hash.clone().expect(ERR_NO_TASK_CONFIGURED); 509 | log!( 510 | "TASK HASH: {:?} {:?} {}", 511 | &hash, 512 | serde_json::to_string(&hash).unwrap(), 513 | serde_json::to_string(&hash).unwrap() 514 | ); 515 | ext_croncat::get_task( 516 | // hash, 517 | serde_json::to_string(&hash).unwrap().to_string(), 518 | &self.cron.clone().expect(ERR_NO_CRON_CONFIGURED), 519 | NO_DEPOSIT, 520 | GAS_FOR_STATUS_CALL, 521 | ) 522 | .then(ext::schedule_callback( 523 | &env::current_account_id(), 524 | NO_DEPOSIT, 525 | GAS_FOR_STATUS_CALLBACK, 526 | )) 527 | } 528 | 529 | /// Get the task hash, and store in state 530 | /// NOTE: This method helps contract understand remaining task balance, in case more is needed to continue running. 531 | /// NOTE: This could handle things about the task, or have logic about changing the task in some way. 532 | #[private] 533 | pub fn status_callback(&self, #[callback] task: Option) -> Option { 534 | // NOTE: Check remaining balance here 535 | // NOTE: Could have logic to another callback IF the balance is running low 536 | task 537 | } 538 | 539 | /// Get the stats! 540 | /// 541 | /// ```bash 542 | /// near call crosscontract.testnet status 543 | /// ``` 544 | pub fn stats(&self) -> (u64, u64, Option, Option) { 545 | ( 546 | self.hourly_balances.len(), 547 | self.daily_balances.len(), 548 | self.task_hash.clone(), 549 | self.cron.clone(), 550 | ) 551 | } 552 | } 553 | 554 | // NOTE: Im sorry, i didnt have time for adding tests. 555 | // DO YOU? If so, get a bounty reward: https://github.com/Cron-Near/bounties 556 | // 557 | // // use the attribute below for unit tests 558 | // #[cfg(test)] 559 | // mod tests { 560 | // use super::*; 561 | // use near_sdk::MockedBlockchain; 562 | // use near_sdk::{testing_env, VMContext}; 563 | // } 564 | -------------------------------------------------------------------------------- /examples/views/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-C", "link-args=-s"] -------------------------------------------------------------------------------- /examples/views/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "views" 3 | version = "0.0.1" 4 | authors = ["Cron.cat", "@trevorjtclarke"] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | near-sdk = "3.1.0" 12 | -------------------------------------------------------------------------------- /examples/views/src/lib.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::{ 2 | borsh::{self, BorshDeserialize, BorshSerialize}, 3 | env, 4 | json_types::Base64VecU8, 5 | near_bindgen, 6 | }; 7 | 8 | near_sdk::setup_alloc!(); 9 | 10 | pub const INTERVAL: u64 = 2; // Check if EVEN number minute 11 | pub const ONE_MINUTE: u64 = 60_000_000_000; // 60 seconds in nanos 12 | 13 | pub type CroncatTriggerResponse = (bool, Option); 14 | 15 | #[near_bindgen] 16 | #[derive(Default, BorshDeserialize, BorshSerialize)] 17 | pub struct Views {} 18 | 19 | #[near_bindgen] 20 | impl Views { 21 | /// Get configured interval 22 | /// 23 | /// ```bash 24 | /// near view views.testnet get_interval 25 | /// ``` 26 | pub fn get_interval() -> u64 { 27 | return INTERVAL; 28 | } 29 | 30 | /// Get a boolean that represents underlying logic to execute an action 31 | /// Think of this as the entry point to "IF THIS, THEN THAT" where "IF THIS" is _this_ function. 32 | /// 33 | /// ```bash 34 | /// near view views.testnet get_a_boolean 35 | /// ``` 36 | pub fn get_a_boolean(&self) -> CroncatTriggerResponse { 37 | let current_block_ts = env::block_timestamp(); 38 | let remainder = current_block_ts % ONE_MINUTE; 39 | let fixed_block = current_block_ts.saturating_sub(remainder); 40 | 41 | // modulo check 42 | (fixed_block % (INTERVAL * ONE_MINUTE) == 0, None) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /manager/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-C", "link-args=-s"] -------------------------------------------------------------------------------- /manager/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "manager" 3 | version = "0.5.0" 4 | authors = ["cron.cat", "@trevorjtclarke", "@mikedotexe"] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | near-sdk = "3.1.0" 12 | cron_schedule = "0.2.0" 13 | near-contract-standards = "3.2.0" 14 | 15 | [dev-dependencies] 16 | near-sdk-sim = "3.1.0" 17 | chrono = "~0.4" 18 | near-primitives-core = "0.4.0" -------------------------------------------------------------------------------- /manager/src/agent.rs: -------------------------------------------------------------------------------- 1 | use near_contract_standards::storage_management::StorageManagement; 2 | 3 | use crate::*; 4 | 5 | #[derive(BorshDeserialize, BorshSerialize, Debug, Serialize, Deserialize, PartialEq)] 6 | #[serde(crate = "near_sdk::serde")] 7 | pub enum AgentStatus { 8 | // Default for any new agent, if tasks ratio allows 9 | Active, 10 | 11 | // Default for any new agent, until more tasks come online 12 | Pending, 13 | } 14 | 15 | #[derive(BorshDeserialize, BorshSerialize, Debug, Serialize, Deserialize, PartialEq)] 16 | #[serde(crate = "near_sdk::serde")] 17 | pub struct Agent { 18 | pub status: AgentStatus, 19 | 20 | // Where rewards get transferred 21 | pub payable_account_id: AccountId, 22 | 23 | // accrued reward balance 24 | pub balance: U128, 25 | 26 | // stats 27 | pub total_tasks_executed: U128, 28 | 29 | // Holds slot number of a missed slot. 30 | // If other agents see an agent miss a slot, they store the missed slot number. 31 | // If agent does a task later, this number is reset to zero. 32 | // Example data: 1633890060000000000 or 0 33 | pub last_missed_slot: u128, 34 | } 35 | 36 | #[near_bindgen] 37 | impl Contract { 38 | /// Add any account as an agent that will be able to execute tasks. 39 | /// Registering allows for rewards accruing with micro-payments which will accumulate to more long-term. 40 | /// 41 | /// Optional Parameters: 42 | /// "payable_account_id" - Allows a different account id to be specified, so a user can receive funds at a different account than the agent account. 43 | /// 44 | /// ```bash 45 | /// near call manager_v1.croncat.testnet register_agent '{"payable_account_id": "YOU.testnet"}' --accountId YOUR_AGENT.testnet 46 | /// ``` 47 | #[payable] 48 | pub fn register_agent(&mut self, payable_account_id: Option) { 49 | assert_eq!(self.paused, false, "Register agent paused"); 50 | 51 | let deposit: Balance = env::attached_deposit(); 52 | let required_deposit: Balance = 53 | Balance::from(self.agent_storage_usage) * env::storage_byte_cost(); 54 | 55 | assert!( 56 | deposit >= required_deposit, 57 | "Insufficient deposit. Please deposit {} yoctoⓃ to register an agent.", 58 | required_deposit.clone() 59 | ); 60 | 61 | let account = env::predecessor_account_id(); 62 | // check that account isn't already added 63 | if let Some(agent) = self.agents.get(&account) { 64 | let panic_msg = format!("Agent already exists: {:?}. Refunding the deposit.", agent); 65 | env::panic(panic_msg.as_bytes()); 66 | }; 67 | 68 | let payable_id = payable_account_id 69 | .map(|a| a.into()) 70 | .unwrap_or_else(|| env::predecessor_account_id()); 71 | 72 | let total_agents = self.agent_active_queue.len(); 73 | let agent_status = if total_agents == 0 { 74 | self.agent_active_queue.push(&account); 75 | AgentStatus::Active 76 | } else { 77 | self.agent_pending_queue.push(&account); 78 | AgentStatus::Pending 79 | }; 80 | 81 | let agent = Agent { 82 | status: agent_status, 83 | payable_account_id: payable_id, 84 | balance: U128::from(required_deposit), 85 | total_tasks_executed: U128::from(0), 86 | last_missed_slot: 0, 87 | }; 88 | 89 | self.agents.insert(&account, &agent); 90 | self.available_balance = self.available_balance.saturating_add(required_deposit); 91 | 92 | // If the user deposited more than needed, refund them. 93 | let refund = deposit - required_deposit; 94 | if refund > 0 { 95 | Promise::new(env::predecessor_account_id()).transfer(refund); 96 | } 97 | } 98 | 99 | /// Update agent details, specifically the payable account id for an agent. 100 | /// 101 | /// ```bash 102 | /// near call manager_v1.croncat.testnet update_agent '{"payable_account_id": "YOU.testnet"}' --accountId YOUR_AGENT.testnet 103 | /// ``` 104 | #[payable] 105 | pub fn update_agent(&mut self, payable_account_id: Option) { 106 | assert_eq!(self.paused, false, "Update agent paused"); 107 | assert_one_yocto(); 108 | 109 | let account = env::predecessor_account_id(); 110 | 111 | // check that predecessor agent exists 112 | if let Some(mut agent) = self.agents.get(&account) { 113 | if payable_account_id.is_some() { 114 | agent.payable_account_id = payable_account_id.unwrap().into(); 115 | self.agents.insert(&account, &agent); 116 | } 117 | } else { 118 | panic!("Agent must register"); 119 | }; 120 | 121 | // If the user deposited more than needed, refund them. 122 | let yocto: Balance = 1; 123 | let refund = env::attached_deposit() - yocto; 124 | self.available_balance = self.available_balance.saturating_add(yocto); 125 | if refund > 0 { 126 | Promise::new(env::predecessor_account_id()).transfer(refund); 127 | } 128 | } 129 | 130 | /// Removes the agent from the active set of agents. 131 | /// Withdraws all reward balances to the agent payable account id. 132 | /// Requires attaching 1 yoctoⓃ ensure it comes from a full-access key. 133 | /// 134 | /// ```bash 135 | /// near call manager_v1.croncat.testnet unregister_agent --accountId YOUR_AGENT.testnet 136 | /// ``` 137 | #[payable] 138 | pub fn unregister_agent(&mut self) { 139 | // This method name is quite explicit, so calling storage_unregister and setting the 'force' option to true. 140 | self.storage_unregister(Some(true)); 141 | } 142 | 143 | /// Removes the agent from the active set of agents. 144 | /// Withdraws all reward balances to the agent payable account id. 145 | #[private] 146 | pub fn exit_agent(&mut self, account_id: Option, remove: Option) -> Promise { 147 | let account = account_id.unwrap_or_else(env::predecessor_account_id); 148 | let storage_fee = self.agent_storage_usage as u128 * env::storage_byte_cost(); 149 | 150 | // check that signer agent exists 151 | if let Some(mut agent) = self.agents.get(&account) { 152 | let agent_balance = agent.balance.0; 153 | // If remove is present, still allow exiting of only storage balance agent 154 | if remove.is_none() { 155 | assert!( 156 | agent_balance > storage_fee, 157 | "No Agent balance beyond the storage balance" 158 | ); 159 | } 160 | let withdrawal_amount = agent_balance - storage_fee; 161 | agent.balance = U128::from(agent_balance - withdrawal_amount); 162 | 163 | // if this is a full exit, remove agent. Otherwise, update agent 164 | if let Some(remove) = remove { 165 | if remove { 166 | self.remove_agent(account); 167 | } 168 | } else { 169 | self.agents.insert(&account, &agent); 170 | } 171 | 172 | log!("Withdrawal of {} has been sent.", withdrawal_amount); 173 | self.available_balance = self.available_balance.saturating_sub(withdrawal_amount); 174 | Promise::new(agent.payable_account_id.to_string()).transfer(withdrawal_amount) 175 | } else { 176 | env::panic(b"No Agent") 177 | } 178 | } 179 | 180 | /// Removes the agent from the active & pending set of agents. 181 | // NOTE: swap_remove takes last element in vector and replaces index removed, so potentially FIFO agent lists can get out of order for pending queue. Not exactly "fair". Could change to use "replace", if storage write is not too expensive with large lists. 182 | // TODO: Check the state changes! getting: Smart contract panicked: The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly? 183 | #[private] 184 | pub fn remove_agent(&mut self, account_id: AccountId) { 185 | self.agents.remove(&account_id); 186 | // remove agent from agent_active_queue 187 | let index = self.agent_active_queue.iter().position(|x| x == account_id); 188 | if let Some(index) = index { 189 | self.agent_active_queue.swap_remove(index as u64); 190 | } 191 | // remove agent from agent_pending_queue 192 | let p_index = self 193 | .agent_pending_queue 194 | .iter() 195 | .position(|x| x == account_id); 196 | if let Some(p_index) = p_index { 197 | self.agent_pending_queue.swap_remove(p_index as u64); 198 | } 199 | } 200 | 201 | /// Allows an agent to withdraw all rewards, paid to the specified payable account id. 202 | /// 203 | /// ```bash 204 | /// near call manager_v1.croncat.testnet withdraw_task_balance --accountId YOUR_AGENT.testnet 205 | /// ``` 206 | pub fn withdraw_task_balance(&mut self) -> Promise { 207 | self.exit_agent(None, None) 208 | } 209 | 210 | /// Gets the agent data stats 211 | /// 212 | /// ```bash 213 | /// near view manager_v1.croncat.testnet get_agent '{"account_id": "YOUR_AGENT.testnet"}' 214 | /// ``` 215 | pub fn get_agent(&self, account_id: AccountId) -> Option { 216 | self.agents.get(&account_id) 217 | } 218 | } 219 | 220 | #[cfg(test)] 221 | mod tests { 222 | use super::*; 223 | use near_sdk::json_types::ValidAccountId; 224 | use near_sdk::test_utils::{accounts, VMContextBuilder}; 225 | use near_sdk::{testing_env, MockedBlockchain}; 226 | 227 | const BLOCK_START_BLOCK: u64 = 52_201_040; 228 | const BLOCK_START_TS: u64 = 1_624_151_503_447_000_000; 229 | const AGENT_REGISTRATION_COST: u128 = 2_260_000_000_000_000_000_000; 230 | 231 | fn get_context(predecessor_account_id: ValidAccountId) -> VMContextBuilder { 232 | let mut builder = VMContextBuilder::new(); 233 | builder 234 | .current_account_id(accounts(0)) 235 | .signer_account_id(predecessor_account_id.clone()) 236 | .signer_account_pk(b"ed25519:4ZhGmuKTfQn9ZpHCQVRwEr4JnutL8Uu3kArfxEqksfVM".to_vec()) 237 | .predecessor_account_id(predecessor_account_id) 238 | .block_index(BLOCK_START_BLOCK) 239 | .block_timestamp(BLOCK_START_TS); 240 | builder 241 | } 242 | 243 | #[test] 244 | fn test_agent_register_check() { 245 | let mut context = get_context(accounts(1)); 246 | testing_env!(context.build()); 247 | let contract = Contract::new(); 248 | testing_env!(context.is_view(true).build()); 249 | assert!(contract.get_agent(accounts(1).to_string()).is_none()); 250 | } 251 | 252 | #[test] 253 | fn test_agent_register_new() { 254 | let mut context = get_context(accounts(1)); 255 | context.attached_deposit(AGENT_REGISTRATION_COST); 256 | testing_env!(context.is_view(false).build()); 257 | let mut contract = Contract::new(); 258 | contract.register_agent(Some(accounts(1))); 259 | 260 | testing_env!(context.is_view(true).build()); 261 | let _agent = contract.get_agent(accounts(1).to_string()); 262 | assert_eq!( 263 | contract.get_agent(accounts(1).to_string()), 264 | Some(Agent { 265 | status: AgentStatus::Active, 266 | payable_account_id: accounts(1).to_string(), 267 | balance: U128::from(AGENT_REGISTRATION_COST), 268 | total_tasks_executed: U128::from(0), 269 | last_missed_slot: 0, 270 | }) 271 | ); 272 | } 273 | 274 | #[test] 275 | #[should_panic(expected = "Agent must register")] 276 | fn test_agent_update_check() { 277 | let mut context = get_context(accounts(1)); 278 | context.attached_deposit(1); 279 | testing_env!(context.build()); 280 | let mut contract = Contract::new(); 281 | contract.update_agent(None); 282 | contract.update_agent(Some(accounts(2))); 283 | } 284 | 285 | #[test] 286 | fn test_agent_update() { 287 | let mut context = get_context(accounts(1)); 288 | context.attached_deposit(AGENT_REGISTRATION_COST); 289 | testing_env!(context.is_view(false).build()); 290 | let mut contract = Contract::new(); 291 | contract.register_agent(Some(accounts(1))); 292 | context.attached_deposit(1); 293 | testing_env!(context.build()); 294 | contract.update_agent(Some(accounts(2))); 295 | 296 | testing_env!(context.is_view(true).build()); 297 | let _agent = contract.get_agent(accounts(1).to_string()); 298 | assert_eq!( 299 | contract.get_agent(accounts(1).to_string()), 300 | Some(Agent { 301 | status: AgentStatus::Active, 302 | payable_account_id: accounts(2).to_string(), 303 | balance: U128::from(AGENT_REGISTRATION_COST), 304 | total_tasks_executed: U128::from(0), 305 | last_missed_slot: 0, 306 | }) 307 | ); 308 | } 309 | 310 | #[test] 311 | fn test_agent_unregister_no_balance() { 312 | let mut context = get_context(accounts(1)); 313 | context.attached_deposit(AGENT_REGISTRATION_COST); 314 | testing_env!(context.is_view(false).build()); 315 | let mut contract = Contract::new(); 316 | contract.register_agent(Some(accounts(1))); 317 | context.attached_deposit(1); 318 | testing_env!(context.build()); 319 | contract.unregister_agent(); 320 | 321 | testing_env!(context.is_view(true).build()); 322 | let _agent = contract.get_agent(accounts(1).to_string()); 323 | assert_eq!(contract.get_agent(accounts(1).to_string()), None); 324 | } 325 | 326 | #[test] 327 | #[should_panic(expected = "No Agent")] 328 | fn test_agent_withdraw_check() { 329 | let context = get_context(accounts(3)); 330 | testing_env!(context.build()); 331 | let mut contract = Contract::new(); 332 | contract.withdraw_task_balance(); 333 | } 334 | 335 | #[test] 336 | fn agent_storage_check() { 337 | let context = get_context(accounts(1)); 338 | testing_env!(context.build()); 339 | let contract = Contract::new(); 340 | assert_eq!( 341 | 226, contract.agent_storage_usage, 342 | "Expected different storage usage for the agent." 343 | ); 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /manager/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use agent::Agent; 2 | use cron_schedule::Schedule; 3 | use near_sdk::{ 4 | assert_one_yocto, 5 | borsh::{self, BorshDeserialize, BorshSerialize}, 6 | collections::{LookupMap, TreeMap, UnorderedMap, Vector}, 7 | env, 8 | json_types::{Base64VecU8, ValidAccountId, U128, U64}, 9 | log, near_bindgen, 10 | serde::{Deserialize, Serialize}, 11 | serde_json::json, 12 | AccountId, Balance, BorshStorageKey, Gas, PanicOnDefault, Promise, PromiseResult, StorageUsage, 13 | }; 14 | use std::str::FromStr; 15 | pub use tasks::Task; 16 | pub use tasks::TaskHumanFriendly; 17 | pub use triggers::Trigger; 18 | 19 | mod agent; 20 | mod owner; 21 | mod storage_impl; 22 | mod tasks; 23 | mod triggers; 24 | mod utils; 25 | mod views; 26 | 27 | near_sdk::setup_alloc!(); 28 | 29 | // Balance & Fee Definitions 30 | pub const ONE_NEAR: u128 = 1_000_000_000_000_000_000_000_000; 31 | pub const BASE_BALANCE: Balance = ONE_NEAR * 5; // safety overhead 32 | pub const GAS_BASE_PRICE: Balance = 100_000_000; 33 | pub const GAS_BASE_FEE: Gas = 3_000_000_000_000; 34 | // actual is: 13534954161128, higher in case treemap rebalance 35 | pub const GAS_FOR_CALLBACK: Gas = 30_000_000_000_000; 36 | pub const AGENT_BASE_FEE: Balance = 500_000_000_000_000_000_000; // 0.0005 Ⓝ (2000 tasks = 1 Ⓝ) 37 | pub const STAKE_BALANCE_MIN: u128 = 10 * ONE_NEAR; 38 | 39 | // Boundary Definitions 40 | pub const MAX_BLOCK_TS_RANGE: u64 = 1_000_000_000_000_000_000; 41 | pub const SLOT_GRANULARITY: u64 = 60_000_000_000; // 60 seconds in nanos 42 | pub const AGENT_EJECT_THRESHOLD: u128 = 600; // how many slots an agent can miss before being ejected. 10 * 60 = 1hr 43 | pub const NANO: u64 = 1_000_000_000; 44 | 45 | #[derive(BorshStorageKey, BorshSerialize)] 46 | pub enum StorageKeys { 47 | Tasks, 48 | Agents, 49 | Slots, 50 | AgentsActive, 51 | AgentsPending, 52 | Triggers, 53 | TaskOwners, 54 | } 55 | 56 | #[near_bindgen] 57 | #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] 58 | // #[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, PanicOnDefault)] 59 | // #[serde(crate = "near_sdk::serde")] 60 | pub struct Contract { 61 | // Runtime 62 | paused: bool, 63 | owner_id: AccountId, 64 | treasury_id: Option, 65 | 66 | // Agent management 67 | agents: LookupMap, 68 | agent_active_queue: Vector, 69 | agent_pending_queue: Vector, 70 | // The ratio of tasks to agents, where index 0 is agents, index 1 is tasks 71 | // Example: [1, 10] 72 | // Explanation: For every 1 agent, 10 tasks per slot are available. 73 | // NOTE: Caveat, when there are odd number of tasks or agents, the overflow will be available to first-come first-serve. This doesnt negate the possibility of a failed txn from race case choosing winner inside a block. 74 | // NOTE: The overflow will be adjusted to be handled by sweeper in next implementation. 75 | agent_task_ratio: [u64; 2], 76 | agent_active_index: u64, 77 | agents_eject_threshold: u128, 78 | 79 | // Basic management 80 | slots: TreeMap>>, 81 | tasks: UnorderedMap, Task>, 82 | task_owners: UnorderedMap>>, 83 | triggers: UnorderedMap, Trigger>, 84 | 85 | // Economics 86 | available_balance: Balance, // tasks + rewards balance 87 | staked_balance: Balance, 88 | agent_fee: Balance, 89 | gas_price: Balance, 90 | proxy_callback_gas: Gas, 91 | slot_granularity: u64, 92 | 93 | // Storage 94 | agent_storage_usage: StorageUsage, 95 | trigger_storage_usage: StorageUsage, 96 | } 97 | 98 | // TODO: Setup state migration for tasks/triggers, including initial storage calculation 99 | #[near_bindgen] 100 | impl Contract { 101 | /// ```bash 102 | /// near call manager_v1.croncat.testnet new --accountId manager_v1.croncat.testnet 103 | /// ``` 104 | #[init] 105 | pub fn new() -> Self { 106 | let mut this = Contract { 107 | paused: false, 108 | owner_id: env::signer_account_id(), 109 | treasury_id: None, 110 | tasks: UnorderedMap::new(StorageKeys::Tasks), 111 | task_owners: UnorderedMap::new(StorageKeys::TaskOwners), 112 | triggers: UnorderedMap::new(StorageKeys::Triggers), 113 | agents: LookupMap::new(StorageKeys::Agents), 114 | agent_active_queue: Vector::new(StorageKeys::AgentsActive), 115 | agent_pending_queue: Vector::new(StorageKeys::AgentsPending), 116 | agent_task_ratio: [1, 2], 117 | agent_active_index: 0, 118 | agents_eject_threshold: AGENT_EJECT_THRESHOLD, 119 | slots: TreeMap::new(StorageKeys::Slots), 120 | available_balance: 0, 121 | staked_balance: 0, 122 | agent_fee: AGENT_BASE_FEE, 123 | gas_price: GAS_BASE_PRICE, 124 | proxy_callback_gas: GAS_FOR_CALLBACK, 125 | slot_granularity: SLOT_GRANULARITY, 126 | agent_storage_usage: 0, 127 | trigger_storage_usage: 0, 128 | }; 129 | this.measure_account_storage_usage(); 130 | this 131 | } 132 | 133 | /// Measure the storage an agent will take and need to provide 134 | fn measure_account_storage_usage(&mut self) { 135 | let initial_storage_usage = env::storage_usage(); 136 | let max_len_string = "a".repeat(64); 137 | 138 | // Create a temporary, dummy entry and measure the storage used. 139 | let tmp_agent = Agent { 140 | status: agent::AgentStatus::Pending, 141 | payable_account_id: max_len_string.clone(), 142 | balance: U128::from(0), 143 | total_tasks_executed: U128::from(0), 144 | last_missed_slot: 0, 145 | }; 146 | self.agents.insert(&max_len_string, &tmp_agent); 147 | self.agent_storage_usage = env::storage_usage() - initial_storage_usage; 148 | // Remove the temporary entry. 149 | self.agents.remove(&max_len_string); 150 | 151 | // Calc the trigger storage needs 152 | let tmp_hash = max_len_string.clone().try_to_vec().unwrap(); 153 | let tmp_trigger = Trigger { 154 | owner_id: max_len_string.clone(), 155 | contract_id: max_len_string.clone(), 156 | function_id: max_len_string.clone(), 157 | task_hash: Base64VecU8::from(tmp_hash.clone()), 158 | arguments: Base64VecU8::from("a".repeat(1024).try_to_vec().unwrap()), 159 | }; 160 | self.triggers.insert(&tmp_hash, &tmp_trigger); 161 | self.trigger_storage_usage = env::storage_usage() - initial_storage_usage; 162 | // Remove the temporary entry. 163 | self.triggers.remove(&tmp_hash); 164 | } 165 | 166 | /// Takes an optional `offset`: the number of seconds to offset from now (current block timestamp) 167 | /// If no offset, returns current slot based on current block timestamp 168 | /// If offset, returns next slot based on current block timestamp & seconds offset 169 | fn get_slot_id(&self, offset: Option) -> u128 { 170 | let current_block_ts = env::block_timestamp(); 171 | 172 | let slot_id: u64 = if let Some(o) = offset { 173 | // NOTE: Assumption here is that the offset will be in seconds. (60 seconds per slot) 174 | let next = current_block_ts + (self.slot_granularity + o); 175 | 176 | // Protect against extreme future block schedules 177 | u64::min(next, current_block_ts + MAX_BLOCK_TS_RANGE) 178 | } else { 179 | current_block_ts 180 | }; 181 | 182 | // rounded to nearest granularity 183 | let slot_remainder = slot_id % self.slot_granularity; 184 | let slot_id_round = slot_id.saturating_sub(slot_remainder); 185 | 186 | u128::from(slot_id_round) 187 | } 188 | 189 | /// Parse cadence into a schedule 190 | /// Get next approximate block from a schedule 191 | /// return slot from the difference of upcoming block and current block 192 | fn get_slot_from_cadence(&self, cadence: String) -> u128 { 193 | let current_block_ts = env::block_timestamp(); // NANOS 194 | 195 | // Schedule params 196 | // NOTE: eventually use TryFrom 197 | let schedule = Schedule::from_str(&cadence).unwrap(); 198 | let next_ts = schedule.next_after(¤t_block_ts).unwrap(); 199 | let next_diff = next_ts - current_block_ts; 200 | 201 | // Get the next slot, based on the timestamp differences 202 | let current = self.get_slot_id(None); 203 | let next_slot = self.get_slot_id(Some(next_diff)); 204 | 205 | if current == next_slot { 206 | // Add slot granularity to make sure the minimum next slot is a block within next slot granularity range 207 | current + u128::from(self.slot_granularity) 208 | } else { 209 | next_slot 210 | } 211 | } 212 | } 213 | 214 | #[cfg(test)] 215 | mod tests { 216 | use super::*; 217 | use near_sdk::json_types::ValidAccountId; 218 | use near_sdk::test_utils::{accounts, VMContextBuilder}; 219 | use near_sdk::{testing_env, MockedBlockchain}; 220 | 221 | const BLOCK_START_BLOCK: u64 = 52_201_040; 222 | const BLOCK_START_TS: u64 = 1_624_151_503_447_000_000; 223 | 224 | fn get_context(predecessor_account_id: ValidAccountId) -> VMContextBuilder { 225 | let mut builder = VMContextBuilder::new(); 226 | builder 227 | .current_account_id(accounts(0)) 228 | .signer_account_id(predecessor_account_id.clone()) 229 | .signer_account_pk(b"ed25519:4ZhGmuKTfQn9ZpHCQVRwEr4JnutL8Uu3kArfxEqksfVM".to_vec()) 230 | .predecessor_account_id(predecessor_account_id) 231 | .block_index(BLOCK_START_BLOCK) 232 | .block_timestamp(BLOCK_START_TS); 233 | builder 234 | } 235 | 236 | #[test] 237 | fn test_contract_new() { 238 | let mut context = get_context(accounts(1)); 239 | testing_env!(context.build()); 240 | let contract = Contract::new(); 241 | testing_env!(context.is_view(true).build()); 242 | assert!(contract.get_tasks(None, None, None).is_empty()); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /manager/src/owner.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | /// Changes core configurations 6 | /// Should only be updated by owner -- in best case DAO based :) 7 | pub fn update_settings( 8 | &mut self, 9 | owner_id: Option, 10 | slot_granularity: Option, 11 | paused: Option, 12 | agent_fee: Option, 13 | gas_price: Option, 14 | proxy_callback_gas: Option, 15 | agent_task_ratio: Option>, 16 | agents_eject_threshold: Option, 17 | treasury_id: Option, 18 | ) { 19 | assert_eq!( 20 | self.owner_id, 21 | env::predecessor_account_id(), 22 | "Must be owner" 23 | ); 24 | 25 | // BE CAREFUL! 26 | if let Some(owner_id) = owner_id { 27 | self.owner_id = owner_id; 28 | } 29 | if let Some(treasury_id) = treasury_id { 30 | self.treasury_id = Some(treasury_id); 31 | } 32 | 33 | if let Some(slot_granularity) = slot_granularity { 34 | self.slot_granularity = slot_granularity; 35 | } 36 | if let Some(paused) = paused { 37 | self.paused = paused; 38 | } 39 | if let Some(gas_price) = gas_price { 40 | self.gas_price = gas_price.0; 41 | } 42 | if let Some(proxy_callback_gas) = proxy_callback_gas { 43 | self.proxy_callback_gas = proxy_callback_gas.0; 44 | } 45 | if let Some(agent_fee) = agent_fee { 46 | self.agent_fee = agent_fee.0; 47 | } 48 | if let Some(agent_task_ratio) = agent_task_ratio { 49 | self.agent_task_ratio = [agent_task_ratio[0].0, agent_task_ratio[1].0]; 50 | } 51 | if let Some(agents_eject_threshold) = agents_eject_threshold { 52 | self.agents_eject_threshold = agents_eject_threshold.0; 53 | } 54 | } 55 | 56 | /// Allows admin to calculate internal balances 57 | /// Returns surplus and rewards balances 58 | /// Can be used to measure how much surplus is remaining for staking / etc 59 | #[private] 60 | pub fn calc_balances(&mut self) -> (U128, U128) { 61 | let base_balance = BASE_BALANCE; // safety overhead 62 | let storage_balance = env::storage_byte_cost().saturating_mul(env::storage_usage() as u128); 63 | 64 | // Using storage + threshold as the start for how much balance is required 65 | let required_balance = base_balance.saturating_add(storage_balance); 66 | let mut total_task_balance: Balance = 0; 67 | let mut total_reward_balance: Balance = 0; 68 | 69 | // Loop all tasks and add 70 | for (_, t) in self.tasks.iter() { 71 | total_task_balance = total_task_balance.saturating_add(t.total_deposit.0); 72 | } 73 | 74 | // Loop all agents rewards and add 75 | for a in self.agent_active_queue.iter() { 76 | if let Some(agent) = self.agents.get(&a) { 77 | total_reward_balance = total_reward_balance.saturating_add(agent.balance.0); 78 | } 79 | } 80 | 81 | let total_available_balance: Balance = 82 | total_task_balance.saturating_add(total_reward_balance); 83 | 84 | // Calculate surplus, which could be used for staking 85 | // TODO: This would be adjusted by preferences of like 30% of total task deposit or similar 86 | let surplus = u128::max( 87 | env::account_balance() 88 | .saturating_sub(total_available_balance) 89 | .saturating_sub(required_balance), 90 | 0, 91 | ); 92 | log!("Stakeable surplus {}", surplus); 93 | 94 | // update internal values 95 | self.available_balance = u128::max(total_available_balance, 0); 96 | 97 | // Return surplus value in case we want to trigger staking based off outcome 98 | (U128::from(surplus), U128::from(total_reward_balance)) 99 | } 100 | 101 | /// Move Balance 102 | /// Allows owner to move balance to DAO or to let treasury transfer to itself only. 103 | pub fn move_balance(&mut self, amount: U128, account_id: AccountId) -> Promise { 104 | // Check if is owner OR the treasury account 105 | let transfer_warning = b"Not approved for transfer"; 106 | if let Some(treasury_id) = self.treasury_id.clone() { 107 | if treasury_id != env::predecessor_account_id() 108 | && self.owner_id != env::predecessor_account_id() 109 | { 110 | env::panic(transfer_warning); 111 | } 112 | } else if self.owner_id != env::predecessor_account_id() { 113 | env::panic(transfer_warning); 114 | } 115 | // for now, only allow movement of funds between owner and treasury 116 | let check_account = self.treasury_id.clone().unwrap_or(self.owner_id.clone()); 117 | if check_account != account_id.clone() { 118 | env::panic(b"Cannot move funds to this account"); 119 | } 120 | // Check that the amount is not larger than available 121 | let (_, _, _, surplus) = self.get_balances(); 122 | assert!(amount.0 < surplus.0, "Amount is too high"); 123 | 124 | // transfer 125 | // NOTE: Not updating available balance, as we are simply allowing surplus transfer only 126 | Promise::new(account_id).transfer(amount.0) 127 | } 128 | 129 | // /// Allows admin to remove slot data, in case a task gets stuck due to missed exits 130 | // pub fn remove_slot_owner(&mut self, slot: U128) { 131 | // // assert_eq!( 132 | // // self.owner_id, 133 | // // env::predecessor_account_id(), 134 | // // "Must be owner" 135 | // // ); 136 | // assert_eq!( 137 | // env::current_account_id(), 138 | // env::predecessor_account_id(), 139 | // "Must be owner" 140 | // ); 141 | // self.slots.remove(&slot.0); 142 | // } 143 | 144 | // /// Deletes a task in its entirety, returning any remaining balance to task owner. 145 | // /// 146 | // /// ```bash 147 | // /// near call manager_v1.croncat.testnet remove_task_owner '{"task_hash": ""}' --accountId YOU.testnet 148 | // /// ``` 149 | // #[private] 150 | // pub fn remove_task_owner(&mut self, task_hash: Base64VecU8) { 151 | // let hash = task_hash.0; 152 | // self.tasks.get(&hash).expect("No task found by hash"); 153 | 154 | // // If owner, allow to remove task 155 | // self.exit_task(hash); 156 | // } 157 | 158 | // /// Deletes a trigger in its entirety, only by owner. 159 | // /// 160 | // /// ```bash 161 | // /// near call manager_v1.croncat.testnet remove_trigger_owner '{"trigger_hash": ""}' --accountId YOU.testnet 162 | // /// ``` 163 | // #[private] 164 | // pub fn remove_trigger_owner(&mut self, trigger_hash: Base64VecU8) { 165 | // self.triggers 166 | // .remove(&trigger_hash.0) 167 | // .expect("No trigger found by hash"); 168 | // } 169 | } 170 | 171 | #[cfg(test)] 172 | mod tests { 173 | use super::*; 174 | use near_sdk::json_types::ValidAccountId; 175 | use near_sdk::test_utils::{accounts, VMContextBuilder}; 176 | use near_sdk::{testing_env, MockedBlockchain}; 177 | 178 | const BLOCK_START_BLOCK: u64 = 52_201_040; 179 | const BLOCK_START_TS: u64 = 1_624_151_503_447_000_000; 180 | 181 | fn get_context(predecessor_account_id: ValidAccountId) -> VMContextBuilder { 182 | let mut builder = VMContextBuilder::new(); 183 | builder 184 | .current_account_id(accounts(0)) 185 | .signer_account_id(predecessor_account_id.clone()) 186 | .signer_account_pk(b"ed25519:4ZhGmuKTfQn9ZpHCQVRwEr4JnutL8Uu3kArfxEqksfVM".to_vec()) 187 | .predecessor_account_id(predecessor_account_id) 188 | .block_index(BLOCK_START_BLOCK) 189 | .block_timestamp(BLOCK_START_TS); 190 | builder 191 | } 192 | 193 | #[test] 194 | #[should_panic(expected = "Must be owner")] 195 | fn test_update_settings_fail() { 196 | let mut context = get_context(accounts(1)); 197 | testing_env!(context.build()); 198 | let mut contract = Contract::new(); 199 | testing_env!(context.is_view(true).build()); 200 | assert_eq!(contract.slot_granularity, SLOT_GRANULARITY); 201 | 202 | testing_env!(context 203 | .is_view(false) 204 | .signer_account_id(accounts(3)) 205 | .predecessor_account_id(accounts(3)) 206 | .build()); 207 | contract.update_settings(None, Some(10), None, None, None, None, None, None, None); 208 | } 209 | 210 | #[test] 211 | fn test_update_settings() { 212 | let mut context = get_context(accounts(1)); 213 | testing_env!(context.build()); 214 | let mut contract = Contract::new(); 215 | testing_env!(context.is_view(true).build()); 216 | assert_eq!(contract.slot_granularity, SLOT_GRANULARITY); 217 | 218 | testing_env!(context.is_view(false).build()); 219 | contract.update_settings( 220 | None, 221 | Some(10), 222 | Some(true), 223 | None, 224 | None, 225 | None, 226 | None, 227 | None, 228 | None, 229 | ); 230 | testing_env!(context.is_view(true).build()); 231 | assert_eq!(contract.slot_granularity, 10); 232 | assert_eq!(contract.paused, true); 233 | } 234 | 235 | #[test] 236 | fn test_update_settings_agent_ratio() { 237 | let mut context = get_context(accounts(1)); 238 | testing_env!(context.build()); 239 | let mut contract = Contract::new(); 240 | testing_env!(context.is_view(true).build()); 241 | assert_eq!(contract.slot_granularity, SLOT_GRANULARITY); 242 | 243 | testing_env!(context.is_view(false).build()); 244 | contract.update_settings( 245 | None, 246 | None, 247 | Some(true), 248 | None, 249 | None, 250 | None, 251 | Some(vec![U64(2), U64(5)]), 252 | None, 253 | None, 254 | ); 255 | testing_env!(context.is_view(true).build()); 256 | assert_eq!(contract.agent_task_ratio[0], 2); 257 | assert_eq!(contract.agent_task_ratio[1], 5); 258 | assert_eq!(contract.paused, true); 259 | } 260 | 261 | #[test] 262 | fn test_calc_balances() { 263 | let mut context = get_context(accounts(1)); 264 | testing_env!(context.build()); 265 | let mut contract = Contract::new(); 266 | let base_agent_storage: u128 = 2260000000000000000000; 267 | contract.calc_balances(); 268 | 269 | testing_env!(context 270 | .is_view(false) 271 | .attached_deposit(ONE_NEAR * 5) 272 | .build()); 273 | contract.create_task( 274 | accounts(3), 275 | "increment".to_string(), 276 | "0 0 */1 * * *".to_string(), 277 | Some(true), 278 | Some(U128::from(ONE_NEAR)), 279 | Some(200), 280 | None, 281 | ); 282 | contract.register_agent(Some(accounts(1))); 283 | testing_env!(context.is_view(false).build()); 284 | 285 | // recalc the balances 286 | let (surplus, rewards) = contract.calc_balances(); 287 | testing_env!(context.is_view(true).build()); 288 | assert_eq!(contract.available_balance, 5002260000000000000000000); 289 | assert_eq!(surplus.0, 91925740000000000000000000); 290 | assert_eq!(rewards.0, base_agent_storage); 291 | } 292 | 293 | #[test] 294 | fn test_move_balance() { 295 | let mut context = get_context(accounts(1)); 296 | testing_env!(context.is_view(false).build()); 297 | let mut contract = Contract::new(); 298 | contract.calc_balances(); 299 | contract.move_balance(U128::from(ONE_NEAR / 2), accounts(1).to_string()); 300 | testing_env!(context.is_view(true).build()); 301 | 302 | let (_, _, _, surplus) = contract.get_balances(); 303 | assert_eq!(surplus.0, 91928000000000000000000000); 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /manager/src/storage_impl.rs: -------------------------------------------------------------------------------- 1 | use crate::Contract; 2 | use near_contract_standards::storage_management::{ 3 | StorageBalance, StorageBalanceBounds, StorageManagement, 4 | }; 5 | use near_sdk::json_types::{ValidAccountId, U128}; 6 | use near_sdk::{assert_one_yocto, env, log, AccountId, Balance, Promise}; 7 | 8 | impl Contract { 9 | fn internal_storage_balance_of(&self, account_id: &AccountId) -> Option { 10 | if self.agents.contains_key(account_id) { 11 | // The "available" balance is always zero because the storage isn't 12 | // variable for this contract. 13 | Some(StorageBalance { 14 | total: self.storage_balance_bounds().min, 15 | available: 0.into(), 16 | }) 17 | } else { 18 | None 19 | } 20 | } 21 | } 22 | 23 | impl StorageManagement for Contract { 24 | // `registration_only` doesn't affect the implementation here, as there's no need to add additional 25 | // storage, so there's only one balance to attach. 26 | #[allow(unused_variables)] 27 | fn storage_deposit( 28 | &mut self, 29 | account_id: Option, 30 | registration_only: Option, 31 | ) -> StorageBalance { 32 | self.register_agent(account_id.clone()); 33 | let account_id = account_id 34 | .map(|a| a.into()) 35 | .unwrap_or_else(|| env::predecessor_account_id()); 36 | self.internal_storage_balance_of(&account_id).unwrap() 37 | } 38 | 39 | /// While storage_withdraw normally allows the caller to retrieve `available` balance, this 40 | /// contract sets storage_balance_bounds.min = storage_balance_bounds.max, 41 | /// which means available balance will always be 0. So this implementation: 42 | /// * panics if `amount > 0` 43 | /// * never transfers Ⓝ to caller 44 | /// * returns a `storage_balance` struct if `amount` is 0 45 | fn storage_withdraw(&mut self, amount: Option) -> StorageBalance { 46 | assert_one_yocto(); 47 | let predecessor = env::predecessor_account_id(); 48 | if let Some(storage_balance) = self.internal_storage_balance_of(&predecessor) { 49 | match amount { 50 | Some(amount) if amount.0 > 0 => { 51 | let panic_msg = format!("The amount is greater than the available storage balance. Remember there's a minimum balance needed for an agent's storage. That minimum is {}. To unregister an agent, use the 'unregister_agent' or 'storage_unregister' with the 'force' option.", self.agent_storage_usage); 52 | env::panic(panic_msg.as_bytes()); 53 | } 54 | _ => storage_balance, 55 | } 56 | } else { 57 | env::panic(format!("The account {} is not registered", &predecessor).as_bytes()); 58 | } 59 | } 60 | 61 | fn storage_unregister(&mut self, force: Option) -> bool { 62 | assert_one_yocto(); 63 | let account_id = env::predecessor_account_id(); 64 | let force = force.unwrap_or(false); 65 | if let Some(agent) = self.agents.get(&account_id) { 66 | let balance = agent.balance.0; 67 | if balance == 0 || force { 68 | self.remove_agent(account_id.clone()); 69 | 70 | // We add 1 to reimburse for the 1 yoctoⓃ used to call this method 71 | self.available_balance = self.available_balance.saturating_sub(balance + 1); 72 | Promise::new(account_id).transfer(balance + 1); 73 | log!( 74 | "Agent has been removed and refunded the storage cost of {}", 75 | balance + 1 76 | ); 77 | true 78 | } else { 79 | env::panic(b"Can't unregister the agent with the positive balance. Must use the 'force' parameter if desired.") 80 | } 81 | } else { 82 | log!("The agent {} is not registered", &account_id); 83 | false 84 | } 85 | } 86 | 87 | fn storage_balance_bounds(&self) -> StorageBalanceBounds { 88 | let required_storage_balance = 89 | Balance::from(self.agent_storage_usage) * env::storage_byte_cost(); 90 | StorageBalanceBounds { 91 | min: required_storage_balance.into(), 92 | max: Some(required_storage_balance.into()), 93 | } 94 | } 95 | 96 | fn storage_balance_of(&self, account_id: ValidAccountId) -> Option { 97 | self.internal_storage_balance_of(account_id.as_ref()) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /manager/src/triggers.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use near_sdk::serde_json; 3 | 4 | pub const NO_DEPOSIT: Balance = 0; 5 | pub const VIEW_CALL_GAS: Gas = 240_000_000_000_000; 6 | 7 | #[derive(BorshDeserialize, BorshSerialize, Debug, Serialize, Deserialize, PartialEq)] 8 | #[serde(crate = "near_sdk::serde")] 9 | pub struct Trigger { 10 | /// Entity responsible for this task, can change task details 11 | pub owner_id: AccountId, 12 | 13 | /// Account to direct all view calls against 14 | pub contract_id: AccountId, 15 | 16 | /// Contract method this trigger will be viewing 17 | pub function_id: String, 18 | 19 | // NOTE: Only allow static pre-defined bytes 20 | pub arguments: Base64VecU8, 21 | 22 | /// The task to trigger if view results in TRUE 23 | /// Task can still use a cadence, or can utilize a very large time window and allow view triggers to be main source of execution 24 | pub task_hash: Base64VecU8, 25 | } 26 | 27 | #[derive(BorshDeserialize, BorshSerialize, Debug, Serialize, Deserialize, PartialEq)] 28 | #[serde(crate = "near_sdk::serde")] 29 | pub struct TriggerHumanFriendly { 30 | pub owner_id: AccountId, 31 | pub contract_id: AccountId, 32 | pub function_id: String, 33 | pub arguments: Base64VecU8, 34 | pub task_hash: Base64VecU8, 35 | pub hash: Base64VecU8, 36 | } 37 | 38 | pub type CroncatTriggerResponse = (bool, Option); 39 | 40 | #[near_bindgen] 41 | impl Contract { 42 | /// !IMPORTANT!:: BETA FEATURE!!!!!!!!! 43 | /// Configure a VIEW call to map to a task, allowing IFTTT functionality 44 | /// IMPORTANT: Trigger methods MUST respond with a boolean 45 | /// 46 | /// ```bash 47 | /// near call manager_v1.croncat.testnet create_trigger '{"contract_id": "counter.in.testnet","function_id": "increment","arguments":"","task_hash":""}' --accountId YOU.testnet 48 | /// ``` 49 | #[payable] 50 | pub fn create_trigger( 51 | &mut self, 52 | contract_id: ValidAccountId, 53 | function_id: String, 54 | task_hash: Base64VecU8, 55 | arguments: Option, 56 | ) -> Base64VecU8 { 57 | // No adding triggers while contract is paused 58 | assert_eq!(self.paused, false, "Create trigger paused"); 59 | // Check attached deposit includes trigger_storage_usage 60 | assert!( 61 | env::attached_deposit() >= self.trigger_storage_usage as u128, 62 | "Trigger storage payment of {} required", 63 | self.trigger_storage_usage 64 | ); 65 | // prevent dumb mistakes 66 | assert!(contract_id.to_string().len() > 0, "Contract ID missing"); 67 | assert!(function_id.len() > 0, "Function ID missing"); 68 | assert!(task_hash.0.len() > 0, "Task Hash missing"); 69 | assert_ne!( 70 | contract_id.clone().to_string(), 71 | env::current_account_id(), 72 | "Trigger cannot call self" 73 | ); 74 | 75 | // Confirm owner of task is same 76 | let task = self.tasks.get(&task_hash.0).expect("No task found"); 77 | assert_eq!( 78 | task.owner_id, 79 | env::predecessor_account_id(), 80 | "Must be task owner" 81 | ); 82 | 83 | let item = Trigger { 84 | owner_id: env::predecessor_account_id(), 85 | contract_id: contract_id.into(), 86 | function_id, 87 | task_hash, 88 | arguments: arguments.unwrap_or_else(|| Base64VecU8::from(vec![])), 89 | }; 90 | 91 | let trigger_hash = self.get_trigger_hash(&item); 92 | 93 | // Add trigger to catalog 94 | assert!( 95 | self.triggers.insert(&trigger_hash, &item).is_none(), 96 | "Trigger already exists" 97 | ); 98 | 99 | Base64VecU8::from(trigger_hash) 100 | } 101 | 102 | /// Deletes a task in its entirety, returning any remaining balance to task owner. 103 | /// 104 | /// ```bash 105 | /// near call manager_v1.croncat.testnet remove_trigger '{"trigger_hash": ""}' --accountId YOU.testnet 106 | /// ``` 107 | pub fn remove_trigger(&mut self, trigger_hash: Base64VecU8) { 108 | let hash = trigger_hash.0; 109 | let trigger = self.triggers.get(&hash).expect("No task found by hash"); 110 | 111 | assert_eq!( 112 | trigger.owner_id, 113 | env::predecessor_account_id(), 114 | "Only owner can remove their trigger." 115 | ); 116 | 117 | // If owner, allow to remove task 118 | self.triggers 119 | .remove(&hash) 120 | .expect("No trigger found by hash"); 121 | 122 | // Refund trigger storage 123 | Promise::new(trigger.owner_id).transfer(self.trigger_storage_usage as u128); 124 | } 125 | 126 | /// Get the hash of a trigger based on parameters 127 | pub fn get_trigger_hash(&self, item: &Trigger) -> Vec { 128 | // Generate hash, needs to be from known values so we can reproduce the hash without storing 129 | let input = format!( 130 | "{:?}{:?}{:?}{:?}{:?}", 131 | item.contract_id, item.function_id, item.task_hash, item.owner_id, item.arguments 132 | ); 133 | env::sha256(input.as_bytes()) 134 | } 135 | 136 | /// Returns trigger data 137 | /// 138 | /// ```bash 139 | /// near view manager_v1.croncat.testnet get_triggers '{"from_index": 0, "limit": 10}' 140 | /// ``` 141 | pub fn get_triggers( 142 | &self, 143 | from_index: Option, 144 | limit: Option, 145 | ) -> Vec { 146 | let mut ret: Vec = Vec::new(); 147 | let mut start = 0; 148 | let mut end = 10; 149 | if let Some(from_index) = from_index { 150 | start = from_index.0; 151 | } 152 | if let Some(limit) = limit { 153 | end = u64::min(start + limit.0, self.tasks.len()); 154 | } 155 | 156 | // Return all tasks within range 157 | let keys = self.triggers.keys_as_vector(); 158 | for i in start..end { 159 | if let Some(trigger_hash) = keys.get(i) { 160 | if let Some(trigger) = self.triggers.get(&trigger_hash) { 161 | ret.push(TriggerHumanFriendly { 162 | owner_id: trigger.owner_id.clone(), 163 | contract_id: trigger.contract_id.clone(), 164 | function_id: trigger.function_id.clone(), 165 | arguments: trigger.arguments.clone(), 166 | task_hash: trigger.task_hash.clone(), 167 | hash: Base64VecU8::from(self.get_trigger_hash(&trigger)), 168 | }); 169 | } 170 | } 171 | } 172 | ret 173 | } 174 | 175 | /// Returns trigger 176 | /// 177 | /// ```bash 178 | /// near view manager_v1.croncat.testnet get_trigger '{"trigger_hash": "..."}' 179 | /// ``` 180 | pub fn get_trigger(&self, trigger_hash: Base64VecU8) -> TriggerHumanFriendly { 181 | let trigger = self 182 | .triggers 183 | .get(&trigger_hash.0) 184 | .expect("No trigger found"); 185 | 186 | TriggerHumanFriendly { 187 | owner_id: trigger.owner_id.clone(), 188 | contract_id: trigger.contract_id.clone(), 189 | function_id: trigger.function_id.clone(), 190 | arguments: trigger.arguments.clone(), 191 | task_hash: trigger.task_hash.clone(), 192 | hash: Base64VecU8::from(self.get_trigger_hash(&trigger)), 193 | } 194 | } 195 | 196 | /// !IMPORTANT!:: BETA FEATURE!!!!!!!!! 197 | /// Allows agents to check if a view method should trigger a task immediately 198 | /// 199 | /// TODO: 200 | /// - Check for range hash 201 | /// - Loop range to find view BOOL TRUE 202 | /// - Get task details 203 | /// - Execute task 204 | /// 205 | /// ```bash 206 | /// near call manager_v1.croncat.testnet proxy_conditional_call '{"trigger_hash": ""}' --accountId YOU.testnet 207 | /// ``` 208 | pub fn proxy_conditional_call(&mut self, trigger_hash: Base64VecU8) { 209 | // No adding tasks while contract is paused 210 | assert_eq!(self.paused, false, "Task execution paused"); 211 | 212 | // only registered agent signed, because micropayments will benefit long term 213 | let agent_opt = self.agents.get(&env::predecessor_account_id()); 214 | if agent_opt.is_none() { 215 | env::panic(b"Agent not registered"); 216 | } 217 | 218 | // TODO: Think about agent rewards - as they could pay for a failed CB 219 | let trigger = self 220 | .triggers 221 | .get(&trigger_hash.into()) 222 | .expect("No trigger found by hash"); 223 | 224 | // TODO: check the task actually exists 225 | 226 | // Make sure this isnt calling manager 227 | assert_ne!( 228 | trigger.contract_id.clone().to_string(), 229 | env::current_account_id(), 230 | "Trigger cannot call self" 231 | ); 232 | 233 | // Call external contract with task variables 234 | let promise_first = env::promise_create( 235 | trigger.contract_id.clone(), 236 | &trigger.function_id.as_bytes(), 237 | trigger.arguments.0.as_slice(), 238 | NO_DEPOSIT, 239 | VIEW_CALL_GAS, 240 | ); 241 | let promise_second = env::promise_then( 242 | promise_first, 243 | env::current_account_id(), 244 | b"proxy_conditional_callback", 245 | json!({ 246 | "task_hash": trigger.task_hash, 247 | "agent_id": &env::predecessor_account_id(), 248 | }) 249 | .to_string() 250 | .as_bytes(), 251 | NO_DEPOSIT, 252 | GAS_FOR_CALLBACK, 253 | ); 254 | env::promise_return(promise_second); 255 | } 256 | 257 | /// !IMPORTANT!:: BETA FEATURE!!!!!!!!! 258 | /// Callback, if response is TRUE, then do the actual proxy call 259 | #[private] 260 | pub fn proxy_conditional_callback(&mut self, task_hash: Base64VecU8, agent_id: AccountId) { 261 | assert_eq!( 262 | env::promise_results_count(), 263 | 1, 264 | "Expected 1 promise result." 265 | ); 266 | let mut agent = self.agents.get(&agent_id).expect("Agent not found"); 267 | match env::promise_result(0) { 268 | PromiseResult::NotReady => { 269 | unreachable!() 270 | } 271 | PromiseResult::Successful(trigger_result) => { 272 | let result: CroncatTriggerResponse = serde_json::de::from_slice(&trigger_result) 273 | .expect("Could not get result from trigger"); 274 | 275 | // TODO: Refactor to re-used method 276 | if result.0 { 277 | let mut task = self 278 | .tasks 279 | .get(&task_hash.clone().into()) 280 | .expect("No task found by hash"); 281 | 282 | // Fee breakdown: 283 | // - Used Gas: Task Txn Fee Cost 284 | // - Agent Fee: Incentivize Execution SLA 285 | // 286 | // Task Fee Examples: 287 | // Total Fee = Gas Fee + Agent Fee 288 | // Total Balance = Task Deposit + Total Fee 289 | // 290 | // NOTE: Gas cost includes the cross-contract call & internal logic of this contract. 291 | // Direct contract gas fee will be lower than task execution costs, however 292 | // we require the task owner to appropriately estimate gas for overpayment. 293 | // The gas overpayment will also accrue to the agent since there is no way to read 294 | // how much gas was actually used on callback. 295 | let call_fee_used = u128::from(task.gas) * self.gas_price; 296 | let call_total_fee = call_fee_used + self.agent_fee; 297 | let call_total_balance = task.deposit.0 + call_total_fee; 298 | 299 | // Update agent storage 300 | // Increment agent reward & task count 301 | // Reward for agent MUST include the amount of gas used as a reimbursement 302 | agent.balance = U128::from(agent.balance.0 + call_total_fee); 303 | agent.total_tasks_executed = U128::from(agent.total_tasks_executed.0 + 1); 304 | self.available_balance = self.available_balance - call_total_fee; 305 | 306 | // Reset missed slot, if any 307 | if agent.last_missed_slot != 0 { 308 | agent.last_missed_slot = 0; 309 | } 310 | self.agents.insert(&env::signer_account_id(), &agent); 311 | 312 | // Decrease task balance, Update task storage 313 | task.total_deposit = U128::from(task.total_deposit.0 - call_total_balance); 314 | self.tasks.insert(&task_hash.into(), &task); 315 | 316 | // Call external contract with task variables 317 | let promise_first = env::promise_create( 318 | task.contract_id.clone(), 319 | &task.function_id.as_bytes(), 320 | // TODO: support CroncatTriggerResponse optional view arguments 321 | task.arguments.0.as_slice(), 322 | task.deposit.0, 323 | task.gas, 324 | ); 325 | 326 | env::promise_return(promise_first); 327 | } else { 328 | log!("Trigger returned false"); 329 | } 330 | } 331 | PromiseResult::Failed => { 332 | // Problem with the creation transaction, reward money has been returned to this contract. 333 | log!("Trigger call failed"); 334 | self.send_base_agent_reward(agent); 335 | } 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /manager/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | /// Tick: Cron Manager Heartbeat 6 | /// Used to manage agents, manage internal use of funds 7 | /// 8 | /// Return operations balances, for external on-chain contract monitoring 9 | /// 10 | /// near call manager_v1.croncat.testnet tick '{}' 11 | pub fn tick(&mut self) { 12 | // TBD: Internal staking management 13 | log!( 14 | "Balances [Operations, Treasury]: [{},{}]", 15 | self.available_balance, 16 | self.staked_balance 17 | ); 18 | 19 | // execute agent management every tick so we can allow coming/going of agents without each agent paying to manage themselves 20 | // NOTE: the agent CAN pay to execute "tick" method if they are anxious to become an active agent. The most they can query is every 10s. 21 | self.manage_agents(); 22 | } 23 | 24 | /// Manage agents 25 | fn manage_agents(&mut self) { 26 | let current_slot = self.get_slot_id(None); 27 | 28 | // Loop all agents to assess if really active 29 | // Why the copy here? had to get a mutable reference from immutable self instance 30 | let mut bad_agents: Vec = Vec::from(self.agent_active_queue.to_vec()); 31 | bad_agents.retain(|agent_id| { 32 | let _agent = self.agents.get(&agent_id); 33 | 34 | if let Some(_agent) = _agent { 35 | let last_slot = u128::from(_agent.last_missed_slot); 36 | 37 | // Check if any agents need to be ejected, looking at previous task slot and current 38 | // LOGIC: If agent misses X number of slots, eject! 39 | if current_slot 40 | > last_slot + (self.agents_eject_threshold * u128::from(self.slot_granularity)) 41 | { 42 | true 43 | } else { 44 | false 45 | } 46 | } else { 47 | false 48 | } 49 | }); 50 | 51 | // EJECT! 52 | // Dont eject if only 1 agent remaining... so sad. no lonely allowed. 53 | if self.agent_active_queue.len() > 2 { 54 | for id in bad_agents { 55 | self.exit_agent(Some(id), Some(true)); 56 | } 57 | } 58 | 59 | // Get data needed to check for agent<>task ratio 60 | let total_tasks = self.tasks.len(); 61 | let total_agents = self.agent_active_queue.len(); 62 | let [agent_amount, task_amount] = self.agent_task_ratio; 63 | 64 | // no panic returns. safe-guard from idiot ratios. 65 | if total_tasks == 0 || total_agents == 0 { 66 | return; 67 | } 68 | if agent_amount == 0 || task_amount == 0 { 69 | return; 70 | } 71 | let ratio = task_amount.div_euclid(agent_amount); 72 | let total_available_agents = total_tasks.div_euclid(ratio); 73 | 74 | // Check if there are more tasks to allow a new agent 75 | if total_available_agents > total_agents { 76 | // There's enough tasks to support another agent, check if we have any pending 77 | if self.agent_pending_queue.len() > 0 { 78 | // FIFO grab pending agents 79 | let agent_id = self.agent_pending_queue.swap_remove(0); 80 | if let Some(mut agent) = self.agents.get(&agent_id) { 81 | agent.status = agent::AgentStatus::Active; 82 | self.agents.insert(&agent_id, &agent); 83 | self.agent_active_queue.push(&agent_id); 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use super::*; 93 | use near_sdk::json_types::ValidAccountId; 94 | use near_sdk::test_utils::{accounts, VMContextBuilder}; 95 | use near_sdk::{testing_env, MockedBlockchain}; 96 | 97 | const BLOCK_START_TS: u64 = 1633759320000000000; 98 | 99 | fn get_context(predecessor_account_id: ValidAccountId) -> VMContextBuilder { 100 | let mut builder = VMContextBuilder::new(); 101 | builder 102 | .current_account_id(accounts(0)) 103 | .signer_account_id(predecessor_account_id.clone()) 104 | .signer_account_pk(b"ed25519:4ZhGmuKTfQn9ZpHCQVRwEr4JnutL8Uu3kArfxEqksfVM".to_vec()) 105 | .predecessor_account_id(predecessor_account_id) 106 | .block_timestamp(BLOCK_START_TS); 107 | builder 108 | } 109 | 110 | // TODO: Add test for checking pending agent here. 111 | #[test] 112 | fn test_tick() { 113 | let mut context = get_context(accounts(1)); 114 | testing_env!(context.is_view(false).build()); 115 | let mut contract = Contract::new(); 116 | testing_env!(context.is_view(true).build()); 117 | testing_env!(context 118 | .is_view(false) 119 | .block_timestamp(1633759440000000000) 120 | .build()); 121 | contract.tick(); 122 | testing_env!(context 123 | .is_view(false) 124 | .block_timestamp(1633760160000000000) 125 | .build()); 126 | contract.tick(); 127 | testing_env!(context 128 | .is_view(false) 129 | .block_timestamp(1633760460000000000) 130 | .build()); 131 | testing_env!(context.is_view(true).build()); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /manager/tests/sim/test_utils.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | TaskBase64Hash, AGENT_ID, COUNTER_ID, COUNTER_WASM_BYTES, CRON_MANAGER_WASM_BYTES, MANAGER_ID, 3 | SPUTNIKV2_ID, SPUTNIKV2_WASM_BYTES, USER_ID, 4 | }; 5 | use near_primitives_core::account::Account as PrimitiveAccount; 6 | use near_sdk::json_types::Base64VecU8; 7 | use near_sdk::serde_json; 8 | use near_sdk::serde_json::json; 9 | use near_sdk_sim::account::AccessKey; 10 | use near_sdk_sim::near_crypto::{InMemorySigner, KeyType, Signer}; 11 | use near_sdk_sim::runtime::{GenesisConfig, RuntimeStandalone}; 12 | use near_sdk_sim::state_record::StateRecord; 13 | use near_sdk_sim::types::AccountId; 14 | use near_sdk_sim::{ 15 | init_simulator, to_yocto, ExecutionResult, UserAccount, DEFAULT_GAS, STORAGE_AMOUNT, 16 | }; 17 | use std::cell::{RefCell, RefMut}; 18 | use std::rc::Rc; 19 | 20 | pub(crate) fn helper_create_task(cron: &UserAccount, counter: &UserAccount) -> TaskBase64Hash { 21 | let execution_result = counter.call( 22 | cron.account_id(), 23 | "create_task", 24 | &json!({ 25 | "contract_id": COUNTER_ID, 26 | "function_id": "increment".to_string(), 27 | "cadence": "0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri 2018/2".to_string(), 28 | "recurring": true, 29 | "deposit": "12000000000000", 30 | "gas": 3000000000000u64, 31 | }) 32 | .to_string() 33 | .into_bytes(), 34 | DEFAULT_GAS, 35 | 2_600_000_024_000_000_000_000u128, // deposit 36 | ); 37 | execution_result.assert_success(); 38 | let hash: Base64VecU8 = execution_result.unwrap_json(); 39 | serde_json::to_string(&hash).unwrap() 40 | } 41 | 42 | /// Basic initialization returning the "root account" for the simulator 43 | /// and the NFT account with the contract deployed and initialized. 44 | pub(crate) fn sim_helper_init() -> (UserAccount, UserAccount) { 45 | let mut root_account = init_simulator(None); 46 | root_account = root_account.create_user("sim".to_string(), to_yocto("1000000")); 47 | 48 | // Deploy cron manager and call "new" method 49 | let cron = root_account.deploy(&CRON_MANAGER_WASM_BYTES, MANAGER_ID.into(), STORAGE_AMOUNT); 50 | cron.call( 51 | cron.account_id(), 52 | "new", 53 | &[], 54 | DEFAULT_GAS, 55 | 0, // attached deposit 56 | ) 57 | .assert_success(); 58 | 59 | (root_account, cron) 60 | } 61 | 62 | pub(crate) fn sim_helper_create_agent_user( 63 | root_account: &UserAccount, 64 | ) -> (UserAccount, UserAccount) { 65 | let hundred_near = to_yocto("100"); 66 | let agent = root_account.create_user(AGENT_ID.into(), hundred_near); 67 | let user = root_account.create_user(USER_ID.into(), hundred_near); 68 | (agent, user) 69 | } 70 | 71 | pub(crate) fn sim_helper_init_counter(root_account: &UserAccount) -> UserAccount { 72 | // Deploy counter 73 | let counter = root_account.deploy(&COUNTER_WASM_BYTES, COUNTER_ID.into(), STORAGE_AMOUNT); 74 | counter 75 | } 76 | 77 | pub(crate) fn sim_helper_init_sputnikv2(root_account: &UserAccount) -> UserAccount { 78 | // Deploy SputnikDAOv2 and call "new" method 79 | let sputnik = root_account.deploy(&SPUTNIKV2_WASM_BYTES, SPUTNIKV2_ID.into(), STORAGE_AMOUNT); 80 | /* 81 | export COUNCIL='["'$CONTRACT_ID'"]' 82 | near call $CONTRACT_ID new '{"config": {"name": "genesis2", "purpose": "test", "metadata": ""}, "policy": '$COUNCIL'}' --accountId $CONTRACT_ID 83 | */ 84 | root_account.call( 85 | sputnik.account_id.clone(), 86 | "new", 87 | &json!({ 88 | "config": { 89 | "name": "cron dao", 90 | "purpose": "not chew bubble gum", 91 | "metadata": "" 92 | }, 93 | "policy": [USER_ID] 94 | }) 95 | .to_string() 96 | .into_bytes(), 97 | DEFAULT_GAS, 98 | 0, 99 | ); 100 | sputnik 101 | } 102 | 103 | pub(crate) fn counter_create_task( 104 | counter: &UserAccount, 105 | cron: AccountId, 106 | cadence: &str, 107 | ) -> ExecutionResult { 108 | counter.call( 109 | cron, 110 | "create_task", 111 | &json!({ 112 | "contract_id": counter.account_id, 113 | "function_id": "increment".to_string(), 114 | "cadence": cadence, 115 | "recurring": true, 116 | "deposit": "0", 117 | // "gas": 100_000_000_000_000u64, 118 | "gas": 2_400_000_000_000u64, 119 | }) 120 | .to_string() 121 | .into_bytes(), 122 | DEFAULT_GAS, 123 | 120480000000000000000000, // deposit (0.120000000002 Ⓝ) 124 | ) 125 | } 126 | 127 | pub(crate) fn bootstrap_time_simulation() -> ( 128 | InMemorySigner, 129 | UserAccount, 130 | UserAccount, 131 | UserAccount, 132 | UserAccount, 133 | ) { 134 | let mut genesis = GenesisConfig::default(); 135 | let root_account_id = "root".to_string(); 136 | let signer = genesis.init_root_signer(&root_account_id); 137 | 138 | // Make agent signer 139 | let agent_signer = InMemorySigner::from_seed("agent.root", KeyType::ED25519, "aloha"); 140 | // Push agent account to state_records 141 | genesis.state_records.push(StateRecord::Account { 142 | account_id: "agent.root".to_string(), 143 | account: PrimitiveAccount { 144 | amount: to_yocto("6000"), 145 | locked: 0, 146 | code_hash: Default::default(), 147 | storage_usage: 0, 148 | }, 149 | }); 150 | genesis.state_records.push(StateRecord::AccessKey { 151 | account_id: "agent.root".to_string(), 152 | public_key: agent_signer.clone().public_key(), 153 | access_key: AccessKey::full_access(), 154 | }); 155 | 156 | let runtime = RuntimeStandalone::new_with_store(genesis); 157 | let runtime_rc = &Rc::new(RefCell::new(runtime)); 158 | let root_account = UserAccount::new(runtime_rc, root_account_id, signer); 159 | 160 | // create "counter" account and deploy 161 | let counter = root_account.deploy( 162 | &COUNTER_WASM_BYTES, 163 | "counter.root".to_string(), 164 | STORAGE_AMOUNT, 165 | ); 166 | 167 | // create "agent" account from signer 168 | let agent = UserAccount::new(runtime_rc, "agent.root".to_string(), agent_signer.clone()); 169 | 170 | // create "cron" account, deploy and call "new" 171 | let cron = root_account.deploy( 172 | &CRON_MANAGER_WASM_BYTES, 173 | "cron.root".to_string(), 174 | STORAGE_AMOUNT, 175 | ); 176 | cron.call( 177 | cron.account_id(), 178 | "new", 179 | &[], 180 | DEFAULT_GAS, 181 | 0, // attached deposit 182 | ) 183 | .assert_success(); 184 | 185 | (agent_signer, root_account, agent, counter, cron) 186 | } 187 | 188 | pub(crate) fn find_log_from_outcomes(root_runtime: &RefMut, msg: &String) { 189 | let last_outcomes = &root_runtime.last_outcomes; 190 | 191 | // This isn't great, but we check to make sure the log exists about the transfer 192 | // At the time of this writing, finding the TransferAction with the correct 193 | // deposit was not happening with simulation tests. 194 | // Look for a log saying "Withdrawal of 60000000000000000000000 has been sent." in one of these 195 | let mut found_withdrawal_log = false; 196 | for outcome_hash in last_outcomes { 197 | let eo = root_runtime.outcome(&outcome_hash).unwrap(); 198 | for log in eo.logs { 199 | if log.contains(msg) { 200 | found_withdrawal_log = true; 201 | } 202 | } 203 | } 204 | assert!( 205 | found_withdrawal_log, 206 | "Expected a recent outcome to have a log about the transfer action. Log: {}", 207 | msg 208 | ); 209 | } 210 | -------------------------------------------------------------------------------- /manager/tests/sputnik/sputnikdao2.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CronCats/contracts/cafd3caafb91b45abb6e811ce0fa2819980d6f96/manager/tests/sputnik/sputnikdao2.wasm -------------------------------------------------------------------------------- /rewards/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-C", "link-args=-s"] -------------------------------------------------------------------------------- /rewards/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rewards" 3 | version = "0.1.1" 4 | authors = ["cron.cat", "@trevorjtclarke", "@mikedotexe"] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | near-sdk = "3.1.0" 12 | near-contract-standards = "3.2.0" 13 | 14 | [dev-dependencies] 15 | near-sdk-sim = "3.1.0" 16 | near-primitives-core = "0.4.0" -------------------------------------------------------------------------------- /rewards/src/lib.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::{ 2 | borsh::{self, BorshDeserialize, BorshSerialize}, 3 | collections::UnorderedSet, 4 | env, ext_contract, 5 | json_types::{Base64VecU8, ValidAccountId, U128, U64}, 6 | log, near_bindgen, 7 | serde::{Deserialize, Serialize}, 8 | serde_json, AccountId, BorshStorageKey, Gas, PanicOnDefault, Promise, PromiseResult, 9 | }; 10 | 11 | near_sdk::setup_alloc!(); 12 | 13 | // Fee Definitions 14 | pub const NO_DEPOSIT: u128 = 0; 15 | pub const GAS_FOR_CHECK_TASK_CALL: Gas = 60_000_000_000_000; 16 | pub const GAS_FOR_CHECK_TASK_CALLBACK: Gas = 60_000_000_000_000; 17 | pub const GAS_FOR_PXPET_DISTRO_CALL: Gas = 20_000_000_000_000; 18 | 19 | #[derive(BorshDeserialize, BorshSerialize, Debug, Serialize, Deserialize, PartialEq)] 20 | #[serde(crate = "near_sdk::serde")] 21 | pub struct Task { 22 | pub owner_id: AccountId, 23 | pub contract_id: AccountId, 24 | pub function_id: String, 25 | pub cadence: String, 26 | pub recurring: bool, 27 | pub deposit: U128, 28 | pub gas: Gas, 29 | pub arguments: Base64VecU8, 30 | } 31 | 32 | #[ext_contract(ext_pixelpet)] 33 | pub trait ExtPixelpet { 34 | fn distribute_croncat( 35 | &self, 36 | account_id: AccountId, 37 | #[callback] 38 | #[serializer(borsh)] 39 | task: Option, 40 | ); 41 | } 42 | 43 | #[ext_contract(ext_croncat)] 44 | pub trait ExtCroncat { 45 | fn get_slot_tasks(&self, offset: Option) -> (Vec, U128); 46 | fn get_tasks( 47 | &self, 48 | slot: Option, 49 | from_index: Option, 50 | limit: Option, 51 | ) -> Vec; 52 | // fn get_task(&self, task_hash: Base64VecU8) -> Task; 53 | fn get_task(&self, task_hash: String) -> Task; 54 | fn create_task( 55 | &mut self, 56 | contract_id: String, 57 | function_id: String, 58 | cadence: String, 59 | recurring: Option, 60 | deposit: Option, 61 | gas: Option, 62 | arguments: Option>, 63 | ) -> Base64VecU8; 64 | fn remove_task(&mut self, task_hash: Base64VecU8); 65 | } 66 | 67 | #[ext_contract(ext_rewards)] 68 | pub trait ExtRewards { 69 | fn pet_distribute_croncat(&mut self, owner_id: AccountId); 70 | } 71 | 72 | #[derive(BorshStorageKey, BorshSerialize)] 73 | pub enum StorageKeys { 74 | PixelpetAccounts, 75 | } 76 | 77 | #[near_bindgen] 78 | #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] 79 | pub struct Contract { 80 | // Runtime 81 | paused: bool, 82 | cron_account_id: AccountId, 83 | dao_account_id: AccountId, 84 | 85 | // Pixelpet configs 86 | pixelpet_account_id: AccountId, 87 | pixelpet_accounts_claimed: UnorderedSet, 88 | pixelpet_max_issued: u8, 89 | // TBD: NFT & DAO Management 90 | } 91 | 92 | #[near_bindgen] 93 | impl Contract { 94 | /// ```bash 95 | /// near call rewards.cron.testnet --initFunction new --initArgs '{"cron_account_id": "manager.cron.testnet", "dao_account_id": "dao.sputnikv2.testnet"}' --accountId manager_v1.croncat.testnet 96 | /// ``` 97 | #[init] 98 | pub fn new(cron_account_id: ValidAccountId, dao_account_id: ValidAccountId) -> Self { 99 | Contract { 100 | paused: false, 101 | cron_account_id: cron_account_id.into(), 102 | dao_account_id: dao_account_id.into(), 103 | 104 | // Pixelpet configs 105 | pixelpet_account_id: env::signer_account_id(), 106 | pixelpet_accounts_claimed: UnorderedSet::new(StorageKeys::PixelpetAccounts), 107 | pixelpet_max_issued: 50, 108 | } 109 | } 110 | 111 | /// Returns semver of this contract. 112 | /// 113 | /// ```bash 114 | /// near view rewards.cron.testnet version 115 | /// ``` 116 | pub fn version(&self) -> String { 117 | env!("CARGO_PKG_VERSION").to_string() 118 | } 119 | 120 | /// Returns stats of this contract 121 | /// 122 | /// ```bash 123 | /// near view rewards.cron.testnet stats 124 | /// ``` 125 | pub fn stats(&self) -> (u64, String) { 126 | ( 127 | self.pixelpet_accounts_claimed.len(), 128 | self.pixelpet_accounts_claimed 129 | .iter() 130 | .map(|a| a + ",") 131 | .collect(), 132 | ) 133 | } 134 | 135 | /// Settings changes 136 | /// ```bash 137 | /// near call rewards.cron.testnet update_settings '{"pixelpet_account_id": "pixeltoken.near"}' --accountId manager_v1.croncat.testnet 138 | /// ``` 139 | #[private] 140 | pub fn update_settings(&mut self, pixelpet_account_id: Option) { 141 | if let Some(pixelpet_account_id) = pixelpet_account_id { 142 | self.pixelpet_account_id = pixelpet_account_id; 143 | } 144 | } 145 | 146 | /// Check a cron task, then grant owner a pet 147 | /// ```bash 148 | /// near call rewards.cron.testnet pet_check_task_ownership '{"task_hash": "r2Jv…T4U4="}' --accountId manager_v1.croncat.testnet 149 | /// ``` 150 | pub fn pet_check_task_ownership(&mut self, task_hash: String) -> Promise { 151 | let owner_id = env::predecessor_account_id(); 152 | 153 | // Check owner doesnt already ahve pet 154 | assert!( 155 | !self.pixelpet_accounts_claimed.contains(&owner_id), 156 | "Owner already has pet" 157 | ); 158 | 159 | // Check there are pets left 160 | assert!( 161 | self.pixelpet_accounts_claimed.len() <= u64::from(self.pixelpet_max_issued), 162 | "All pets claimed" 163 | ); 164 | 165 | // Get the task data 166 | ext_croncat::get_task( 167 | task_hash, 168 | &self.cron_account_id, 169 | NO_DEPOSIT, 170 | GAS_FOR_CHECK_TASK_CALL, 171 | ) 172 | .then(ext_rewards::pet_distribute_croncat( 173 | owner_id, 174 | &env::current_account_id(), 175 | NO_DEPOSIT, 176 | GAS_FOR_CHECK_TASK_CALLBACK, 177 | )) 178 | } 179 | 180 | /// Watch for new cron task that grants a pet 181 | #[private] 182 | pub fn pet_distribute_croncat(&mut self, owner_id: AccountId) { 183 | assert_eq!( 184 | env::promise_results_count(), 185 | 1, 186 | "Expected 1 promise result." 187 | ); 188 | match env::promise_result(0) { 189 | PromiseResult::NotReady => { 190 | unreachable!() 191 | } 192 | PromiseResult::Successful(task_result) => { 193 | let task: Task = serde_json::de::from_slice(&task_result) 194 | .expect("Could not get result from task hash"); 195 | 196 | if !task.owner_id.is_empty() { 197 | let mut pet_owner_id = owner_id.clone(); 198 | // Two paths: 199 | // 1. automated claim via croncat manager 200 | // 2. directly without manager, but has a task already 201 | if &owner_id == &self.cron_account_id { 202 | // Check that the task is the right function method 203 | assert_eq!( 204 | &task.contract_id, 205 | &env::current_account_id(), 206 | "Must be game account id" 207 | ); 208 | assert_eq!( 209 | &task.function_id, 210 | &String::from("pet_check_task_ownership"), 211 | "Must be game function method" 212 | ); 213 | pet_owner_id = task.owner_id.replace("\"", ""); 214 | } else { 215 | // Check that task owner matches this owner 216 | assert_eq!(&owner_id, &task.owner_id, "Task is not owned by you"); 217 | } 218 | log!("Minting croncat pet to {:?}", &pet_owner_id); 219 | 220 | // NOTE: Possible for promise to fail and this blocks another attempt to claim pet 221 | self.pixelpet_accounts_claimed.insert(&pet_owner_id); 222 | 223 | // Trigger call to pixel pets 224 | ext_pixelpet::distribute_croncat( 225 | pet_owner_id, 226 | &self.pixelpet_account_id, 227 | NO_DEPOSIT, 228 | GAS_FOR_PXPET_DISTRO_CALL, 229 | ); 230 | } else { 231 | log!("No pet distributed"); 232 | } 233 | } 234 | PromiseResult::Failed => { 235 | // Problem with the creation transaction, reward money has been returned to this contract. 236 | log!("No pet distributed"); 237 | } 238 | } 239 | } 240 | 241 | /// Remove stale distributions (to correct released pets) 242 | /// ```bash 243 | /// near call rewards.cron.near pet_clear_owner '{"account_id": "someone.near"}' --accountId manager_v1.croncat.testnet 244 | /// ``` 245 | #[private] 246 | pub fn pet_clear_owner(&mut self, account_id: AccountId) { 247 | self.pixelpet_accounts_claimed.remove(&account_id); 248 | } 249 | } 250 | 251 | // Want to help with tests? Join our discord for bounty opps 252 | // #[cfg(test)] 253 | // mod tests { 254 | // use super::*; 255 | // use near_sdk::json_types::ValidAccountId; 256 | // use near_sdk::test_utils::{accounts, VMContextBuilder}; 257 | // use near_sdk::{testing_env, MockedBlockchain}; 258 | 259 | // const BLOCK_START_BLOCK: u64 = 52_201_040; 260 | // const BLOCK_START_TS: u64 = 1_624_151_503_447_000_000; 261 | 262 | // fn get_context(predecessor_account_id: ValidAccountId) -> VMContextBuilder { 263 | // let mut builder = VMContextBuilder::new(); 264 | // builder 265 | // .current_account_id(accounts(0)) 266 | // .signer_account_id(predecessor_account_id.clone()) 267 | // .signer_account_pk(b"ed25519:4ZhGmuKTfQn9ZpHCQVRwEr4JnutL8Uu3kArfxEqksfVM".to_vec()) 268 | // .predecessor_account_id(predecessor_account_id) 269 | // .block_index(BLOCK_START_BLOCK) 270 | // .block_timestamp(BLOCK_START_TS); 271 | // builder 272 | // } 273 | 274 | // #[test] 275 | // fn test_contract_new() { 276 | // let mut context = get_context(accounts(1)); 277 | // testing_env!(context.build()); 278 | // let contract = Contract::new(); 279 | // testing_env!(context.is_view(true).build()); 280 | // assert!(contract.get_tasks(None, None, None).is_empty()); 281 | // } 282 | // } 283 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | stable -------------------------------------------------------------------------------- /scripts/airdrop_bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [ -d "res" ]; then 5 | echo "" 6 | else 7 | mkdir res 8 | fi 9 | 10 | cd "`dirname $0`" 11 | 12 | if [ -z "$KEEP_NAMES" ]; then 13 | export RUSTFLAGS='-C link-arg=-s' 14 | else 15 | export RUSTFLAGS='' 16 | fi 17 | 18 | # build the things 19 | cargo build --all --target wasm32-unknown-unknown --release 20 | cp ../target/wasm32-unknown-unknown/release/*.wasm ./res/ 21 | 22 | # Uncomment the desired network 23 | export NEAR_ENV=testnet 24 | # export NEAR_ENV=mainnet 25 | # export NEAR_ENV=guildnet 26 | # export NEAR_ENV=betanet 27 | 28 | # export FACTORY=testnet 29 | export FACTORY=near 30 | # export FACTORY=registrar 31 | 32 | export MAX_GAS=300000000000000 33 | 34 | if [ -z ${NEAR_ACCT+x} ]; then 35 | export NEAR_ACCT=croncat.$FACTORY 36 | else 37 | export NEAR_ACCT=$NEAR_ACCT 38 | fi 39 | 40 | export CRON_ACCOUNT_ID=manager_v1.$NEAR_ACCT 41 | export AIRDROP_ACCOUNT_ID=airdrop.$NEAR_ACCT 42 | export AGENT_ACCOUNT_ID=agent.$NEAR_ACCT 43 | export DAO_ACCOUNT_ID=croncat.sputnikv2.$FACTORY 44 | export FT_ACCOUNT_ID=ft.$NEAR_ACCT 45 | export NFT_ACCOUNT_ID=nft.$NEAR_ACCT 46 | 47 | # near delete $AIRDROP_ACCOUNT_ID $NEAR_ACCT 48 | # near create-account $AIRDROP_ACCOUNT_ID --masterAccount $NEAR_ACCT 49 | # near deploy --wasmFile ./res/airdrop.wasm --accountId $AIRDROP_ACCOUNT_ID --initFunction new --initArgs '{"ft_account_id": "'$FT_ACCOUNT_ID'","nft_account_id": "'$NFT_ACCOUNT_ID'"}' 50 | # # near deploy --wasmFile ./res/airdrop.wasm --accountId $AIRDROP_ACCOUNT_ID 51 | 52 | # # Setup & Deploy FT & NFT 53 | # near delete $FT_ACCOUNT_ID $NEAR_ACCT 54 | # near delete $NFT_ACCOUNT_ID $NEAR_ACCT 55 | # near create-account $FT_ACCOUNT_ID --masterAccount $NEAR_ACCT 56 | # near create-account $NFT_ACCOUNT_ID --masterAccount $NEAR_ACCT 57 | # near deploy --wasmFile ../res/fungible_token.wasm --accountId $FT_ACCOUNT_ID --initFunction new --initArgs '{ "owner_id": "'$AIRDROP_ACCOUNT_ID'", "total_supply": "100000000000000000", "metadata": { "spec": "ft-1.0.0", "name": "Airdrop Token", "symbol": "ADP", "decimals": 18 } }' 58 | # near deploy --wasmFile ../res/non_fungible_token.wasm --accountId $NFT_ACCOUNT_ID --initFunction new_default_meta --initArgs '{"owner_id": "'$AIRDROP_ACCOUNT_ID'"}' 59 | # near view $FT_ACCOUNT_ID ft_balance_of '{"account_id": "'$AIRDROP_ACCOUNT_ID'"}' 60 | # near call $NFT_ACCOUNT_ID nft_mint '{"token_id": "2", "token_owner_id": "'$AIRDROP_ACCOUNT_ID'", "token_metadata": { "title": "Olympus Mons", "description": "Tallest mountain in charted solar system", "media": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/00/Olympus_Mons_alt.jpg/1024px-Olympus_Mons_alt.jpg", "copies": 100}}' --accountId $AIRDROP_ACCOUNT_ID --deposit 0.1 61 | # near view $NFT_ACCOUNT_ID nft_tokens_for_owner '{"account_id": "'$AIRDROP_ACCOUNT_ID'"}' 62 | # near call $NFT_ACCOUNT_ID nft_transfer '{ "receiver_id": "user_1.in.testnet", "token_id": "2" }' --accountId $AIRDROP_ACCOUNT_ID --depositYocto 1 63 | 64 | # # # mint a bunch of test NFTs 65 | # ### NOTE: Id rather transfer copies, but seems theres not a way to do that? 66 | # declare -i INDEX_NFTS=5 67 | # declare -i TOTAL_NFTS=12 68 | # for (( e=0; e<=TOTAL_NFTS; e++ )) 69 | # do 70 | # declare -i TMP_NFT_ID=($e+$INDEX_NFTS) 71 | # near call $NFT_ACCOUNT_ID nft_mint '{"token_id": "'$TMP_NFT_ID'", "token_owner_id": "'$AIRDROP_ACCOUNT_ID'", "token_metadata": { "title": "Olympus Mons", "description": "Tallest mountain in charted solar system", "media": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/00/Olympus_Mons_alt.jpg/1024px-Olympus_Mons_alt.jpg", "copies": 100}}' --accountId $AIRDROP_ACCOUNT_ID --deposit 0.1 72 | # done 73 | # near view $NFT_ACCOUNT_ID nft_tokens_for_owner '{"account_id": "'$AIRDROP_ACCOUNT_ID'"}' 74 | 75 | # # Check all configs first 76 | near view $AIRDROP_ACCOUNT_ID stats 77 | 78 | # near call $AIRDROP_ACCOUNT_ID reset_index --accountId $AIRDROP_ACCOUNT_ID --gas $MAX_GAS 79 | 80 | # # Config things 81 | # near call $AIRDROP_ACCOUNT_ID add_manager '{"account_id": "'$AIRDROP_ACCOUNT_ID'"}' --accountId $AIRDROP_ACCOUNT_ID --gas $MAX_GAS 82 | # near call $AIRDROP_ACCOUNT_ID add_manager '{"account_id": "'$DAO_ACCOUNT_ID'"}' --accountId $AIRDROP_ACCOUNT_ID --gas $MAX_GAS 83 | 84 | # # # create a bunch of test users to airdrop to 85 | # TOTAL_USERS=10 86 | # for (( e=0; e<=TOTAL_USERS; e++ )) 87 | # do 88 | # TMP_USER="user_${e}.$NEAR_ACCT" 89 | 90 | # near delete $TMP_USER $NEAR_ACCT 91 | # near create-account $TMP_USER --masterAccount $NEAR_ACCT 92 | # near call $FT_ACCOUNT_ID storage_deposit --accountId $TMP_USER --amount 0.00484 93 | 94 | # near call $AIRDROP_ACCOUNT_ID add_account '{"account_id": "'$TMP_USER'"}' --accountId $AIRDROP_ACCOUNT_ID --gas $MAX_GAS 95 | # done 96 | 97 | # Test automated distro 98 | # near call $AIRDROP_ACCOUNT_ID multisend '{"transfer_type": "Near", "amount": "500000000000000000000000"}' --amount 10 --accountId $AIRDROP_ACCOUNT_ID --gas $MAX_GAS 99 | # testing FT setup 100 | # near call $AIRDROP_ACCOUNT_ID multisend '{"transfer_type": "FungibleToken", "amount": "5"}' --accountId $AIRDROP_ACCOUNT_ID --gas $MAX_GAS 101 | # testing NFT setup 102 | # near call $AIRDROP_ACCOUNT_ID multisend '{"transfer_type": "NonFungibleToken", "amount": "5"}' --accountId $AIRDROP_ACCOUNT_ID --gas $MAX_GAS 103 | 104 | # # Register "multisend" task, which will get triggered back to back until the pagination is complete 105 | # near call $CRON_ACCOUNT_ID remove_task '{"task_hash": "UK1+xizXmG974zooHOH8VvkoNT1vOz3PqJpk3A/lCbo="}' --accountId $AIRDROP_ACCOUNT_ID 106 | # # # Args are for 0.5 near transfer per account 107 | # near call $CRON_ACCOUNT_ID create_task '{"contract_id": "'$AIRDROP_ACCOUNT_ID'","function_id": "multisend","cadence": "0 * * * * *","recurring": true,"deposit": "2500000000000000000000000","gas": 200000000000000, "arguments": "eyJ0cmFuc2Zlcl90eXBlIjogIk5lYXIiLCAiYW1vdW50IjogIjUwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCJ9"}' --accountId $AIRDROP_ACCOUNT_ID --amount 8 108 | # # # Args are for 0.5 Fungible Token transfer per account 109 | # near call $CRON_ACCOUNT_ID create_task '{"contract_id": "'$AIRDROP_ACCOUNT_ID'","function_id": "multisend","cadence": "1 * * * * *","recurring": true,"deposit": "2500000000000000000000000","gas": 200000000000000, "arguments": "eyJ0cmFuc2Zlcl90eXBlIjogIkZ1bmdpYmxlVG9rZW4iLCAiYW1vdW50IjogIjUwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCJ9"}' --accountId $AIRDROP_ACCOUNT_ID --amount 1 110 | # # # Args are for a Non Fungible Token transfer per account 111 | # # near call $CRON_ACCOUNT_ID create_task '{"contract_id": "'$AIRDROP_ACCOUNT_ID'","function_id": "multisend","cadence": "0 * * * * *","recurring": true,"deposit": "2500000000000000000000000","gas": 200000000000000, "arguments": "eyJ0cmFuc2Zlcl90eXBlIjogIk5vbkZ1bmdpYmxlVG9rZW4iLCAiYW1vdW50IjogIjUifQ=="}' --accountId $AIRDROP_ACCOUNT_ID --amount 1 112 | 113 | # # Call proxy_call to trigger multisend 114 | # # near call $CRON_ACCOUNT_ID register_agent '{"payable_account_id": "'$AGENT_ACCOUNT_ID'"}' --accountId $AGENT_ACCOUNT_ID --amount 0.00484 115 | 116 | # sleep 1m 117 | # near call $CRON_ACCOUNT_ID proxy_call --accountId $AGENT_ACCOUNT_ID --gas $MAX_GAS 118 | # near call $CRON_ACCOUNT_ID proxy_call --accountId $AGENT_ACCOUNT_ID --gas $MAX_GAS 119 | # near call $CRON_ACCOUNT_ID proxy_call --accountId $AGENT_ACCOUNT_ID --gas $MAX_GAS 120 | # near call $CRON_ACCOUNT_ID proxy_call --accountId $AGENT_ACCOUNT_ID --gas $MAX_GAS 121 | # near call $CRON_ACCOUNT_ID proxy_call --accountId $AGENT_ACCOUNT_ID --gas $MAX_GAS 122 | # near call $CRON_ACCOUNT_ID proxy_call --accountId $AGENT_ACCOUNT_ID --gas $MAX_GAS 123 | 124 | # End result 125 | near view $AIRDROP_ACCOUNT_ID stats 126 | 127 | echo "Cron $NEAR_ENV Airdrop Complete" 128 | -------------------------------------------------------------------------------- /scripts/clear_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Uncomment the desired network 3 | export NEAR_ENV=testnet 4 | # export NEAR_ENV=mainnet 5 | # export NEAR_ENV=guildnet 6 | # export NEAR_ENV=betanet 7 | 8 | export FACTORY=testnet 9 | # export FACTORY=near 10 | # export FACTORY=registrar 11 | 12 | if [ -z ${NEAR_ACCT+x} ]; then 13 | export NEAR_ACCT=croncat.$FACTORY 14 | else 15 | export NEAR_ACCT=$NEAR_ACCT 16 | fi 17 | 18 | export CRON_ACCOUNT_ID=manager_v1.$NEAR_ACCT 19 | export REWARDS_ACCOUNT_ID=rewards.$NEAR_ACCT 20 | export AIRDROP_ACCOUNT_ID=airdrop.$NEAR_ACCT 21 | export COUNTER_ACCOUNT_ID=counter.$NEAR_ACCT 22 | export AGENT_ACCOUNT_ID=agent.$NEAR_ACCT 23 | export USER_ACCOUNT_ID=user.$NEAR_ACCT 24 | export CRUD_ACCOUNT_ID=crudcross.$NEAR_ACCT 25 | export VIEWS_ACCOUNT_ID=views.$NEAR_ACCT 26 | export DAO_ACCOUNT_ID=croncat.sputnikv2.$FACTORY 27 | 28 | # clear and recreate all accounts 29 | near delete $CRON_ACCOUNT_ID $NEAR_ACCT 30 | near delete $REWARDS_ACCOUNT_ID $NEAR_ACCT 31 | near delete $AIRDROP_ACCOUNT_ID $NEAR_ACCT 32 | near delete $COUNTER_ACCOUNT_ID $NEAR_ACCT 33 | near delete $AGENT_ACCOUNT_ID $NEAR_ACCT 34 | near delete $USER_ACCOUNT_ID $NEAR_ACCT 35 | near delete $CRUD_ACCOUNT_ID $NEAR_ACCT 36 | near delete $VIEWS_ACCOUNT_ID $NEAR_ACCT 37 | 38 | echo "Clear Complete" -------------------------------------------------------------------------------- /scripts/create_and_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file is used for starting a fresh set of all contracts & configs 3 | set -e 4 | 5 | if [ -d "res" ]; then 6 | echo "" 7 | else 8 | mkdir res 9 | fi 10 | 11 | cd "`dirname $0`" 12 | 13 | if [ -z "$KEEP_NAMES" ]; then 14 | export RUSTFLAGS='-C link-arg=-s' 15 | else 16 | export RUSTFLAGS='' 17 | fi 18 | 19 | # build the things 20 | cargo build --all --target wasm32-unknown-unknown --release 21 | cp ../target/wasm32-unknown-unknown/release/*.wasm ./res/ 22 | 23 | # Uncomment the desired network 24 | export NEAR_ENV=testnet 25 | # export NEAR_ENV=mainnet 26 | # export NEAR_ENV=guildnet 27 | # export NEAR_ENV=betanet 28 | 29 | export FACTORY=testnet 30 | # export FACTORY=near 31 | # export FACTORY=registrar 32 | 33 | if [ -z ${NEAR_ACCT+x} ]; then 34 | export NEAR_ACCT=croncat.$FACTORY 35 | else 36 | export NEAR_ACCT=$NEAR_ACCT 37 | fi 38 | 39 | export CRON_ACCOUNT_ID=manager_v1.$NEAR_ACCT 40 | export REWARDS_ACCOUNT_ID=rewards.$NEAR_ACCT 41 | export AIRDROP_ACCOUNT_ID=airdrop.$NEAR_ACCT 42 | export COUNTER_ACCOUNT_ID=counter.$NEAR_ACCT 43 | export AGENT_ACCOUNT_ID=agent.$NEAR_ACCT 44 | export USER_ACCOUNT_ID=user.$NEAR_ACCT 45 | export CRUD_ACCOUNT_ID=crudcross.$NEAR_ACCT 46 | export VIEWS_ACCOUNT_ID=views.$NEAR_ACCT 47 | export DAO_ACCOUNT_ID=croncat.sputnikv2.$FACTORY 48 | 49 | # # create all accounts 50 | # near create-account $CRON_ACCOUNT_ID --masterAccount $NEAR_ACCT 51 | # near create-account $REWARDS_ACCOUNT_ID --masterAccount $NEAR_ACCT 52 | # near create-account $AIRDROP_ACCOUNT_ID --masterAccount $NEAR_ACCT 53 | # near create-account $COUNTER_ACCOUNT_ID --masterAccount $NEAR_ACCT 54 | # near create-account $AGENT_ACCOUNT_ID --masterAccount $NEAR_ACCT 55 | # near create-account $USER_ACCOUNT_ID --masterAccount $NEAR_ACCT 56 | # near create-account $CRUD_ACCOUNT_ID --masterAccount $NEAR_ACCT 57 | # near create-account $VIEWS_ACCOUNT_ID --masterAccount $NEAR_ACCT 58 | 59 | # # Deploy all the contracts to their rightful places 60 | # near deploy --wasmFile ./res/manager.wasm --accountId $CRON_ACCOUNT_ID --initFunction new --initArgs '{}' 61 | # near deploy --wasmFile ./res/rewards.wasm --accountId $REWARDS_ACCOUNT_ID --initFunction new --initArgs '{"cron_account_id": "'$CRON_ACCOUNT_ID'", "dao_account_id": "'$DAO_ACCOUNT_ID'"}' 62 | # near deploy --wasmFile ./res/airdrop.wasm --accountId $AIRDROP_ACCOUNT_ID --initFunction new --initArgs '{"ft_account_id": "wrap.'$FACTORY'"}' 63 | # near deploy --wasmFile ./res/rust_counter_tutorial.wasm --accountId $COUNTER_ACCOUNT_ID 64 | # near deploy --wasmFile ./res/cross_contract.wasm --accountId $CRUD_ACCOUNT_ID --initFunction new --initArgs '{"cron": "'$CRON_ACCOUNT_ID'"}' 65 | # near deploy --wasmFile ./res/views.wasm --accountId $VIEWS_ACCOUNT_ID 66 | 67 | # Re-Deploy code changes 68 | near deploy --wasmFile ./res/manager.wasm --accountId $CRON_ACCOUNT_ID 69 | 70 | echo "Setup Complete" -------------------------------------------------------------------------------- /scripts/guildnet_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file is used for starting a fresh set of all contracts & configs 3 | set -e 4 | 5 | if [ -d "res" ]; then 6 | echo "" 7 | else 8 | mkdir res 9 | fi 10 | 11 | cd "`dirname $0`" 12 | 13 | if [ -z "$KEEP_NAMES" ]; then 14 | export RUSTFLAGS='-C link-arg=-s' 15 | else 16 | export RUSTFLAGS='' 17 | fi 18 | 19 | # build the things 20 | cargo build --all --target wasm32-unknown-unknown --release 21 | cp ../target/wasm32-unknown-unknown/release/*.wasm ./res/ 22 | 23 | # Uncomment the desired network 24 | export NEAR_ENV=guildnet 25 | 26 | export FACTORY=guildnet 27 | 28 | if [ -z ${NEAR_ACCT+x} ]; then 29 | # you will need to change this to something you own 30 | export NEAR_ACCT=croncat.$FACTORY 31 | else 32 | export NEAR_ACCT=$NEAR_ACCT 33 | fi 34 | 35 | export MAX_GAS=300000000000000 36 | 37 | export CRON_ACCOUNT_ID=manager_v1.$NEAR_ACCT 38 | export DAO_ACCOUNT_ID=croncat.sputnik-dao.near 39 | 40 | ###### 41 | # NOTE: All commands below WORK, just have them off for safety. 42 | ###### 43 | 44 | ## clear and recreate all accounts 45 | # near delete $CRON_ACCOUNT_ID $NEAR_ACCT 46 | 47 | 48 | ## create all accounts 49 | # near create-account $CRON_ACCOUNT_ID --masterAccount $NEAR_ACCT --initialBalance 1000 50 | 51 | 52 | # Deploy all the contracts to their rightful places 53 | # near deploy --wasmFile ./res/manager.wasm --accountId $CRON_ACCOUNT_ID --initFunction new --initArgs '{}' 54 | 55 | 56 | # # Assign ownership to the DAO 57 | # near call $CRON_ACCOUNT_ID update_settings '{ "owner_id": "'$DAO_ACCOUNT_ID'", "paused": true }' --accountId $CRON_ACCOUNT_ID --gas $MAX_GAS 58 | # near call $CRON_ACCOUNT_ID update_settings '{ "paused": false }' --accountId $CRON_ACCOUNT_ID --gas $MAX_GAS 59 | 60 | 61 | # RE:Deploy all the contracts to their rightful places 62 | # near deploy --wasmFile ./res/manager.wasm --accountId $CRON_ACCOUNT_ID 63 | 64 | 65 | # Check all configs first 66 | near view $CRON_ACCOUNT_ID version 67 | near view $CRON_ACCOUNT_ID get_info 68 | 69 | echo "Testnet Deploy Complete" -------------------------------------------------------------------------------- /scripts/mainnet_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file is used for starting a fresh set of all contracts & configs 3 | set -e 4 | 5 | if [ -d "res" ]; then 6 | echo "" 7 | else 8 | mkdir res 9 | fi 10 | 11 | cd "`dirname $0`" 12 | 13 | if [ -z "$KEEP_NAMES" ]; then 14 | export RUSTFLAGS='-C link-arg=-s' 15 | else 16 | export RUSTFLAGS='' 17 | fi 18 | 19 | # build the things 20 | cargo build --all --target wasm32-unknown-unknown --release 21 | cp ../target/wasm32-unknown-unknown/release/*.wasm ./res/ 22 | 23 | # Uncomment the desired network 24 | export NEAR_ENV=mainnet 25 | 26 | export FACTORY=near 27 | 28 | if [ -z ${NEAR_ACCT+x} ]; then 29 | # you will need to change this to something you own 30 | export NEAR_ACCT=croncat.$FACTORY 31 | else 32 | export NEAR_ACCT=$NEAR_ACCT 33 | fi 34 | 35 | export MAX_GAS=300000000000000 36 | 37 | export CRON_ACCOUNT_ID=manager_v1.$NEAR_ACCT 38 | export REWARDS_ACCOUNT_ID=rewards.$NEAR_ACCT 39 | export DAO_ACCOUNT_ID=croncat.sputnik-dao.near 40 | 41 | ###### 42 | # NOTE: All commands below WORK, just have them off for safety. 43 | ###### 44 | 45 | ## clear and recreate all accounts 46 | # near delete $CRON_ACCOUNT_ID $NEAR_ACCT 47 | # near delete $REWARDS_ACCOUNT_ID $NEAR_ACCT 48 | 49 | 50 | ## create all accounts 51 | # near create-account $CRON_ACCOUNT_ID --masterAccount $NEAR_ACCT --initialBalance 10 52 | # near create-account $REWARDS_ACCOUNT_ID --masterAccount $NEAR_ACCT --initialBalance 3 53 | 54 | 55 | # Deploy all the contracts to their rightful places 56 | # near deploy --wasmFile ./res/manager.wasm --accountId $CRON_ACCOUNT_ID --initFunction new --initArgs '{}' 57 | # near deploy --wasmFile ./res/rewards.wasm --accountId $REWARDS_ACCOUNT_ID --initFunction new --initArgs '{"cron_account_id": "'$CRON_ACCOUNT_ID'", "dao_account_id": "'$DAO_ACCOUNT_ID'"}' 58 | 59 | 60 | # # Assign ownership to the DAO 61 | # near call $CRON_ACCOUNT_ID update_settings '{ "owner_id": "'$DAO_ACCOUNT_ID'", "paused": true }' --accountId $CRON_ACCOUNT_ID --gas $MAX_GAS 62 | 63 | # # Configure initial requirements 64 | # near call $REWARDS_ACCOUNT_ID update_settings '{"pixelpet_account_id": "pixeltoken.near"}' --accountId $REWARDS_ACCOUNT_ID --gas $MAX_GAS 65 | 66 | # RE:Deploy all the contracts to their rightful places 67 | # near deploy --wasmFile ./res/manager.wasm --accountId $CRON_ACCOUNT_ID 68 | # near deploy --wasmFile ./res/rewards.wasm --accountId $REWARDS_ACCOUNT_ID 69 | 70 | # near call $CRON_ACCOUNT_ID calc_balances --accountId $CRON_ACCOUNT_ID 71 | 72 | # Check all configs first 73 | near view $CRON_ACCOUNT_ID version 74 | near view $CRON_ACCOUNT_ID get_info 75 | # near view $REWARDS_ACCOUNT_ID version 76 | 77 | echo "Mainnet Deploy Complete" -------------------------------------------------------------------------------- /scripts/owner_commands.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Uncomment the desired network 3 | export NEAR_ENV=testnet 4 | # export NEAR_ENV=mainnet 5 | # export NEAR_ENV=guildnet 6 | # export NEAR_ENV=betanet 7 | 8 | export FACTORY=testnet 9 | # export FACTORY=near 10 | # export FACTORY=registrar 11 | 12 | export MAX_GAS=300000000000000 13 | 14 | if [ -z ${NEAR_ACCT+x} ]; then 15 | export NEAR_ACCT=croncat.$FACTORY 16 | else 17 | export NEAR_ACCT=$NEAR_ACCT 18 | fi 19 | 20 | export CRON_ACCOUNT_ID=manager_v1.$NEAR_ACCT 21 | export COUNTER_ACCOUNT_ID=counter.$NEAR_ACCT 22 | export AGENT_ACCOUNT_ID=agent.$NEAR_ACCT 23 | export USER_ACCOUNT_ID=user.$NEAR_ACCT 24 | export CRUD_ACCOUNT_ID=crud.$NEAR_ACCT 25 | export DAO_ACCOUNT_ID=croncat.sputnikv2.$FACTORY 26 | # export DAO_ACCOUNT_ID=croncat.sputnik-dao.$FACTORY 27 | 28 | # # Change ownership to DAO 29 | # near call $CRON_ACCOUNT_ID update_settings '{"owner_id": "'$DAO_ACCOUNT_ID'"}' --accountId $CRON_ACCOUNT_ID 30 | 31 | # # Submit proposal to change a configuration setting (Example: Change agent fee) 32 | # ARGS=`echo "{ \"agent_fee\": \"1000000000000000000000\" }" | base64` 33 | # FIXED_ARGS=`echo $ARGS | tr -d '\r' | tr -d ' '` 34 | # near call $DAO_ACCOUNT_ID add_proposal '{"proposal": {"description": "Change cron manager settings, see attached arguments for what is changing", "kind": {"FunctionCall": {"receiver_id": "'$CRON_ACCOUNT_ID'", "actions": [{"method_name": "update_settings", "args": "'$FIXED_ARGS'", "deposit": "0", "gas": "20000000000000"}]}}}}' --accountId $NEAR_ACCT --amount 0.1 35 | 36 | # # Check all configs 37 | near view $CRON_ACCOUNT_ID version 38 | near view $CRON_ACCOUNT_ID get_info -------------------------------------------------------------------------------- /scripts/rewards_bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Uncomment the desired network 3 | # export NEAR_ENV=testnet 4 | export NEAR_ENV=mainnet 5 | # export NEAR_ENV=guildnet 6 | # export NEAR_ENV=betanet 7 | 8 | # export FACTORY=testnet 9 | export FACTORY=near 10 | # export FACTORY=registrar 11 | 12 | export MAX_GAS=300000000000000 13 | 14 | if [ -z ${NEAR_ACCT+x} ]; then 15 | export NEAR_ACCT=croncat.$FACTORY 16 | else 17 | export NEAR_ACCT=$NEAR_ACCT 18 | fi 19 | 20 | export CRON_ACCOUNT_ID=manager_v1.$NEAR_ACCT 21 | export REWARDS_ACCOUNT_ID=rewards.$NEAR_ACCT 22 | export COUNTER_ACCOUNT_ID=counter.$NEAR_ACCT 23 | export AGENT_ACCOUNT_ID=agent.$NEAR_ACCT 24 | export USER_ACCOUNT_ID=user.$NEAR_ACCT 25 | export CRUD_ACCOUNT_ID=crudcross.$NEAR_ACCT 26 | export DAO_ACCOUNT_ID=croncat.sputnikv2.$FACTORY 27 | 28 | # Check all configs first 29 | near view $REWARDS_ACCOUNT_ID version 30 | 31 | # Config things 32 | # near call $REWARDS_ACCOUNT_ID update_settings '{"pixelpet_account_id": "pixeltoken.near"}' --accountId $REWARDS_ACCOUNT_ID --gas $MAX_GAS 33 | 34 | # Test automated distro 35 | # 0.0251 payment needed 36 | # near call $CRON_ACCOUNT_ID create_task '{"contract_id": "'$REWARDS_ACCOUNT_ID'","function_id": "pet_check_task_ownership","cadence": "0 */1 * * * *","recurring": true,"deposit": "0","gas": 120000000000000, "arguments": "eyJ0YXNrX2hhc2giOiIxZjVMZHBqdGtLUHJRN2dvUDNUSUNXQ2Q5ZjZwMzhYNWNibXJqam9HdHVvPSJ9"}' --accountId $USER_ACCOUNT_ID --amount 0.0251 --gas $MAX_GAS 37 | 38 | # Do quick test of pet distro 39 | # near call $REWARDS_ACCOUNT_ID pet_check_task_ownership '{"task_hash": "TBD"}' --accountId $USER_ACCOUNT_ID --gas $MAX_GAS 40 | 41 | echo "Cron $NEAR_ENV Bootstrap Complete" 42 | -------------------------------------------------------------------------------- /scripts/simple_bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Uncomment the desired network 3 | export NEAR_ENV=testnet 4 | # export NEAR_ENV=mainnet 5 | # export NEAR_ENV=guildnet 6 | # export NEAR_ENV=betanet 7 | 8 | export FACTORY=testnet 9 | # export FACTORY=near 10 | # export FACTORY=registrar 11 | 12 | export MAX_GAS=300000000000000 13 | 14 | if [ -z ${NEAR_ACCT+x} ]; then 15 | export NEAR_ACCT=croncat.$FACTORY 16 | else 17 | export NEAR_ACCT=$NEAR_ACCT 18 | fi 19 | 20 | export CRON_ACCOUNT_ID=manager_v1.$NEAR_ACCT 21 | export REWARDS_ACCOUNT_ID=rewards_v1.$NEAR_ACCT 22 | export COUNTER_ACCOUNT_ID=counter.$NEAR_ACCT 23 | export AGENT_ACCOUNT_ID=agent.$NEAR_ACCT 24 | export USER_ACCOUNT_ID=user.$NEAR_ACCT 25 | export CRUD_ACCOUNT_ID=crudcross.$NEAR_ACCT 26 | export DAO_ACCOUNT_ID=croncat.sputnikv2.$FACTORY 27 | 28 | # Check all configs first 29 | near view $CRON_ACCOUNT_ID version 30 | near view $CRON_ACCOUNT_ID get_info 31 | 32 | # # UnPause the manager (only turn on for rapid testing, otherwise the main flow will go through DAO) 33 | # near call $CRON_ACCOUNT_ID update_settings '{ "paused": false }' --accountId $CRON_ACCOUNT_ID --gas $MAX_GAS 34 | 35 | # # Assign ownership to the DAO 36 | # near call $CRON_ACCOUNT_ID update_settings '{ "owner_id": "'$DAO_ACCOUNT_ID'", "paused": false }' --accountId $CRON_ACCOUNT_ID --gas $MAX_GAS 37 | 38 | # # Register the "tick" task, as the base for regulating BPS 39 | # near call $CRON_ACCOUNT_ID create_task '{"contract_id": "'$CRON_ACCOUNT_ID'","function_id": "tick","cadence": "0 0 * * * *","recurring": true,"deposit": "0","gas": 9000000000000}' --accountId $CRON_ACCOUNT_ID --amount 10 40 | 41 | # Register "increment" task, for doing basic cross-contract test 42 | near call $CRON_ACCOUNT_ID create_task '{"contract_id": "'$COUNTER_ACCOUNT_ID'","function_id": "increment","cadence": "0 */1 * * * *","recurring": true,"deposit": "0","gas": 5000000000000}' --accountId $COUNTER_ACCOUNT_ID --amount 10 43 | 44 | # # Register "tick" from crud example 45 | # near call $CRON_ACCOUNT_ID create_task '{"contract_id": "'$CRUD_ACCOUNT_ID'","function_id": "tick","cadence": "0 */5 * * * *","recurring": true,"deposit": "0","gas": 10000000000000}' --accountId $CRUD_ACCOUNT_ID --amount 10 46 | 47 | # Check the tasks were setup right: 48 | near view $CRON_ACCOUNT_ID get_tasks 49 | 50 | # Register 1 agent 51 | # near call $CRON_ACCOUNT_ID register_agent '{"payable_account_id": "'$USER_ACCOUNT_ID'"}' --accountId $USER_ACCOUNT_ID --amount 0.00484 52 | # near view $CRON_ACCOUNT_ID get_agent '{"account_id": "'$USER_ACCOUNT_ID'"}' 53 | # near call $CRON_ACCOUNT_ID register_agent '{"payable_account_id": "'$AGENT_ACCOUNT_ID'"}' --accountId $AGENT_ACCOUNT_ID --amount 0.00484 54 | # near view $CRON_ACCOUNT_ID get_agent '{"account_id": "'$AGENT_ACCOUNT_ID'"}' 55 | 56 | # # Agent check for first task 57 | # near view $CRON_ACCOUNT_ID get_agent_tasks '{"account_id": "'$USER_ACCOUNT_ID'"}' 58 | # near view $CRON_ACCOUNT_ID get_slot_tasks 59 | 60 | # # Call the first task 61 | # near call $CRON_ACCOUNT_ID proxy_call --accountId $USER_ACCOUNT_ID --gas $MAX_GAS 62 | 63 | # # Pause the manager 64 | # near call $CRON_ACCOUNT_ID update_settings '{ "paused": true }' --accountId $CRON_ACCOUNT_ID --gas $MAX_GAS 65 | 66 | # Insane battery of tasks to test multiple agents 67 | # near call $CRON_ACCOUNT_ID create_task '{"contract_id": "'$COUNTER_ACCOUNT_ID'","function_id": "increment","cadence": "0 */2 * * * *","recurring": true,"deposit": "0","gas": 2400000000000}' --accountId $COUNTER_ACCOUNT_ID --amount 0.5 68 | # near call $CRON_ACCOUNT_ID create_task '{"contract_id": "'$COUNTER_ACCOUNT_ID'","function_id": "increment","cadence": "0 */3 * * * *","recurring": true,"deposit": "0","gas": 2400000000000}' --accountId $COUNTER_ACCOUNT_ID --amount 0.5 69 | # near call $CRON_ACCOUNT_ID create_task '{"contract_id": "'$COUNTER_ACCOUNT_ID'","function_id": "increment","cadence": "0 */4 * * * *","recurring": true,"deposit": "0","gas": 2400000000000}' --accountId $COUNTER_ACCOUNT_ID --amount 0.5 70 | # near call $CRON_ACCOUNT_ID create_task '{"contract_id": "'$COUNTER_ACCOUNT_ID'","function_id": "increment","cadence": "0 */5 * * * *","recurring": true,"deposit": "0","gas": 2400000000000}' --accountId $COUNTER_ACCOUNT_ID --amount 0.5 71 | # near call $CRON_ACCOUNT_ID create_task '{"contract_id": "'$COUNTER_ACCOUNT_ID'","function_id": "increment","cadence": "0 */6 * * * *","recurring": true,"deposit": "0","gas": 2400000000000}' --accountId $COUNTER_ACCOUNT_ID --amount 0.5 72 | # near call $CRON_ACCOUNT_ID create_task '{"contract_id": "'$COUNTER_ACCOUNT_ID'","function_id": "increment","cadence": "0 */7 * * * *","recurring": true,"deposit": "0","gas": 2400000000000}' --accountId $COUNTER_ACCOUNT_ID --amount 0.5 73 | # near call $CRON_ACCOUNT_ID create_task '{"contract_id": "'$COUNTER_ACCOUNT_ID'","function_id": "increment","cadence": "0 */8 * * * *","recurring": true,"deposit": "0","gas": 2400000000000}' --accountId $COUNTER_ACCOUNT_ID --amount 0.5 74 | # near call $CRON_ACCOUNT_ID create_task '{"contract_id": "'$COUNTER_ACCOUNT_ID'","function_id": "increment","cadence": "0 */9 * * * *","recurring": true,"deposit": "0","gas": 2400000000000}' --accountId $COUNTER_ACCOUNT_ID --amount 0.5 75 | 76 | # echo "" 77 | # echo "Start your agents, waiting 1m to onboard another agent..." 78 | # echo "" 79 | 80 | # sleep 1m 81 | # near call $CRON_ACCOUNT_ID tick --accountId $CRON_ACCOUNT_ID 82 | 83 | echo "Cron $NEAR_ENV Bootstrap Complete" -------------------------------------------------------------------------------- /scripts/testnet_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file is used for starting a fresh set of all contracts & configs 3 | set -e 4 | 5 | if [ -d "res" ]; then 6 | echo "" 7 | else 8 | mkdir res 9 | fi 10 | 11 | cd "`dirname $0`" 12 | 13 | if [ -z "$KEEP_NAMES" ]; then 14 | export RUSTFLAGS='-C link-arg=-s' 15 | else 16 | export RUSTFLAGS='' 17 | fi 18 | 19 | # build the things 20 | cargo build --all --target wasm32-unknown-unknown --release 21 | cp ../target/wasm32-unknown-unknown/release/*.wasm ./res/ 22 | 23 | # Uncomment the desired network 24 | export NEAR_ENV=testnet 25 | 26 | export FACTORY=testnet 27 | 28 | if [ -z ${NEAR_ACCT+x} ]; then 29 | # you will need to change this to something you own 30 | export NEAR_ACCT=croncat.$FACTORY 31 | else 32 | export NEAR_ACCT=$NEAR_ACCT 33 | fi 34 | 35 | export CRON_ACCOUNT_ID=manager_v1.$NEAR_ACCT 36 | export REWARDS_ACCOUNT_ID=rewards.$NEAR_ACCT 37 | export COUNTER_ACCOUNT_ID=counter.$NEAR_ACCT 38 | export AGENT_ACCOUNT_ID=agent.$NEAR_ACCT 39 | export USER_ACCOUNT_ID=user.$NEAR_ACCT 40 | export CRUD_ACCOUNT_ID=crud.$NEAR_ACCT 41 | export VIEWS_ACCOUNT_ID=views.$NEAR_ACCT 42 | export DAO_ACCOUNT_ID=dao.sputnikv2.$FACTORY 43 | 44 | ###### 45 | # NOTE: All commands below WORK, just have them off for safety. 46 | ###### 47 | 48 | ## clear and recreate all accounts 49 | # near delete $CRON_ACCOUNT_ID $NEAR_ACCT 50 | # near delete $COUNTER_ACCOUNT_ID $NEAR_ACCT 51 | # near delete $AGENT_ACCOUNT_ID $NEAR_ACCT 52 | # near delete $USER_ACCOUNT_ID $NEAR_ACCT 53 | # near delete $CRUD_ACCOUNT_ID $NEAR_ACCT 54 | # near delete $VIEWS_ACCOUNT_ID $NEAR_ACCT 55 | 56 | 57 | ## create all accounts 58 | # near create-account $CRON_ACCOUNT_ID --masterAccount $NEAR_ACCT 59 | # near create-account $COUNTER_ACCOUNT_ID --masterAccount $NEAR_ACCT 60 | # near create-account $AGENT_ACCOUNT_ID --masterAccount $NEAR_ACCT 61 | # near create-account $USER_ACCOUNT_ID --masterAccount $NEAR_ACCT 62 | # near create-account $CRUD_ACCOUNT_ID --masterAccount $NEAR_ACCT 63 | # near create-account $VIEWS_ACCOUNT_ID --masterAccount $NEAR_ACCT 64 | 65 | 66 | # Deploy all the contracts to their rightful places 67 | # near deploy --wasmFile ./res/manager.wasm --accountId $CRON_ACCOUNT_ID --initFunction new --initArgs '{}' 68 | # near deploy --wasmFile ./res/rust_counter_tutorial.wasm --accountId $COUNTER_ACCOUNT_ID 69 | # near deploy --wasmFile ./res/cross_contract.wasm --accountId $CRUD_ACCOUNT_ID --initFunction new --initArgs '{"cron": "'$CRON_ACCOUNT_ID'"}' 70 | # near deploy --wasmFile ./res/views.wasm --accountId $VIEWS_ACCOUNT_ID 71 | 72 | 73 | # RE:Deploy all the contracts to their rightful places 74 | # near deploy --wasmFile ./res/manager.wasm --accountId $CRON_ACCOUNT_ID 75 | # near deploy --wasmFile ./res/rust_counter_tutorial.wasm --accountId $COUNTER_ACCOUNT_ID 76 | # near deploy --wasmFile ./res/cross_contract.wasm --accountId $CRUD_ACCOUNT_ID 77 | # near deploy --wasmFile ./res/rewards.wasm --accountId $REWARDS_ACCOUNT_ID 78 | 79 | # near call $CRON_ACCOUNT_ID calc_balances --accountId $CRON_ACCOUNT_ID --gas 300000000000000 80 | 81 | near view $CRON_ACCOUNT_ID version 82 | near view $CRON_ACCOUNT_ID get_info 83 | 84 | echo "Testnet Deploy Complete" -------------------------------------------------------------------------------- /scripts/triggers_bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Uncomment the desired network 3 | export NEAR_ENV=testnet 4 | # export NEAR_ENV=mainnet 5 | # export NEAR_ENV=guildnet 6 | # export NEAR_ENV=betanet 7 | 8 | export FACTORY=testnet 9 | # export FACTORY=near 10 | # export FACTORY=registrar 11 | 12 | export MAX_GAS=300000000000000 13 | 14 | if [ -z ${NEAR_ACCT+x} ]; then 15 | export NEAR_ACCT=croncat.$FACTORY 16 | else 17 | export NEAR_ACCT=$NEAR_ACCT 18 | fi 19 | 20 | export CRON_ACCOUNT_ID=manager_v1.$NEAR_ACCT 21 | export REWARDS_ACCOUNT_ID=rewards.$NEAR_ACCT 22 | export COUNTER_ACCOUNT_ID=counter.$NEAR_ACCT 23 | export AGENT_ACCOUNT_ID=agent.$NEAR_ACCT 24 | export USER_ACCOUNT_ID=user.$NEAR_ACCT 25 | export CRUD_ACCOUNT_ID=crudcross.$NEAR_ACCT 26 | export VIEWS_ACCOUNT_ID=views.$NEAR_ACCT 27 | export DAO_ACCOUNT_ID=croncat.sputnikv2.$FACTORY 28 | 29 | # Register an agent 30 | # near call $CRON_ACCOUNT_ID register_agent '{"payable_account_id": "'$AGENT_ACCOUNT_ID'"}' --accountId $AGENT_ACCOUNT_ID --amount 0.00484 31 | 32 | # Create a task 33 | # near call $CRON_ACCOUNT_ID create_task '{"contract_id": "'$COUNTER_ACCOUNT_ID'","function_id": "increment","cadence": "0 0 * 12 * *","recurring": true,"deposit": "0","gas": 4000000000000}' --accountId $USER_ACCOUNT_ID --amount 10 34 | 35 | # get hash from above 36 | # near call $CRON_ACCOUNT_ID create_trigger '{"contract_id": "'$VIEWS_ACCOUNT_ID'","function_id": "get_a_boolean","task_hash":"rmBCOb1CyeypKqIu6QIpozATq5zYXAU/KHUVXD6wI14="}' --accountId $USER_ACCOUNT_ID --amount 0.000017 37 | near call $CRON_ACCOUNT_ID create_trigger '{"contract_id": "'$VIEWS_ACCOUNT_ID'","function_id": "get_a_boolean","task_hash":"or3Wdi4yq2idU90Zrrg3/T0iCogfIBV2O7ruwQSjt/I="}' --accountId $COUNTER_ACCOUNT_ID --amount 0.000017 38 | 39 | # VSB8VDqS8QgmTTCTuvt5q9BiXLUnv77AJxwBWZIO7U4= 40 | near view $CRON_ACCOUNT_ID get_triggers '{"from_index": "0", "limit": "100"}' 41 | 42 | # # do a view check 43 | # near view $VIEWS_ACCOUNT_ID get_a_boolean 44 | 45 | # # Make the actual proxy view+call 46 | # near call $CRON_ACCOUNT_ID proxy_conditional_call '{"trigger_hash": "VSB8VDqS8QgmTTCTuvt5q9BiXLUnv77AJxwBWZIO7U4"}' --accountId $AGENT_ACCOUNT_ID --gas 300000000000000 47 | 48 | # sleep 1m 49 | 50 | # # Do AGAIN just in case we were on odd minute 51 | # near call $CRON_ACCOUNT_ID proxy_conditional_call '{"trigger_hash": "VSB8VDqS8QgmTTCTuvt5q9BiXLUnv77AJxwBWZIO7U4"}' --accountId $AGENT_ACCOUNT_ID --gas 300000000000000 52 | 53 | echo "Trigger sample complete" 54 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./build.sh 3 | cargo test -- --nocapture --------------------------------------------------------------------------------