├── .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
--------------------------------------------------------------------------------